[案例:]springboot-redis.zip

什么是分布式锁

加锁的目的就是为了互斥访问“临界资源”.(说白了就是同一时刻只允许一个线程访问)分布式环境下如果需要做到对临界资源的互斥访问,就需要加锁,那这把锁就是分布式锁.

如何实现锁

  • 单线程环境下,如果要解决资源竞争问题,可以使用synchronized等解决
  • 多线程环境下我们可以使用redis,zookeeper,数据库等来解决


分布式锁的特点

  • 互斥:和我们本地锁一样互斥性是最基本,但是分布式锁需要保证在不同节点的不同线程的互斥
  • 可重入:同一个节点上的同一个线程如果获取了锁之后那么也可以再次获取这个锁
  • 锁超时:和本地锁一样支持锁超时,防止死锁
  • 高可用:加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级
  • 加锁与解锁是同一个对象

redis如何实现分布式锁

使用redis自带命令实现分布式锁

redis中存在一条命令setnx,如果不存在则更新,对某个资源加锁可以

  1. setNx key value

这里有个问题,加锁了之后如果机器宕机那么这个锁就不会得到释放所以会加入过期时间,加入过期时间需要和setNx同一个原子操作,在Redis2.8之前我们需要使用Lua脚本达到我们的目的,但是redis2.8之后redis支持nx和ex操作是同一原子操作。

  1. set key value ex 10 nx

常见的EX、PX、NX、XX的含义

  • EXseconds – 设置键key的过期时间,单位时秒
  • PXmilliseconds – 设置键key的过期时间,单位时毫秒
  • NX – 只有键key不存在的时候才会设置key的值
  • XX – 只有键key存在的时候才会设置key的值

场景示例

现在需要减库存服务,采用的是集群部署.这时候使用传统的synchronized已经不能解决(synchronized是jvm进程内的锁),此时就需要分布式锁来实现.保证同一时刻只有一个线程操作临界资源.

代码如下:

下面是使用redis自带的setnx命令来实现

  1. @GetMapping("/delete_stock")
  2. public String deleteProduct() {
  3. String lockKey = "product_001";
  4. String clientId = UUID.randomUUID().toString();
  5. try {
  6. Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);
  7. if (result != null && !result) {
  8. return "error001";
  9. }
  10. int stock = Integer.parseInt(Objects.requireNonNull(redisTemplate.opsForValue().get("stock")));
  11. if (stock > 0) {
  12. int realStock = stock - 1;
  13. redisTemplate.opsForValue().set("stock", realStock + "");
  14. System.out.println("扣减库存成功,剩余:" + realStock);
  15. } else {
  16. System.out.println("库存不足");
  17. }
  18. } finally {
  19. if (clientId.equals(redisTemplate.opsForValue().get(lockKey))) {
  20. redisTemplate.delete(lockKey);
  21. }
  22. }
  23. return "end";
  24. }

image.png
如上述所示一个简单的分布式锁就实现了.但是仔细看还是会存在一个问题,就是这个超时时间不好确定.
假如业务执行需要15s,这个超时时间设置为10s.这时候执行10s锁被释放,新的线程进来,还是会存在超卖问题.

Redisson实现分布式锁

Redisson框架举例说明,它已经封装了一套基于Redis的分布式框架,使用起来也很简单.

Redisson依赖

需要在maven中引入redisson的包

  1. <!-- Redisson 实现分布式锁 -->
  2. <dependency>
  3. <groupId>org.redisson</groupId>
  4. <artifactId>redisson</artifactId>
  5. <version>2.11.5</version>
  6. </dependency>

Redission配置

  • 针对redis 单实例
    需要配置redis的地址和密码

    1. @Bean
    2. public RedissonClient redissonClient(){
    3. Config config = new Config();
    4. config.useSingleServer()
    5. .setAddress(redissonAddress).setPassword(redissonPassword);
    6. RedissonClient redisson = Redisson.create(config);
    7. return redisson;
    8. }
  • 针对redis 哨兵
    需要配置redis哨兵的地址和密码

    1. @Bean
    2. public RedissonClient redissonClient(){
    3. Config config = new Config();
    4. config.useSentinelServers()
    5. .addSentinelAddress("redis://127.0.0.1:26379")
    6. .addSentinelAddress("redis://127.0.0.2:26379")
    7. .addSentinelAddress("redis://127.0.0.3:26379")
    8. .setPassword(redissonPassword);
    9. RedissonClient redisson = Redisson.create(config);
    10. return redisson;
    11. }
  • 针对redis 集群
    需要配置redis集群的地址和密码,并设置集群的扫描间隔时间

    1. @Bean
    2. public RedissonClient redissonClient()
    3. {
    4. Config config = new Config();
    5. config.useClusterServers()
    6. // 集群状态扫描间隔时间,单位是毫秒
    7. .setScanInterval(2000)
    8. //cluster方式至少6个节点(3主3从,3主做sharding,3从用来保证主宕机后可以高可用)
    9. .addNodeAddress("redis://127.0.0.1:6379" )
    10. .addNodeAddress("redis://127.0.0.1:6380")
    11. .addNodeAddress("redis://127.0.0.1:6381")
    12. .addNodeAddress("redis://127.0.0.1:6382")
    13. .addNodeAddress("redis://127.0.0.1:6383")
    14. .addNodeAddress("redis://127.0.0.1:6384")
    15. .setPassword(redissonPassword);
    16. RedissonClient redisson = Redisson.create(config);
    17. return redisson;
    18. }

场景示例

当前示例还是扣减库存.将上述示例进行修改

  1. @Autowired
  2. private RedissonClient redissonClient;
  3. @GetMapping("/delete_stock2")
  4. public String deleteProduct2() {
  5. String lockKey = "product_001";
  6. RLock lock = redissonClient.getLock(lockKey);//获取redis锁
  7. lock.lock();//加锁,实现锁续命.默认30s
  8. try {
  9. int stock = Integer.parseInt(Objects.requireNonNull(redisTemplate.opsForValue().get("stock")));
  10. if (stock > 0) {
  11. int realStock = stock - 1;
  12. redisTemplate.opsForValue().set("stock", realStock + "");
  13. System.out.println("扣减库存成功,剩余:" + realStock);
  14. } else {
  15. System.out.println("库存不足");
  16. }
  17. } finally {
  18. lock.unlock();//解锁
  19. }
  20. return "end";
  21. }

image.png

下面测试下

模拟并发100个线程同时抢购商品.这里使用了CyclicBarrier

  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
  3. @Slf4j
  4. public class RedisDisLockControllerTest {
  5. @Autowired
  6. private StringRedisTemplate redisTemplate;
  7. @Autowired
  8. private RedissonClient redissonClient;
  9. @Before
  10. public void init() {
  11. String productNum = "3";
  12. redisTemplate.opsForValue().set("stock", productNum);
  13. System.out.println("初始化商品:" + productNum + "个");
  14. }
  15. @Test
  16. public void deleteProduct() {
  17. int count = 10;//并发线程数
  18. ExecutorService executorService = Executors.newFixedThreadPool(count);
  19. /**
  20. * param1:是参与线程的个数
  21. * param2:第二个构造方法有一个 Runnable 参数,这个参数的意思是最后一个到达线程要做的任务
  22. *
  23. */
  24. CyclicBarrier cyclicBarrier = new CyclicBarrier(count, () -> System.out.println("==========所有线程准备完毕======="));
  25. for (int i = 0; i < count; i++) {
  26. executorService.execute(() -> {
  27. try {
  28. System.out.println(Thread.currentThread().getName() + "-->来到栅栏准备抢购商品");
  29. cyclicBarrier.await();//等待最后一个线程初始化完毕
  30. this.getProducts();//抢购商品
  31. } catch (Exception e) {
  32. e.printStackTrace();
  33. }
  34. });
  35. }
  36. try {
  37. //这里睡10000毫秒是为了主线程不关闭
  38. Thread.sleep(10000);
  39. } catch (Exception e) {
  40. e.printStackTrace();
  41. }
  42. executorService.shutdown(); // 关闭线程池
  43. }
  44. private void getProducts() {
  45. String lockKey = "product_001";
  46. RLock lock = redissonClient.getLock(lockKey);//获取redis锁
  47. lock.lock();//加锁,实现锁续命.默认30s
  48. try {
  49. int stock = Integer.parseInt(Objects.requireNonNull(redisTemplate.opsForValue().get("stock")));
  50. if (stock > 0) {
  51. int realStock = stock - 1;
  52. redisTemplate.opsForValue().set("stock", realStock + "");
  53. System.out.println("扣减库存成功,剩余:" + realStock);
  54. } else {
  55. System.out.println("库存不足");
  56. }
  57. } finally {
  58. lock.unlock();//解锁
  59. }
  60. }
  61. }

image.png

Redisson工具类封装

下面是基于Redisson实现的分布式锁帮助类,可以拿去直接使用,包含加锁、释放锁、带时间的加锁、尝试获取锁等。

  1. @Slf4j
  2. @Component
  3. public class RedisLockHelper {
  4. @Autowired
  5. private RedissonClient redissonClient;
  6. /**
  7. * 加锁
  8. * @param lockKey
  9. * @return
  10. */
  11. public RLock lock(String lockKey) {
  12. RLock lock = redissonClient.getLock(lockKey);
  13. lock.lock();
  14. return lock;
  15. }
  16. /**
  17. * 释放锁
  18. * @param lockKey
  19. */
  20. public void unlock(String lockKey) {
  21. RLock lock = redissonClient.getLock(lockKey);
  22. lock.unlock();
  23. }
  24. /**
  25. * 释放锁
  26. * @param lock
  27. */
  28. public void unlock(RLock lock) {
  29. lock.unlock();
  30. }
  31. /**
  32. * 带超时的锁
  33. * @param lockKey
  34. * @param timeout 超时时间 单位:秒
  35. */
  36. public RLock lock(String lockKey, int timeout) {
  37. RLock lock = redissonClient.getLock(lockKey);
  38. lock.lock(timeout, TimeUnit.SECONDS);
  39. return lock;
  40. }
  41. /**
  42. * 带超时的锁
  43. * @param lockKey
  44. * @param unit 时间单位
  45. * @param timeout 超时时间
  46. */
  47. public RLock lock(String lockKey, TimeUnit unit ,int timeout) {
  48. RLock lock = redissonClient.getLock(lockKey);
  49. lock.lock(timeout, unit);
  50. return lock;
  51. }
  52. /**
  53. * 尝试获取锁
  54. * @param lockKey
  55. * @param waitTime 最多等待时间
  56. * @param leaseTime 上锁后自动释放锁时间
  57. * @return
  58. */
  59. public boolean tryLock(String lockKey, int waitTime, int leaseTime) {
  60. RLock lock = redissonClient.getLock(lockKey);
  61. try {
  62. return lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
  63. } catch (InterruptedException e) {
  64. return false;
  65. }
  66. }
  67. /**
  68. * 尝试获取锁
  69. * @param lockKey
  70. * @param unit 时间单位
  71. * @param waitTime 最多等待时间
  72. * @param leaseTime 上锁后自动释放锁时间
  73. * @return
  74. */
  75. public boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime) {
  76. RLock lock = redissonClient.getLock(lockKey);
  77. try {
  78. return lock.tryLock(waitTime, leaseTime, unit);
  79. } catch (InterruptedException e) {
  80. return false;
  81. }
  82. }
  83. }