• 分布式锁2:基于redis实现分布式锁


    一 redis实现分布式锁

    1.1 原理

    利用redis中的命令setnx(key, value),key不存在就新增,存在就什么都不做。同时有多个客户端发送setnx命令,只有一个客户端可以成功,返回1(true);其他的客户端返回0(false)。

    二 redis使用senx命令实现分布式锁

    2.1 操作案例

    1.核心代码

    1. public void deductByRedis() {
    2. // 加锁setnx
    3. // 加锁,获取锁失败重试
    4. while (!this.stringRedisTemplate.opsForValue().setIfAbsent("ljf-lock", "111")){
    5. try {
    6. Thread.sleep(40);
    7. } catch (InterruptedException e) {
    8. e.printStackTrace();
    9. }
    10. }
    11. //加锁成功
    12. try {
    13. // 先查询库存是否充足
    14. Stock stock = this.stockMapper.selectById(1L);
    15. // 再减库存
    16. if (stock != null && stock.getCount() > 0){
    17. stock.setCount(stock.getCount() - 1);
    18. this.stockMapper.updateById(stock);
    19. }
    20. else{
    21. System.out.println("库存不足..........."+new Date());
    22. }
    23. } finally {
    24. // 解锁
    25. this.stringRedisTemplate.delete("ljf-lock");
    26. }
    27. }

    2.controller修改调用service的方法

    2.2 测试

    2.2.1.启动zk

    [root@localhost ]  cd  /root/export/apache-zookeeper-3.7.0-bin/bin

    [root@localhost bin]# ./zkServer.sh start
    ZooKeeper JMX enabled by default
    Using config: /root/export/apache-zookeeper-3.7.0-bin/bin/../conf/zoo.cfg
    Starting zookeeper ... STARTED
    [root@localhost bin]# ./zkCli.sh

    2.2.2.启动redis

    [root@localhost ~]# redis-server /myredis/redis.conf
    [root@localhost ~]# redis-cli -a 123456 -p 6379
    Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.

    2.2.3.启动nginx

    2.2.4.启动服务

    2.2.5.jemterces

    1.配置

    2.执行 

    2.2.6.查看数据库

    1.初始

    2.测试后

    2.3 存在问题

    1.存在死锁问题:setnx刚刚获取到锁,当前服务器宕机,导致del释放锁无法执行,进而导致锁无法锁无法释放(死锁)

    解决:给锁设置过期时间,自动释放锁。

    设置过期时间两种方式:

    1. 通过expire设置过期时间(缺乏原子性:如果在setnx和expire之间出现异常,锁也无法释放)

    2. 使用set指令设置过期时间:set key value ex 3 nx(既达到setnx的效果,又设置了过期时间)

    2.防误删:可能会释放其他服务器的锁。  

    场景:如果业务逻辑的执行时间是7s。执行流程如下

    1. index1业务逻辑没执行完,3秒后锁被自动释放。

    2. index2获取到锁,执行业务逻辑,3秒后锁被自动释放。

    3. index3获取到锁,执行业务逻辑

    4. index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁,导致index3的业务只执行1s就被别人释放。

      最终等于没锁的情况。

    解决:setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁

    问题:删除操作缺乏原子性。

    场景:

    1. index1执行删除时,查询到的lock值确实和uuid相等

    2. index1执行删除前,lock刚好过期时间已到,被redis自动释放

    3. index2获取了lock

    4. index1执行删除,此时会把index2的lock删除

    解决方案:没有一个命令可以同时做到判断 + 删除,所有只能通过其他方式实现(LUA脚本)

    3.使用lua保证删除原子性

    4.重入性:redis + Hash

    Redis 提供了 Hash (哈希表)这种可以存储键值对数据结构。所以我们可以使用 Redis Hash 存储的锁的重入次数,然后利用 lua 脚本判断逻辑。  

    5.自动续期

    luna脚本:

    2.对应方法

    三 redis使用senx+lua脚本实现分部署锁

    3.1 逻辑流程

    加锁:

    1. setnx:独占排他 死锁、不可重入、原子性

      . set k v ex 30 nx:独占排他、死锁 不可重入

    2. hash + lua脚本:可重入锁

      1. 判断锁是否被占用(exists),如果没有被占用则直接获取锁(hset/hincrby)并设置过期时间(expire)

      2. 如果锁被占用,则判断是否当前线程占用的(hexists),如果是则重入(hincrby)并重置过期时间(expire)

      3. 否则获取锁失败,将来代码中重试

    3. Timer定时器 + lua脚本:实现锁的自动续期

      判断锁是否自己的锁(hexists == 1),如果是自己的锁则执行expire重置过期时间

    解锁

    1. hash + lua脚本:可重入

      1. 判断当前线程的锁是否存在,不存在则返回nil,将来抛出异常

      2. 存在则直接减1(hincrby -1),判断减1后的值是否为0,为0则释放锁(del),并返回1

      3. 不为0,则返回0

    重试:递归 循环

    3.2  操作步骤

    1.客户端

    1. @Component
    2. public class DistributedLockClient {
    3. @Autowired
    4. private StringRedisTemplate redisTemplate;
    5. private String uuid;
    6. public DistributedLockClient() {
    7. this.uuid = UUID.randomUUID().toString();
    8. }
    9. public DistributedRedisLock getRedisLock(String lockName){
    10. return new DistributedRedisLock(redisTemplate, lockName, uuid);
    11. }
    12. }

    2.分布锁

    1. public class DistributedRedisLock implements Lock {
    2. private StringRedisTemplate redisTemplate;
    3. private String lockName;
    4. private String uuid;
    5. private long expire = 30;
    6. public DistributedRedisLock(StringRedisTemplate redisTemplate, String lockName, String uuid) {
    7. this.redisTemplate = redisTemplate;
    8. this.lockName = lockName;
    9. this.uuid = uuid + ":" + Thread.currentThread().getId();
    10. }
    11. @Override
    12. public void lock() {
    13. this.tryLock();
    14. }
    15. @Override
    16. public void lockInterruptibly() throws InterruptedException {
    17. }
    18. @Override
    19. public boolean tryLock() {
    20. try {
    21. return this.tryLock(-1L, TimeUnit.SECONDS);
    22. } catch (InterruptedException e) {
    23. e.printStackTrace();
    24. }
    25. return false;
    26. }
    27. /**
    28. * 加锁方法
    29. * @param time
    30. * @param unit
    31. * @return
    32. * @throws InterruptedException
    33. */
    34. @Override
    35. public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
    36. if (time != -1){
    37. this.expire = unit.toSeconds(time);
    38. }
    39. String script = "if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
    40. "then " +
    41. " redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
    42. " redis.call('expire', KEYS[1], ARGV[2]) " +
    43. " return 1 " +
    44. "else " +
    45. " return 0 " +
    46. "end";
    47. while (!this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire))){
    48. Thread.sleep(50);
    49. }
    50. // 加锁成功,返回之前,开启定时器自动续期
    51. this.renewExpire();
    52. return true;
    53. }
    54. /**
    55. * 解锁方法
    56. */
    57. @Override
    58. public void unlock() {
    59. String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 " +
    60. "then " +
    61. " return nil " +
    62. "elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 " +
    63. "then " +
    64. " return redis.call('del', KEYS[1]) " +
    65. "else " +
    66. " return 0 " +
    67. "end";
    68. Long flag = this.redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuid);
    69. if (flag == null){
    70. throw new IllegalMonitorStateException("this lock doesn't belong to you!");
    71. }
    72. }
    73. @Override
    74. public Condition newCondition() {
    75. return null;
    76. }
    77. // String getId(){
    78. // return this.uuid + ":" + Thread.currentThread().getId();
    79. // }
    80. private void renewExpire(){
    81. String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
    82. "then " +
    83. " return redis.call('expire', KEYS[1], ARGV[2]) " +
    84. "else " +
    85. " return 0 " +
    86. "end";
    87. new Timer().schedule(new TimerTask() {
    88. @Override
    89. public void run() {
    90. if (redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire))) {
    91. renewExpire();
    92. }
    93. }
    94. }, this.expire * 1000 / 3);
    95. }
    96. }

    3.调用

    1. @Autowired
    2. private DistributedLockClient distributedLockClient;
    3. public void deductByRedisLua() {
    4. DistributedRedisLock redisLock = this.distributedLockClient.getRedisLock("lua-lock");
    5. redisLock.lock();
    6. //加锁成功
    7. try {
    8. // 先查询库存是否充足
    9. Stock stock = this.stockMapper.selectById(1L);
    10. // 再减库存
    11. if (stock != null && stock.getCount() > 0){
    12. stock.setCount(stock.getCount() - 1);
    13. this.stockMapper.updateById(stock);
    14. }
    15. else{
    16. System.out.println("库存不足..........."+new Date());
    17. }
    18. //重入锁
    19. test();
    20. } finally {
    21. // 解锁
    22. redisLock.unlock();
    23. }
    24. }
    25. public void test(){
    26. DistributedRedisLock redisLock = this.distributedLockClient.getRedisLock("lua-lock");
    27. redisLock.lock();;
    28. System.out.println("可重入锁.....");
    29. redisLock.unlock();
    30. }

    4.controller

    3.3  测试

     3.2.1.启动zk

    [root@localhost ]  cd  /root/export/apache-zookeeper-3.7.0-bin/bin

    [root@localhost bin]# ./zkServer.sh start
    ZooKeeper JMX enabled by default
    Using config: /root/export/apache-zookeeper-3.7.0-bin/bin/../conf/zoo.cfg
    Starting zookeeper ... STARTED
    [root@localhost bin]# ./zkCli.sh

    3.2.2.启动redis

    [root@localhost ~]# redis-server /myredis/redis.conf
    [root@localhost ~]# redis-cli -a 123456 -p 6379
    Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.

    3.2.3.启动nginx

    3.2.4.启动服务

    3.2.5.jemterces

    1.配置

    2.执行 

    3.2.6.查看数据库

    1.初始

    2.测试后

    查看

     

  • 相关阅读:
    宠物网页作业HTML 大一作业HTML宠物网页作业 web期末大作业HTML 动物网页作业HTML HTML制作宠物网页作业css
    Postman关联
    [英雄星球六月集训LeetCode解题日报] 第29日 分治
    贴近摄影测量,如何让平遥古城焕发生机?
    高低电平触发,(边沿触发)上升沿触发和下降沿触发 中断区别
    【JAVA】值传递与引用传递
    drf之--认证组件、权限组件(django项目国际化)、频率组件、排序
    java计算商场折扣 判断体重 判断学生成绩等级 验证邮箱 demo
    数组19—unshift() :将一个或多个元素添加到数组的开头
    About Estimation Statistics
  • 原文地址:https://blog.csdn.net/u011066470/article/details/133780162