|
|
在小程序登陆的时候,在`MiniAppAuthenticationProvider`中我们看到这样一行代码
|
|
|
|
|
|
``java
|
|
|
yamiUserDetailsService.insertUserIfNecessary(appConnect);
|
|
|
```
|
|
|
|
|
|
这便是商城用户创建的代码,在`YamiUserServiceImpl#insertUserIfNecessary()`方法中,有一个这样的注解
|
|
|
|
|
|
```java
|
|
|
@RedisLock(lockName = "insertUser", key = "#appConnect.appId + ':' + #appConnect.bizUserId")
|
|
|
```
|
|
|
|
|
|
这里便用了分布式锁,为什么我们要在这里使用锁?分布式锁又是什么?
|
|
|
|
|
|
- 由于用户是通过登录直接注册的,如果一个用户在不刻意之间,又或者前端写的东西有点问题,这就会导致整个系统创建了两个相同的用户,这是非常危险的事情,所以创建用户这里必须加锁。
|
|
|
- 至于为什么使用分布式锁,是因为我们虽然没有用上spring cloud、dubbo之类的东西,实际上我们也是希望我们的商城可以多实例部署的,也就是可以搞分布式的。因此用了分布式锁
|
|
|
|
|
|
分布式锁,简单来说就是锁,而且还是适合分布式环境的。分布式说起来也很奇怪,要是有什么不能共享的东西,那就抽出来共享。比如本地数据缓存不能共享,那么就抽出一个如redis之类的东西,进行共享。session不能共享,那么就将session抽出来,丢到redis之类的东西,又能共享了。
|
|
|
|
|
|
锁不能共享,同样可以丢一个标记到redis,由于redis是单线程的,所以也不用担心redis的线程安全的问题。这个标记就是一个锁的标记,那样你就实现了分布式锁...
|
|
|
|
|
|
我们看回`@RedisLock` 该类,里面有个`expire()`方法
|
|
|
|
|
|
```java
|
|
|
/**
|
|
|
* 过期毫秒数,默认为5000毫秒
|
|
|
*
|
|
|
* @return 锁的时间
|
|
|
*/
|
|
|
int expire() default 5000;
|
|
|
```
|
|
|
|
|
|
由于网络稳定、宕机等各种原因,分布式锁,必须要有过期时间,否则锁无法释放的话,会阻塞一片的实例。
|
|
|
|
|
|
## 实现一个简单的分布式锁注解
|
|
|
|
|
|
由于自己去实现redis的分布式锁,是比较困难的问题,还要考虑redis复制,宕机之类的问题,所以我们使用一个比较优秀的开源项目 **redisson**来实现我们的分布式锁
|
|
|
|
|
|
被`@RedisLock`所注解的方法,会被 `RedisLockAspect` 进行切面管理,代码如下:
|
|
|
|
|
|
```java
|
|
|
@Around("@annotation(redisLock)")
|
|
|
public Object around(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable {
|
|
|
String spel = redisLock.key();
|
|
|
String lockName = redisLock.lockName();
|
|
|
// redissonClient 也就是通过redisson 进行对锁管理
|
|
|
RLock rLock = redissonClient.getLock(getRedisKey(joinPoint,lockName,spel));
|
|
|
|
|
|
rLock.lock(redisLock.expire(),redisLock.timeUnit());
|
|
|
|
|
|
Object result = null;
|
|
|
try {
|
|
|
//执行方法
|
|
|
result = joinPoint.proceed();
|
|
|
|
|
|
} finally {
|
|
|
rLock.unlock();
|
|
|
}
|
|
|
return result;
|
|
|
}
|
|
|
```
|
|
|
|
|
|
## 识别spel表达式
|
|
|
|
|
|
在`@RedisLock(lockName = "insertUser", key = "#appConnect.appId + ':' + #appConnect.bizUserId")`中 `#appConnect.appId` 也仅仅是表示一串字符串而已,而能将其变成表达式,需要一定的转换`SpelUtil.parse`
|
|
|
|
|
|
```java
|
|
|
/**
|
|
|
* 支持 #p0 参数索引的表达式解析
|
|
|
* @param rootObject 根对象,method 所在的对象
|
|
|
* @param spel 表达式
|
|
|
* @param method ,目标方法
|
|
|
* @param args 方法入参
|
|
|
* @return 解析后的字符串
|
|
|
*/
|
|
|
public static String parse(Object rootObject,String spel, Method method, Object[] args) {
|
|
|
if (StrUtil.isBlank(spel)) {
|
|
|
return StrUtil.EMPTY;
|
|
|
}
|
|
|
//获取被拦截方法参数名列表(使用Spring支持类库)
|
|
|
StandardReflectionParameterNameDiscoverer standardReflectionParameterNameDiscoverer = new StandardReflectionParameterNameDiscoverer();
|
|
|
String[] paraNameArr = standardReflectionParameterNameDiscoverer.getParameterNames(method);
|
|
|
if (ArrayUtil.isEmpty(paraNameArr)) {
|
|
|
return spel;
|
|
|
}
|
|
|
//使用SPEL进行key的解析
|
|
|
ExpressionParser parser = new SpelExpressionParser();
|
|
|
//SPEL上下文
|
|
|
StandardEvaluationContext context = new MethodBasedEvaluationContext(rootObject,method,args,standardReflectionParameterNameDiscoverer);
|
|
|
//把方法参数放入SPEL上下文中
|
|
|
for (int i = 0; i < paraNameArr.length; i++) {
|
|
|
context.setVariable(paraNameArr[i], args[i]);
|
|
|
}
|
|
|
return parser.parseExpression(spel).getValue(context, String.class);
|
|
|
}
|
|
|
```
|
|
|
|
|
|
同时我们也害怕redis的key发生冲突,所以会对key加上一些统一的前缀:
|
|
|
|
|
|
redis 锁的key能够识别`spel` 表达式,并且不和其他方法的锁名称或缓存名称重复
|
|
|
|
|
|
```java
|
|
|
/**
|
|
|
* 将spel表达式转换为字符串
|
|
|
* @param joinPoint 切点
|
|
|
* @return redisKey
|
|
|
*/
|
|
|
private String getRedisKey(ProceedingJoinPoint joinPoint,String lockName,String spel) {
|
|
|
Signature signature = joinPoint.getSignature();
|
|
|
MethodSignature methodSignature = (MethodSignature) signature;
|
|
|
Method targetMethod = methodSignature.getMethod();
|
|
|
Object target = joinPoint.getTarget();
|
|
|
Object[] arguments = joinPoint.getArgs();
|
|
|
return REDISSON_LOCK_PREFIX + lockName + StrUtil.COLON + SpelUtil.parse(target,spel, targetMethod, arguments);
|
|
|
}
|
|
|
```
|
|
|
|