最近項目中用到比較多的redis分布式鎖
每個方法都類似於這樣
String key = "";
//嘗試加鎖
if (! jedisManager.tryLock(key)) {
throw new BizException("請稍后重試");
}
try {
//do your biz
}
catch (Exception e) {
throw e;
}
finally {
//釋放鎖
jedisManager.release(key);
}
非常的麻煩,而且每個人有每個人的寫法。所以,決定將分布式鎖與業務進行分離,便於我們以后后續開發
我們需要定義一個分布式鎖注解(RedisLock),分布式鎖aop,分布式鎖對象基類(LockDomian)
RedisLock
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RedisLock {
RedisLocKeyEnum bizKey();
/**
* 默認毫秒
* @return
*/
int expire() default 15000;
/**
* 默認給前端的提示
* @return
*/
String errorMsg() default "請稍后重試";
}
LockDomian
@Slf4j
public class LockDomain {
public static String KEY = "model = [%s], logKey = [%s] : [%s]";
public String redisKey() {
throw new BizException("請重寫你的分布式鎖對象的redisKey方法");
}
/**
* 建議繼承的類重寫這個方法 方便日志查找
* @return
*/
public String logKey() {
return String.valueOf(this.hashCode());
}
public void tryLockSucLog(MutexModelEnum model) {
log.info(String.format(KEY, model.getCode(), this.logKey(), "獲取鎖成功"));
}
public void tryLockFaildLog(MutexModelEnum model) {
log.info(String.format(KEY, model.getCode(), this.logKey(), "獲取鎖失敗"));
}
public void releaseLog(MutexModelEnum model) {
log.info(String.format(KEY, model.getCode(), this.logKey(), "釋放鎖成功"));
}
public void bizError(MutexModelEnum model) {
log.info(String.format(KEY, model.getCode(), this.logKey(), "業務異常"));
}
}
RedisLockAspect
@Slf4j
@Order(1)
@Aspect
@Component
public class RedisLockAspect {
@Autowired
private JedisComponent jedisComponent;
@Resource
private Validator validator;
@Around("@annotation(com.csy.core.aop.RedisLock)")
public Result around(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Method targetMethod = AopUtils.getMostSpecificMethod(method, joinPoint.getTarget().getClass());
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
String message = BeanValidators.validateWithErrorMessage(validator, arg);
if (StringUtils.isNotBlank(message)) {
return Result.wrapErrorResult(message);
}
}
RedisLock redisLock = AnnotationUtils.findAnnotation(targetMethod, RedisLock.class);
if (redisLock == null) {
return Data.wrapErrorResult("框架異常");
}
if (! (args[0] instanceof LockDomain)) {
return Data.wrapErrorResult("請繼承LockDomain");
}
LockDomain lockObj = (LockDomain) args[0];
String key = redisLock.bizKey().getCode() + "_" + lockObj.redisKey();
try {
if (! jedisComponent.tryLock(key, redisLock.expire())) {
lockObj.tryLockFaildLog(redisLock.bizKey());
return Result.wrapErrorResult(redisLock.errorMsg());
}
lockObj.tryLockSucLog(redisLock.bizKey());
return joinPoint.proceed();
}
catch (Throwable e) {
lockObj.bizError(redisLock.bizKey());
//參數異常捕獲
if (e instanceof ParamException) {
log.error("ParamException:", e);
ParamException paramException = (ParamException) e;
return Data.wrapErrorResult(paramException.getError().getErrorCode(), paramException.getError().getErrorMsg());
}
//自定義異常捕獲
if (e instanceof BizException) {
log.error("BizException", e);
BizException bizException = (BizException) e;
return Data.wrapErrorResult(bizException.getError().getErrorCode(), bizException.getError().getErrorMsg());
}
log.error("系統異常:", e);
return Result.wrapErrorResult(ErrorCode.SERVER_ERROR);
}
finally {
lockObj.releaseLog(redisLock.bizKey());
jedisComponent.delKey(key);
}
}
}
分布式鎖業務實現
public class RedisLockDemo {
@RedisLock(bizKey = MutexModelEnum.TEST)
public Result<Boolean> RedisLockTest(RedisLockTestRequest request) {
//do your biz
return Result.success(true);
}
@Getter
@Setter
public class RedisLockTestRequest extends LockDomain {
private Long userId;
private String userName;
/**
* 以userId作為分布式鎖的key
* @return
*/
@Override
public String redisKey() {
return String.valueOf(this.userId);
}
@Override
public String logKey() {
return String.valueOf(this.userId);
}
}
}
可以看到。我們只要在方法上加上@RedisLock,指定鎖的Model,再對入參繼承LockDomain,指定redisKey和logKey就行。
注意事項
aop相關問題
有的同學可能想降低鎖的粒度或者單純的想抽出一個方法。比如:
public void a(RedisLockTestRequest request) {
this.RedisLockTest(request);
}
@RedisLock(bizKey = MutexModelEnum.TEST)
public Result<Boolean> RedisLockTest(RedisLockTestRequest request) {
//do your biz
return Result.success(true);
}
這種情況下,外部調用a方法,aop是不起作用的
因為aop是運行時織入,獲取調用的目標方法(也就是a),判斷是否是切點,再匹配切面。內部方法(不是目標方法)不進行織入。
如何解決上述情況?
我們使用AopContext
public void a(RedisLockTestRequest request) {
RedisLockDemo currentProxy = (RedisLockDemo) AopContext.currentProxy();
currentProxy.RedisLockTest(request);
}
@RedisLock(bizKey = MutexModelEnum.TEST)
public Result<Boolean> RedisLockTest(RedisLockTestRequest request) {
//do your biz
return Result.success(true);
}
通過AopContext獲取到當前的代理類,然后調用。
上述涉及到的aop原理我會后續出個專門的aop淺析。
分布式鎖相關問題
可能網上有一些分布式鎖它是先setnx 然后 expire。其實是有問題的。因為它不是一個原子操作。
我們應該使用Jedis類下的set方法。一步設置值和過期時間
public String set(final String key, final String value, final String nxxx, final String expx, final long time) {
checkIsInMulti();
client.set(key, value, nxxx, expx, time);
return client.getStatusCodeReply();
}