如何用注解来实现加锁?

356 阅读5分钟

哈喽,这里是最锋利的矛,书接上回 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)

大家多多点赞、关注求求了🥺