Redis分散ロック

4995 ワード

前にプロジェクトを引き継いで、ピットを踏んだ経験を記録して、一緒に勉強します.(急いでいる学生は直接底のコードを持ってもいいです)
まず先輩の元のコードを見てみましょう:分かりやすくて、私はここで論理を簡略化しました:
public String redisLock() {
        String lockKey = "ke";
        String clintId = UUID.randomUUID().toString();
        try {
            //   ,  true,     , false,     , redis  setNx   
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "lock", 10, TimeUnit.SECONDS);
            if (!flag) {
                return "    ";
            }
            Integer stock = Integer.valueOf(stringRedisTemplate.opsForValue().get("stock"));
            //          lock     ,           ,             
            // try {
            // Thread.sleep(11000);
            // } catch (InterruptedException e) {
            // e.printStackTrace();
            // }
            //    
            if (stock > 0) {
                stock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", stock.toString());
                log.info("      ,    :{}", stock);
            } else {
                log.info("    !!!");
            }
        } catch (Exception e) {
            log.info("    ", e);
        } finally {
            //    
            stringRedisTemplate.delete(lockKey);
        }
        return "end";
    }

コードは簡単ですが、2つの問題が隠されています.
1、ロックが固定されている、つまりredisに保存されている値が死んでいる.これにより、スレッドを持つロックが他のスレッドによって誤って削除され、新しいスレッドがロックを再取得することに成功する.
2、ネットワークの極端な条件の下で、業務コードの実行時間が10秒を超えた場合、つまりロックのタイムアウト時間であれば、ロックはredisによって消去され、新しいスレッドがロックを再取得することに成功する(ここでは、現在のスレッドをしばらく眠らせ、極端な条件をシミュレートする場合を強制することができる).
次は私が変更したコードです.
    //     
    public static final Integer EXPIRETIME = 10;

    //     ,      ,     redis  
    @Autowired
    private DaemonThread daemonThread;

    private static Thread thread;

    @RequestMapping("lock")
    public String redisLock() {
        String lockKey = "ke";
        String clintId = UUID.randomUUID().toString();
        try {
            Boolean flag =
                stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clintId, EXPIRETIME, TimeUnit.SECONDS);
            //                ,         ,                  
            if (thread != null) {
                boolean isAlive = thread.isAlive();
                if (!isAlive) {
                    thread = new Thread(daemonThread);
                    thread.setDaemon(true);
                    thread.start();
                }
            } else {
                thread = new Thread(daemonThread);
                thread.setDaemon(true);
                thread.start();
            }
            if (!flag) {
                return "    ";
            }
            Integer stock = Integer.valueOf(stringRedisTemplate.opsForValue().get("stock"));
            //          lock     ,           ,             
            // try {
            // Thread.sleep(11000);
            // } catch (InterruptedException e) {
            // e.printStackTrace();
            // }
            if (stock > 0) {
                stock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", stock.toString());
                log.info("      ,    :{}", stock);
            } else {
                log.info("    !!!");
            }
        } catch (Exception e) {
            log.info("    ", e);
        } finally {
            //             
            if (clintId.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(lockKey))) {
                stringRedisTemplate.delete(lockKey);
            }
        }
        return "end";
    }

解決策はこうです.
1、各スレッドが取得するロックを設定するのは唯一(唯一のアルゴリズムについては、自分で発揮することができますが、ここではプレゼンテーションのみ)、こちらのclientIdです.ロックを削除するときに削除したロックを判断するのが、前に設定したロックです.
2、1つのデーモンスレッドを開いてロックのタイムアウト時間を判断し、ロックが生存し、タイムアウト時間が多くなければ、ロックのタイムアウト時間を再設定し、ロックがタイムアウトで期限切れにならないことを保証する.
総括して、このロジックは実は既成のオープンソースの実現があって、Redisson、コードの簡単な暴力、実は原理は第2部分で、興味のある学生はソースコードをめくることができます:
    @Autowired
    private RedissonClient redissonClient;

    @RequestMapping("lock")
    public String redisLock() {
        String lockKey = "ke";
        String clintId = UUID.randomUUID().toString();
        RLock lock = redissonClient.getLock(lockKey);
        try {
            lock.tryLock(10, TimeUnit.SECONDS);
            Integer stock = Integer.valueOf(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                stock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", stock.toString());
                log.info("      ,    :{}", stock);
            } else {
                log.info("    !!!");
            }
        } catch (Exception e) {
            log.info("    ", e);
        } finally {
            lock.unlock();
        }
        return "end";
    }