基于Redisson和自定义注解的分布式锁
为什么要使用到分布式锁?
可以想象一下电商系统中的库存扣减场景:
public void reduceStock(Long productId, int quantity) {
// 1. 查询当前库存
int stock = productDao.getStock(productId);
// 2. 检查库存是否充足
if (stock < quantity) {
throw new RuntimeException("库存不足");
}
// 3. 扣减库存
productDao.updateStock(productId, stock - quantity);
}
在单机环境中,这段代码没有问题。但在分布式环境中,当多个实例同时处理同一个商品的订单时:
- 两个服务实例同时查询到库存为10
- 都判断库存充足
- 都执行库存扣减(10-1=9)
- 最终库存为9,但实际应该扣减两次变为8
这就是并发问题我们需要一种机制确保同一时间只有一个服务实例能执行关键操作。
通过Redisson实现分布式锁
Redisson是一个基于Redis的Java客户端,提供了丰富的分布式对象和服务
示例代码:
public void reduceStock() {
// 获取分布式锁
RLock lock = redissonClient.getLock("product_lock:" + productId);
try {
// 尝试获取锁,等待时间5秒,锁有效期30秒
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
// 获取锁成功,执行业务逻辑
reduceStock(productId, quantity);
}
} finally {
// 释放锁
lock.unlock();
}
}
通过示例代码我们需要手动编写获取锁、判断锁以及释放锁的逻辑,这样的代码重复且冗长。为了简化这一过程,我们引入了基于注解的分布式锁,通过一个注解就可以实现获取锁、判断锁、处理完成后释放锁的逻辑。这样可以大大简化代码,提高开发效率。
实现方式
分布式锁常量
/**
* 分布式锁常量
*
* @author yefeng
*/
public class DistributeLockConstant {
public static final String NONE_KEY = "NONE";
public static final String DEFAULT_OWNER = "DEFAULT";
public static final int DEFAULT_EXPIRE_TIME = -1;
public static final int DEFAULT_WAIT_TIME = -1;
}
自定义注解
为了避免每个方法都写重复的锁代码,我们创建自定义注解,设置这个注解只能用在方法上面:
/**
* 分布式锁注解
*
* @author yefeng
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributeLock {
/**
* 锁的场景
*/
public String scene();
/**
* 加锁的key,优先取key(),如果没有,则取keyExpression()
*/
public String key() default DistributeLockConstant.NONE_KEY;
/**
* SPEL表达式:
* <pre>
* #id
* #insertResult.id
* </pre>
*/
public String keyExpression() default DistributeLockConstant.NONE_KEY;
/**
* 超时时间,毫秒
* 默认情况下不设置超时时间,会自动续期
*/
public int expireTime() default DistributeLockConstant.DEFAULT_EXPIRE_TIME;
/**
* 加锁等待时长,毫秒
* 默认情况下不设置等待时长,不做等待
*/
public int waitTime() default DistributeLockConstant.DEFAULT_WAIT_TIME;
}
AOP切面
我们可以通过AOP的环绕通知,去切面我们自定义的注解
/**
* 分布式锁切面
*
* @author yefeng
*/
@Aspect
@Component
public class DistributeLockAspect {
private RedissonClient redissonClient;
public DistributeLockAspect(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
private static final Logger LOG = LoggerFactory.getLogger(DistributeLockAspect.class);
@Around("@annotation(com.yefneg.lock.DistributeLock)")
public Object process(ProceedingJoinPoint pjp) throws Exception {
Object response = null;
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
DistributeLock distributeLock = method.getAnnotation(DistributeLock.class);
String key = distributeLock.key();
if (DistributeLockConstant.NONE_KEY.equals(key)) {
if (DistributeLockConstant.NONE_KEY.equals(distributeLock.keyExpression())) {
throw new DistributeLockException("no lock key found...");
}
SpelExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(distributeLock.keyExpression());
EvaluationContext context = new StandardEvaluationContext();
// 获取参数值
Object[] args = pjp.getArgs();
// 获取运行时参数的名称
StandardReflectionParameterNameDiscoverer discoverer
= new StandardReflectionParameterNameDiscoverer();
String[] parameterNames = discoverer.getParameterNames(method);
// 将参数绑定到context中
if (parameterNames != null) {
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
}
// 解析表达式,获取结果
key = String.valueOf(expression.getValue(context));
}
String scene = distributeLock.scene();
String lockKey = scene + "#" + key;
int expireTime = distributeLock.expireTime();
int waitTime = distributeLock.waitTime();
RLock rLock = redissonClient.getLock(lockKey);
boolean lockResult = false;
if (waitTime == DistributeLockConstant.DEFAULT_WAIT_TIME) {
if (expireTime == DistributeLockConstant.DEFAULT_EXPIRE_TIME) {
LOG.info(String.format("lock for key : %s", lockKey));
rLock.lock();
} else {
LOG.info(String.format("lock for key : %s , expire : %s", lockKey, expireTime));
rLock.lock(expireTime, TimeUnit.MILLISECONDS);
}
lockResult = true;
} else {
if (expireTime == DistributeLockConstant.DEFAULT_EXPIRE_TIME) {
LOG.info(String.format("try lock for key : %s , wait : %s", lockKey, waitTime));
lockResult = rLock.tryLock(waitTime, TimeUnit.MILLISECONDS);
} else {
LOG.info(String.format("try lock for key : %s , expire : %s , wait : %s", lockKey, expireTime, waitTime));
lockResult = rLock.tryLock(waitTime, expireTime, TimeUnit.MILLISECONDS);
}
}
if (!lockResult) {
LOG.warn(String.format("lock failed for key : %s , expire : %s", lockKey, expireTime));
throw new DistributeLockException("acquire lock failed... key : " + lockKey);
}
try {
LOG.info(String.format("lock success for key : %s , expire : %s", lockKey, expireTime));
response = pjp.proceed();
} catch (Throwable e) {
throw new Exception(e);
} finally {
rLock.unlock();
LOG.info(String.format("unlock for key : %s , expire : %s", lockKey, expireTime));
}
return response;
}
}
使用注解
这样我们就只需要在方法上,加上我们的自定义注解,只需要关注业务逻辑,无需要关心锁的实现代码。
@Service
public class ProductService {
@DistributedLock(key = "'product_lock:' + #productId")
public void reduceStock(Long productId, int quantity) {
// 这里是业务逻辑,无需关心锁的实现
int stock = productDao.getStock(productId);
if (stock < quantity) {
throw new RuntimeException("库存不足");
}
productDao.updateStock(productId, stock - quantity);
}
}