哈喽,这里是最锋利的矛,书接上回 damn,又差一点点被干掉了 - 掘金 (juejin.cn)
解决了线上问题后,不禁想到,对于每个加分布式锁的地方都要修改这些代码吗? 这改动也太大了,就算我同意我小姨子也不会同意啊。
有什么更好的方法呢?在思考后还是想到得上注解最为合适,功在当代,利在千秋嘛。
ok,话不多说,开始干活
定义基础类
既然要用注解,那么第一步要做的肯定是要定义注解,而在我们项目中,普遍面对的场景就是对入参对象内单个或批量业务id进行加锁,在定义锁的key时,我们定义为 模块:LOCK:业务id
既然分为单个和批量加锁,我们定出两个注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Lock {
// 所属模块
LockKeyEnum keyEnum();
// 锁的键名,可以根据业务需求动态生成
String key();
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BatchLock {
// 所属模块
LockKeyEnum keyEnum();
// 锁的键名,可以根据业务需求动态生成
String key();
}
模块我们定义为一个枚举,生成锁key的方法定义在枚举里面
@Getter
@AllArgsConstructor
public enum LockKeyEnum {
PRODUCT("PRODUCT:LOCK", "商品模块"),
ORDER("ORDER:LOCK", "订单模块"),
;
private String name;
private String desc;
public String getKey(Object... args) {
StringBuffer key = new StringBuffer(name);
for (Object arg : args) {
if (null != arg && !"".equals(arg)) {
key.append(":").append(arg);
}
}
return key.toString();
}
}
切面实现
单个加锁切面实现
@Order(5)
@Aspect
@Component
public class LockAspect {
@Resource
private RedissonClient redissonClient;
// 超时时间10秒,可放在配置中心
private static final Integer WAITE_TIME = 10;
// 60秒等待事件,可放在配置中心
private static final Integer LEASE_TIME = 60;
@Around("@annotation(lock)")
public Object around(ProceedingJoinPoint pjp, Lock lock) throws Throwable {
Object[] args = pjp.getArgs();
if (Objects.nonNull(args)) {
return pjp.proceed(args);
}
Object key = SpelUtil.parse(pjp, lock.key());
RLock rLock = redissonClient.getLock(lock.keyEnum().getKey(key));
boolean locked;
try {
// 尝试获取锁
locked = rLock.tryLock(WAITE_TIME, LEASE_TIME, TimeUnit.SECONDS);
if (!locked) {
throw new RuntimeException("获取锁失败");
}
// 注册事务同步监听器,确保在事务结束时释放锁
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCompletion(int status) {
// status 0表示正常提交,1表示回滚
if (rLock.isLocked() && rLock.isHeldByCurrentThread()) {
// 仅在事务成功提交或回滚后释放锁
rLock.unlock();
}
}
});
// 在这里,我们假设事务由Spring管理,因此直接调用目标方法
return pjp.proceed();
} catch (Throwable e) {
throw e;
}
}
}
批量加锁切面实现
@Order(5)
@Aspect
@Component
public class BatchLockAspect {
@Resource
private RedissonClient redissonClient;
// 超时时间10秒,可放在配置中心
private static final Integer WAITE_TIME = 10;
// 60秒等待事件,可放在配置中心
private static final Integer LEASE_TIME = 60;
@Around("@annotation(batchLock)")
public Object around(ProceedingJoinPoint pjp, BatchLock batchLock) throws Throwable {
Object[] args = pjp.getArgs();
if (Objects.nonNull(args)) {
return pjp.proceed(args);
}
Object keys = SpelUtil.parse(pjp, batchLock.key());
Collection collection = null;
if (keys instanceof Collection) {
collection = (Collection) keys;
}
if (collection.isEmpty()) {
return pjp.proceed(args);
}
List<RLock> locks = new ArrayList<>();
try {
// 尝试获取锁
for (Object key : collection) {
RLock lock = redissonClient.getLock(batchLock.keyEnum().getKey(key));
// 尝试获取锁
boolean locked = lock.tryLock(WAITE_TIME, LEASE_TIME, TimeUnit.SECONDS);
if (!locked) {
// 处理锁获取失败的情况
unlockAll(locks); // 释放已获取的锁
throw new RuntimeException("获取锁失败");
}
locks.add(lock);
}
// 在这里,我们假设事务由Spring管理,因此直接调用目标方法
return pjp.proceed();
} catch (Throwable e) {
throw e;
}finally {
// 注册事务同步监听器,确保在事务结束时释放锁
unlockAll(locks);
}
}
// 释放锁的辅助方法,以防万一在未使用TransactionSynchronization时需要
private void unlockAll(List<RLock> locks) {
locks.forEach(lock -> {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCompletion(int status) {
// status 0表示正常提交,1表示回滚
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
// 仅在事务成功提交或回滚后释放锁
lock.unlock();
}
}
});
});
}
}
使用事例
public class Service {
@Lock(module = RedisKeyEnum.ORDER, key = "#req.id")
@Transactional(rollbackFor = Exception.class)
public void single(Req req) {
// 具体业务...
}
@BatchLock(module = RedisKeyEnum.ORDER, key = "#req.ids")
@Transactional(rollbackFor = Exception.class)
public void batch(Req req) {
// 具体业务...
}
}
注意
有同学可能会问了,那我自定义的加锁注解@Lock
和spring注解@Transactional
都加时同一个方法上,虽然切面内释放锁延迟在事物提交之后了,那开启事物和加锁的先后顺序是什么?
如果@Transactional
先于@Lock
执行,那岂不是还会有风险
那我们就要来研究一下Spring里切面的执行顺序了,而他俩的执行顺序依赖于几个因素
- 切面的声明顺序: 如果你没有显式地指定切面的执行顺序,Spring会按照切面类在应用上下文中被声明的顺序来决定它们的执行顺序。这意味着,先声明的切面会比后声明的切面更早执行。
- Ordered接口与@Order注解: 为了精确控制切面的执行顺序,Spring允许切面实现
Ordered
接口或使用@Order
注解来指定执行的优先级。@Order
注解的值越小,该切面的优先级越高,执行顺序也越靠前。默认情况下,如果没有指定@Order
值,其默认顺序值是Integer.MAX_VALUE
,表示最低优先级。 - 类名排序: 当多个切面具有相同的
@Order
值或都没有使用@Order
注解时,Spring会按照切面类名的字典顺序来决定执行顺序。
对于@Transactional
切面,它的默认顺序值通常是Integer.MAX_VALUE
,意味着如果不做特殊配置,它会被视为优先级较低的切面。因此,如果你的Lock
切面有明确的@Order
注解值或者在配置文件中被提前声明,它将会在事务切面前执行。
手动版
但是在实际项目中,我们要加锁的key不一定会在入参中,而是要进行查询后才能得到,但是还要保证加锁和事物的顺序问题,那就只能放大照用手动管理事务+手动加锁的方式了
public class Service {
@Resource
private TransactionTemplate transactionTemplate;
@Resource
private RedissonClient redissonClient;
public void single(Req req) {
// 查询要加锁对象id
Long keyId = //查询逻辑
// 获取锁对象
RLock lock = redissonClient.getLock(lock.keyEnum().getKey(keyId));
try {
// 尝试上锁 超过等待时间失败 持有锁后超时自动释放
MmsAssert.isTrue(lock.tryLock(REDISSON_WAIT_TIME, REDISSON_LEASE_TIME, TimeUnit.SECONDS), "正在操作中,请稍后再试!");
//开启事务
transactionTemplate.execute(status -> {
// 具体业务...
return Boolean.TRUE;
});
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new MmsServiceException(e.getMessage());
} finally {
// 锁释放
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
总结
ok,这期我们介绍了用注解实现对入参中单个以及批量对象进行加锁,至于分布式锁是用什么方案实现是无所谓的,主流的实现方式redis或者zk
至于实现原理可以看我之前的文章哦
还没搞明白分布式锁?我来用最通俗的方式来讲清楚—redis篇 - 掘金 (juejin.cn)
还没搞明白分布式锁?我来用最通俗的方式来讲清楚—zk篇 - 掘金 (juejin.cn)
大家多多点赞、关注求求了🥺