java - 注解实现分布式锁

介绍

分布式锁是分布式环境中解决资源共享问题的一种机制。在一个分布式系统中,有时多个进程,可能会同时访问同一份资源,例如,读写同一个数据库记录,或者获取和修改同一个文件。为了避免并发访问引起的数据不一致问题,我们需要用到分布式锁。一次只有一个进程能够获得锁,进而保证有序地、原子性地操作资源。

使用场景

  1. 幂等性:操作重复提交可能会导致数据的不一致,例如二次点击支付,通过分布式锁可以保证在一段时间内操作的幂等性。
  2. 数据一致性:在微服务或分布式服务中,当多个服务操作同一份数据时,若不加锁,可能会造成数据异常或冲突。通过分布式锁,可以保证在同一时间只有一个服务在操作数据,使得在分布式环境下保证数据的一致性。
  3. 避免超卖:电商中常见的秒杀活动,如果不对商品库存进行锁定,可能会出现超卖现象。使用分布式锁,可以在用户下单时锁定库存,防止超卖。
  4. 有序访问:有时需要对某项资源进行有序访问,例如悲观锁策略,或者对于某些特定设备,例如打印机,只能允许一个任务进行处理,避免任务交叉执行导致的问题。

实现

编程实现

原始写法

通常,当我们实现非分布式锁时,都是遵循同一个模板代码,

RLock lock = redissonClient.getLock(lockKey);
boolean sucess = lock.tryLock();
try {
    // 业务代码
    ...
} finally {
    // 解锁
    lock.unlock();
}

即先获取锁,然后尝试加锁,加锁成功之后执行业务代码,最后释放锁。
试想,如果,如果项目中使用分布式锁的场景很多,那岂不是要编写很多这种重复代码,我们可不可以将模板代码抽象出来,只专注于业务代码的编写,提高开发效率呢?答案是肯定的。

进阶写法

将模板代码抽象出来,使用函数式接口来包裹业务代码,这样实现分布式锁时只需要专注于编写业务代码

@Service
public class LockService {

    @Autowired
    private RedissonClient redissonClient;

    @SneakyThrows
    public <T> T executeWithLock(String lockKey, int waitTime, TimeUnit timeUnit, Supplier<T> supplier) {
        RLock lock = redissonClient.getLock(lockKey);
        boolean sucess = lock.tryLock(waitTime, timeUnit);
        try {
            return supplier.get();
        } finally {
            lock.unlock();
        }
    }

上述代码将分布式锁的模板代码抽象出来,这样在使用分布式锁时只需要调用lockService中的executeWithLock方法即可。
使用的示例代码如下:

lockService.executeWithLock("acquireItem", 5, TimeUnit.SECONDS, () -> {
    //业务代码
    ...
});

其实这样编写已经很简洁了,减少了很多重复代码的编写,但是编写出的代码还是有一点“刻意”在里面,给人的感觉就是这里在刻意的使用分布式锁,隐隐地还是带着一点模板代码的意思。我们可不可以直接在方法中编写业务代码,不再调用其它地方的代码实现分布式锁呢?答案是肯定的,这就是下面要介绍的注解实现分布式锁。

注解实现

其实注解实现分布式锁做的主要工作就是根据方法的入参来获取分布式锁的key和waitTime。
首先我们要编写一个注解:

@Target(ElementType.METHOD)
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
public @interface RedissionLock {

    /**
     * key的前缀
     * @return
     */
    String prefix() default "";

    /**
     * SpEL表达式形式的key
     * @return
     */
    String key() default "";

    /**
     * 锁的过期时间
     * @return
     */
    int waitTime() default -1;

    /**
     * 过期时间的单位,默认是秒
     * @return
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
}

然后编写一个切面来在执行业务代码之前通过方法入参来获取分布式锁的key,

@Slf4j
@Component
@Aspect
@Order(0)// 确保比事务注解先执行
public class RedissionLockAspect {

    @Autowired
    private LockService lockService;

    @Around("@annotation(redissionLock)")
    public Object around(ProceedingJoinPoint joinPoint, RedissionLock redissionLock){
        // 通过jointPoint 获取注解对应的Method
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        String prefix = StringUtils.isBlank(redissionLock.prefix()) ? SpElUtil.getMethodKey(method) : redissionLock.prefix();
        String key = SpElUtil.parseSpEl(method, joinPoint.getArgs(), redissionLock.key());
        return lockService.executeWithLock(prefix + key, redissionLock.waitTime(), redissionLock.timeUnit(), joinPoint::proceed);
    }
}

注意需要在类上添加注解:@Aspect,@Order(0),其中,@Order(0)注解是保证切面比事务注解先执行。
代码中,SpringElUtil是支持SpringEL表达式从方法入参中获取key,代码如下:

@Component
public class SpElUtil {

    private static final ExpressionParser PARSE = new SpelExpressionParser();
    private static final DefaultParameterNameDiscoverer PARAMETER_NAME_DISCOVERERA = new DefaultParameterNameDiscoverer();

    public static String parseSpEl(Method method, Object[] args, String spEl) {
        String[] params = Optional.ofNullable(PARAMETER_NAME_DISCOVERERA.getParameterNames(method)).orElse(new String[]{});//解析参数名
        EvaluationContext context = new StandardEvaluationContext();//el解析需要的上下文对象
        for (int i = 0; i < params.length; i++) {
            context.setVariable(params[i], args[i]);//所有参数都作为原材料扔进去
        }
        // 解析springEl表达式
        Expression expression = PARSE.parseExpression(spEl);
        return expression.getValue(context, String.class);
    }
}

springEl表达式从方法中取出参数的原理是,现将方法参数放置到一个容器中,然后,通过springEl参数解析器来解析表达式中指定格式的参数。
具体可参考:java获取方法入参

怎么在代码中使用注解实现分布式锁?直接在方法上使用注解即可。
示例代码如下:

@RedissionLock(key = "#itemPotent", waitTime = 5)
public void doAcquireItem(Long uid, Long itemId, String itemPotent) {
    // 判断幂等
    UserBackpack userBackpack = userBackpackMapper.getItemByItenPotent(itemPotent);
    if(Objects.nonNull(userBackpack)){
        return;
    }
    // 发放物品
    UserBackpack insert = UserAdapter.buildSendItemUserBack(uid, itemId, itemPotent);
    userBackpackMapper.insert(insert);
}

这样,在doAcquireItem方法代码执行之前,就会先执行切面中的代码,获取分布式锁的key,然后才执行业务代码。

  • 32
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值