• 手写redis分布式锁


    一个靠谱的分布式锁应该有哪些特点?

    1.独占性:任何时候有且仅有一个线程持有锁

    2.放死锁:有超时控制机制或撤销操作,得有个释放锁的兜底方案

    3.不乱抢:不能张冠李戴,不能unlock别人加的锁

    4.可重入性:自己加的锁自己还可以再次获得

    基于setnx命令实现分布式锁,setnx成功返回1,失败返回0

    1.带过期时间的setnx加锁,finally再次手动解锁

    1. public void sale1(){
    2. String key="redisLock";
    3. String uuidValue= IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
    4. while(!redisTemplate.opsForValue().setIfAbsent(key,uuidValue,30L, TimeUnit.SECONDS)){
    5. try {
    6. Thread.sleep(20L);
    7. } catch (InterruptedException e) {
    8. throw new RuntimeException(e);
    9. }
    10. }
    11. try {
    12. String result = (String)redisTemplate.opsForValue().get("inventory001");
    13. int inventorNum =result==null?0:Integer.parseInt(result);
    14. if(inventorNum>0){
    15. redisTemplate.opsForValue().set("inventory001",String.valueOf(--inventorNum));
    16. log.info("卖出一个商品,剩余库存:{}",inventorNum);
    17. }else {
    18. log.info("商品买完了");
    19. }
    20. } finally {
    21. redisTemplate.delete(key);
    22. }
    23. }

    通过setnx命令加锁,set成功执行业务逻辑,set失败,sleep20毫秒继续抢锁。所有代码执行完,再在finally中手动释放锁。

    问题:如果业务执行时间过长,锁已经自己过期了,业务代码还在执行,其它线程就能拿到锁进来了,第一个线程执行完后执行finally释放锁,就把第二个线程加的锁释放掉了。

    改进:

    2.判断是自己加的锁后再删除,判断和删除操作用lua脚本实现原子性

    1. public void sale2(){
    2. String key="redisLock";
    3. String uuidValue= IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
    4. while(!redisTemplate.opsForValue().setIfAbsent(key,uuidValue,30L, TimeUnit.SECONDS)){
    5. try {
    6. Thread.sleep(20L);
    7. } catch (InterruptedException e) {
    8. throw new RuntimeException(e);
    9. }
    10. }
    11. try {
    12. String result = (String)redisTemplate.opsForValue().get("inventory001");
    13. int inventorNum =result==null?0:Integer.parseInt(result);
    14. if(inventorNum>0){
    15. redisTemplate.opsForValue().set("inventory001",String.valueOf(--inventorNum));
    16. log.info("卖出一个商品,剩余库存:{}",inventorNum);
    17. }else {
    18. log.info("商品买完了");
    19. }
    20. } finally {
    21. String luaScript="if(redis.call('get',KEYS[1])==ARGV[1]) then return redis.call('del',KEYS[1]) else return 0 end";
    22. redisTemplate.execute(new DefaultRedisScript(luaScript, Boolean.class), Arrays.asList(key),uuidValue);
    23. }
    24. }

     finally删除锁时判断是自己的锁后再删除。判断删除操作通过lua脚本实现原子性。

    lau脚本:

    if(redis.call('get',KEYS[1])==ARGV[1]) then return redis.call('del',KEYS[1]) else return 0 end

     解释:如果get key的值为自己的uuidValue才执行del key,否则返回0

    还存在问题:不支持可重入性

    改进:

    3.使用redis的hash结构实现锁的可重入性

    hash结构:k k v 依次是lockName  uuid:ThreadId  重入次数

    加锁lua逻辑:

    1. if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then
    2. redis.call('hincrby',KEYS[1],ARGV[1],1)
    3. redis.call('expire',KEYS[1],ARGV[2])
    4. return 1
    5. else
    6. return 0
    7. end

     先判断lockName这个分布式锁是否存在:

            返回0,不存在,新建属于自己的锁 lockName uuId:ThreadId 1

            返回1,存在,再判断是不是自己的锁(是否存在自己的uuId:ThreadId)

                    返回0,不存在,最终返回0,end

                    返回1,存在,自己的锁自增1

    解锁lau脚本:

    1. if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then
    2. return nil
    3. elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then
    4. return redis.call('del',KEYS[1])
    5. else
    6. return 0
    7. end

    判断是否有分布式锁且是自己的锁 

            否,返回nil

            是,再将自己的锁重入次数-1,-1后重入次数是否为0

                    是,删除整个分布式锁

                    否 ,返回0,end

    问题:还是没有完全实现锁的独占性,当一个线程的执行时间过长,锁自动释放,另一个线程就能拿到锁进来了。

    改进:

    4.每次加锁成功后后台启动一个定时任务,用来锁续期

    1. private void renewExpire(){
    2. String script =
    3. "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " +
    4. "return redis.call('expire',KEYS[1],ARGV[2]) " +
    5. "else " +
    6. "return 0 " +
    7. "end";
    8. String uuidValue=IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
    9. new Timer().schedule(new TimerTask()
    10. {
    11. @Override
    12. public void run()
    13. {
    14. if ((Boolean)redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
    15. renewExpire();
    16. }
    17. }
    18. },(this.expireTime * 1000)/3);
    19. }

     假设expireTime是30秒,加锁成功后再调用这个方法,每10秒执行一次定时任务

  • 相关阅读:
    NeRF数据集介绍
    【目标检测】two-stage------SSP-Net浅析-2014
    【无标题】
    运用程序化交易系统的能力表现在哪些方面?
    一个程序员的水平能差到什么程度?
    如果再写for循环,我就锤自己了
    【提高效率】C++使用map替代传统switch case
    LaTeX 数学公式常见问题及解决方案
    Win11如何给应用换图标?Win11给应用换图标的方法
    使用Python进行Base64编码和解码
  • 原文地址:https://blog.csdn.net/m0_73520938/article/details/140344140