• (四)库存超卖案例实战——优化redis分布式锁


    前言

    在上一节内容中,我们已经实现了使用redis分布式锁解决商品“超卖”的问题,本节内容是对redis分布式锁的优化。在上一节的redis分布式锁中,我们的锁有俩个可以优化的问题。第一,锁需要实现可重入,同一个线程不用重复去获取锁;第二,锁没有续期功能,导致业务没有执行完成就已经释放了锁,存在一定的并发访问问题。本案例中通过使用redis的hash数据结构实现可重入锁,使用Timer实现锁的续期功能,完成redis分布式锁的优化。最后,我们通过集成第三方redisson工具包,完成分布式锁以上俩点的优化内容。Redisson提供了简单易用的API,使得开发人员可以轻松地在分布式环境中使用Redis。

    正文

    • 加锁的lua脚本:使用exists和hexists指令判断是否存在锁,如果不存在或者存在锁并且该锁下面的field有值,就使用hincrby指令使锁的值加1,实现可重入,否则直接返回0,加锁失败。
    1. if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
    2. "then " +
    3. " redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
    4. " redis.call('expire', KEYS[1], ARGV[2]) " +
    5. " return 1 " +
    6. "else " +
    7. " return 0 " +
    8. "end"
    • 解锁的lua脚本: 使用hexists指令判断是否存在锁,如果为0,代表没有对应field字段的锁,直接返回nil;如果使用hincrby指令使锁field字段锁的值减少1之后值为0,代表锁已经不在占用,可以删除该锁;否则直接返回0,代表是可重入锁,锁还没有释放。
    1. if redis.call('hexists', KEYS[1], ARGV[1]) == 0 " +
    2. "then " +
    3. " return nil " +
    4. "elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 " +
    5. "then " +
    6. " return redis.call('del', KEYS[1]) " +
    7. "else " +
    8. " return 0 " +
    9. "end"
    •  实现续期的lua脚本:使用hexists指令判断锁的field值是否存在,如果值为1存在,则将该锁的过期时间更新,否则直接返回0,代表没有找到该锁,续期失败。
    1. if redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
    2. "then " +
    3. " return redis.call('expire', KEYS[1], ARGV[2]) " +
    4. "else " +
    5. " return 0 " +
    6. "end";
    • 创建一个自定义的锁工具类MyRedisDistributeLock,实现加锁、解锁、续期功能

    - MyRedisDistributeLock实现

    1. package com.ht.atp.plat.util;
    2. import org.jetbrains.annotations.NotNull;
    3. import org.springframework.data.redis.core.StringRedisTemplate;
    4. import org.springframework.data.redis.core.script.DefaultRedisScript;
    5. import java.util.Arrays;
    6. import java.util.Timer;
    7. import java.util.TimerTask;
    8. import java.util.UUID;
    9. import java.util.concurrent.TimeUnit;
    10. import java.util.concurrent.locks.Condition;
    11. import java.util.concurrent.locks.Lock;
    12. public class MyRedisDistributeLock implements Lock {
    13. public MyRedisDistributeLock(StringRedisTemplate redisTemplate, String lockName, long expire) {
    14. this.redisTemplate = redisTemplate;
    15. this.lockName = lockName;
    16. this.expire = expire;
    17. this.uuid = getId();
    18. }
    19. /**
    20. * redis工具类
    21. */
    22. private StringRedisTemplate redisTemplate;
    23. /**
    24. * 锁名称
    25. */
    26. private String lockName;
    27. /**
    28. * 过期时间
    29. */
    30. private Long expire;
    31. /**
    32. * 锁的值
    33. */
    34. private String uuid;
    35. @Override
    36. public void lock() {
    37. this.tryLock();
    38. }
    39. @Override
    40. public void lockInterruptibly() {
    41. }
    42. @Override
    43. public boolean tryLock() {
    44. try {
    45. return this.tryLock(-1L, TimeUnit.SECONDS);
    46. } catch (InterruptedException e) {
    47. e.printStackTrace();
    48. }
    49. return false;
    50. }
    51. @Override
    52. public boolean tryLock(long time, @NotNull TimeUnit unit) throws InterruptedException {
    53. if (time != -1) {
    54. this.expire = unit.toSeconds(time);
    55. }
    56. String script = "if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
    57. "then " +
    58. " redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
    59. " redis.call('expire', KEYS[1], ARGV[2]) " +
    60. " return 1 " +
    61. "else " +
    62. " return 0 " +
    63. "end";
    64. while (!this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire))) {
    65. Thread.sleep(50);
    66. }
    67. // //加锁成功后,自动续期
    68. this.renewExpire();
    69. return true;
    70. }
    71. @Override
    72. public void unlock() {
    73. String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 " +
    74. "then " +
    75. " return nil " +
    76. "elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 " +
    77. "then " +
    78. " return redis.call('del', KEYS[1]) " +
    79. "else " +
    80. " return 0 " +
    81. "end";
    82. Long flag = this.redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuid);
    83. if (flag == null) {
    84. throw new IllegalMonitorStateException("this lock doesn't belong to you!");
    85. }
    86. }
    87. @NotNull
    88. @Override
    89. public Condition newCondition() {
    90. return null;
    91. }
    92. /**
    93. * 给线程拼接唯一标识
    94. *
    95. * @return
    96. */
    97. private String getId() {
    98. return UUID.randomUUID() + "-" + Thread.currentThread().getId();
    99. }
    100. private void renewExpire() {
    101. String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
    102. "then " +
    103. " return redis.call('expire', KEYS[1], ARGV[2]) " +
    104. "else " +
    105. " return 0 " +
    106. "end";
    107. new Timer().schedule(new TimerTask() {
    108. @Override
    109. public void run() {
    110. System.out.println("-------------------");
    111. Boolean flag = redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire));
    112. if (flag) {
    113. renewExpire();
    114. }
    115. }
    116. }, this.expire * 1000 / 3);
    117. }
    118. }

    - 实现加锁功能

    - 实现解锁功能


     - 使用Timer实现锁的续期功能

    • 使用MyRedisDistributeLock实现库存的加锁业务 

    - 使用自定义MyRedisDistributeLock工具类实现加锁业务

    1. public void checkAndReduceStock() {
    2. //1.获取锁
    3. MyRedisDistributeLock myRedisDistributeLock = new MyRedisDistributeLock(stringRedisTemplate, "stock", 10);
    4. myRedisDistributeLock.lock();
    5. try {
    6. // 2. 查询库存数量
    7. String stockQuantity = stringRedisTemplate.opsForValue().get("P0001");
    8. // 3. 判断库存是否充足
    9. if (stockQuantity != null && stockQuantity.length() != 0) {
    10. Integer quantity = Integer.valueOf(stockQuantity);
    11. if (quantity > 0) {
    12. // 4.扣减库存
    13. stringRedisTemplate.opsForValue().set("P0001", String.valueOf(--quantity));
    14. }
    15. } else {
    16. System.out.println("该库存不存在!");
    17. }
    18. } finally {
    19. myRedisDistributeLock.unlock();
    20. }
    21. }

    - 启动服务7000、7001、7002,压测优化后的自定义分布式锁:平均访问时间362ms,吞吐量每秒246,库存扣减为0,表明优化后的分布式锁是可用的。

    • 集成redisson工具包,使用第三方工具包实现分布式锁,完成并发访问“超卖”问题案例演示
    1. org.redisson
    2. redisson-spring-boot-starter
    3. 3.11.6
    • 创建一个redisson配置类,引入redisson客户端工具
    1. package com.ht.atp.plat.config;
    2. import org.redisson.Redisson;
    3. import org.redisson.api.RedissonClient;
    4. import org.redisson.config.Config;
    5. import org.springframework.context.annotation.Bean;
    6. import org.springframework.context.annotation.Configuration;
    7. @Configuration
    8. public class MyRedissonConfig {
    9. @Bean
    10. RedissonClient redissonClient() {
    11. Config config = new Config();
    12. config.useSingleServer()
    13. .setAddress("redis://192.168.110.88:6379");
    14. //配置看门狗的默认超时时间为30s,供续期使用
    15. config.setLockWatchdogTimeout(30000);
    16. return Redisson.create(config);
    17. }
    18. }
    • 使用Redisson锁实现“超卖”业务方法 
    1. //可重入锁
    2. @Override
    3. public void checkAndReduceStock() {
    4. // 1.加锁,获取锁失败重试
    5. RLock lock = this.redissonClient.getLock("lock");
    6. lock.lock();
    7. try {
    8. // 2. 查询库存数量
    9. String stockQuantity = stringRedisTemplate.opsForValue().get("P0001");
    10. // 3. 判断库存是否充足
    11. if (stockQuantity != null && stockQuantity.length() != 0) {
    12. Integer quantity = Integer.valueOf(stockQuantity);
    13. if (quantity > 0) {
    14. // 4.扣减库存
    15. stringRedisTemplate.opsForValue().set("P0001", String.valueOf(--quantity));
    16. }
    17. } else {
    18. System.out.println("该库存不存在!");
    19. }
    20. } finally {
    21. // 4.释放锁
    22. lock.unlock();
    23. }
    24. }
    • 开启7000、7001、7002服务,压测扣减库存接口 

    - 压测结果:平均访问时间222ms,吞吐量为384每秒

    - 库存扣减结果为0

    结语

    综上所述,无论是自定义分布式锁还是使用redisson工具类,都能实现分布式锁解决并发访问的“超卖问题”,redisson工具使用集成更加方便简洁,推荐使用redisson工具包。本节内容到这里就结束了,我们下期见。。。。。。

  • 相关阅读:
    《微信小程序-进阶篇》Lin-ui组件库源码分析-列表组件List(三)
    s905l3a系列刷armbian 教你从0搭建自己的博客
    老风控的心声:风控的“痛”与“恨”|内卷当下,单做好风控已远远不够
    nosql之问什么在你 ,答什么在我
    《Java虚拟机原理图解》5. JVM类加载器机制与类加载过程
    微信公众号开发:网页授权
    Linux 标准IO
    LeetCode:1402. 做菜顺序、2316. 统计无向图中无法互相到达点对数
    Java.lang.Class类 getConstructors()方法有什么功能呢?
    Unity Shader第二章作业
  • 原文地址:https://blog.csdn.net/yprufeng/article/details/134077909