使用Redis实现分布式锁

背景:
spring项目中我们经常使用定时注解@Scheduled结合cron表达式来完成我们的定时任务需求,这种方式在单点状态下是没有问题的,但是实际使用时,为了高可用,我们经常是多节点部署项目,这时就出现了定时任务被多次执行的问题。
我们当然可以通过统一的配置中心配置项来指定具体执行定时任务的实例,但这样就失去了高可用的效果,并且也不利于后期项目交接与维护。所以需要分布式锁,来解决目前的问题。当然也有一些成型的分布式定时任务方案,如XXL等,这里为了方便,我们基于Redis实现一个简单的分布式锁。

需要:
redis(当然)
redisTemplate/jedis

1. 实现思路

每个任务在执行前,先去redis中获得锁,如果获得到了,才可以正常执行定时任务,所谓获得锁,就是去redis中存放一对kv,如果放置成功了,就认为获得到了锁,由于redis是单线程的,所以本身是线程安全的。

1. 加锁

加锁这个部分需要注意的并不多,主要是利用redis的
SET key value NX PX max-lock-time

也就是在存放kv时,如果key不存在,就放置成功,如果key存在,则存放失败,不予更新,并且在存放时设定一个超时时间,防止发生死锁。这个命令映射到redisTemplate里,是setIfAbsent这个api;在jedis中,直接jedis.set(key, value, “NX”, “PX”, timeout)即可。在设定时,key由自己的业务需求设计,value需要一个唯一值,目的是在释放锁时,能够识别出这个锁确实是自己的锁,防止误释放其它实例的锁。这里我使用UUID。代码:

    /**
     * 带超时时间锁定
     * @param lockKey 要锁定的key
     * @param uniqueValue 唯一值(UUID等)
     * @param timeout 超时时间
     * @return Boolean true:锁定成功 false:锁定失败,已由其它实例获得锁
     */
    public Boolean lock(String lockKey, String uniqueValue, Long timeout) {
        return redisUtil.setIfAbsent(lockKey, uniqueValue, timeout);
    }

需要注意的是,千万不能先去set一对kv,再set超时时间,因为我们通过api分步调用这种方式无法保证原子性,一旦set完kv后,程序崩溃了,这个锁就永远存在于redis中,成为死锁。

2. 释放锁

释放锁时,主要是先去通过value校验身份,再把这对kv从redis中del掉。简单来说就是:

	if (get(key) == value) {
	    del(key)
	}

和上锁时一样,我们不能分步完成这个流程,因为我们先去get这个key,再del,无法保证原子性。所以我们使用LUA脚本来完成这个流程,LUA在执行时是可以保证原子性的,代码:

    /**
     * 释放锁
     * @param unlockKey 要解锁的key
     * @param verifyValue 用于验证身份的唯一值(要与上锁时的值一致,用于验证身份,防止误释放其它实例的锁)
     * @return Boolean true:释放成功 false:释放失败,已由其它实例释放,或不是自己的锁
     */
    public Boolean unLock(String unlockKey, String verifyValue) {
        Long SUCCESS = 1L;
        //使用LUA脚本,保证操作原子性
        String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return   redis.call('del',KEYS[1])  else return 0 end";
        Object obj = redisUtil.eval(luaScript, Collections.singletonList(unlockKey), Collections.singletonList(verifyValue));
        return SUCCESS.equals(obj);
    }
3.完整代码
@Component
public class DistributedLockUtil {
    @Autowired
    RedisUtil redisUtil;

    /**
     * 带超时时间锁定
     * @param lockKey 要锁定的key
     * @param uniqueValue 唯一值(UUID等)
     * @param timeout 超时时间
     * @return Boolean true:锁定成功 false:锁定失败,已由其它实例获得锁
     */
    public Boolean lock(String lockKey, String uniqueValue, Long timeout) {
        return redisUtil.setIfAbsent(lockKey, uniqueValue, timeout);
    }

    /**
     * 锁定(建议使用带超时时间锁定,防止发生死锁)
     * @param lockKey 要锁定的key
     * @param uniqueValue 唯一值(UUID等)
     * @return Boolean true:锁定成功 false:锁定失败,已由其它实例获得锁
     */
    public Boolean lock(String lockKey, String uniqueValue) {
        return redisUtil.setIfAbsent(lockKey, uniqueValue);
    }

    /**
     * 释放锁
     * @param unlockKey 要解锁的key
     * @param verifyValue 用于验证身份的唯一值(要与上锁时的值一致,用于验证身份,防止误释放其它实例的锁)
     * @return Boolean true:释放成功 false:释放失败,已由其它实例释放,或不是自己的锁
     */
    public Boolean unLock(String unlockKey, String verifyValue) {
        Long SUCCESS = 1L;
        //使用LUA脚本,保证操作原子性
        String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return   redis.call('del',KEYS[1])  else return 0 end";
        Object obj = redisUtil.eval(luaScript, Collections.singletonList(unlockKey), Collections.singletonList(verifyValue));
        return SUCCESS.equals(obj);
    }
}
4.总结

使用时,我们只需要先成一个随机value,然后通过这个value去lock和unlock就可以了,例如:

	String eqValue = UUID.randomUUID().toString().replace("-", "").toLowerCase();
	distributedLockUtil.lock(RedisKey.key, eqValue, TimeConstant.THREE_HOUR);
	//业务代码
	...
	distributedLockUtil.unLock(RedisKey.key, eqValue);

实际上,如果你的业务和我一样,是为了解决分布式定时问题,而不是同一个实例内部去抢锁,唯一value可以不必真的唯一,比如可以使用ip + port的方法,也能满足需求,这样也可以使api更易于使用。而且在上锁后,只要指定一个合理的超时时间,甚至不需要去释放锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用Redis实现分布式的方法是通过使用setnx命令进行上,del命令进行释放,以及expire命令设置的过期时间。当一个进程或线程需要获取时,它会尝试执行setnx命令将一个特定的键设置为1(表示被获取),如果设置成功,则表示获取到了,如果设置失败,则表示已被其他进程或线程获取。当进程或线程完成任务后,可以使用del命令将该键删除,从而释放。为了防止死被一直持有,还可以使用expire命令设置的过期时间,确保即使没有被主动释放,也能在一定时间后自动过期。这样就实现了基于Redis分布式。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [Redis实现分布式](https://blog.csdn.net/m0_52884709/article/details/127697133)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [分布式实现(一)Redis篇](https://blog.csdn.net/lans_g/article/details/126118046)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值