1 分布式锁的概述

概述

随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题,常用的分布式锁的解决方案。
CAP理论:一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)

方案 细分方案 说明
基于数据库实现分布式锁 可使用乐观锁、悲观锁来来控制 性能最差,高并发不合适
基于Redis 多主机单Redis:CP 综合来看,Redis性能最高。多台主机集群,Redis异步复制可能会造成锁丢失:主机刚把key set了就挂了,从机还没复制到该条数据。解决:不适用master-slaver,而使用多台master主机。
多主机多Redis集群:AP
基于Zookeeper集群 集群CP 可靠性最高。多台主机集群,为了追求可靠性,牺牲了高可用性。
多用于银行等对于数据一致性要求很高的业务场景。

ZooKeeper集群:CP
image.png
image.png
Eureka集群:AP
image.png
Redis集群:AP
详见RedLock算法之Redisson的落地实现框架。

Redis实现分布式锁的原理

实现逻辑

  1. # set sku:1:info “OK” NX PX 10000
  2. EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value
  3. PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value
  4. NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value
  5. XX :只在键已经存在时,才对键进行设置操作。

image.png
1. 多个客户端同时获取锁(setnx)
2. 获取成功,执行业务逻辑{从db获取数据,放入缓存},执行完成释放锁(del)
3. 其他客户端等待重试

XShell下测试

在xsehll下的撰写栏输入:setnx name fly,可以看到只有一个设置成功了。
image.pngimage.png

2 多主机单Redis锁控制

使用Lua+Redis自己编码

copy机器模拟负载均衡:Services->Copy Configuration,在program arguments: --server.port=10003
image.pngimage.pngimage.png
注意网关必须配置了才能达到负载均衡的效果//当然如果只是简易的测试,可以使用nginx做轮询

  1. spring:
  2. cloud:
  3. gateway:
  4. routes:
  5. - id: product_route
  6. uri: lb://gulimall-product-micservere
  7. predicates:
  8. - Path=/api/product/**
  9. filters:
  10. - RewritePath=/api/(?<segment>.*),/$\{segment}
  11. 其中:gulimall-product-micservere在项目间的配置
  12. spring:
  13. application:
  14. name: gulimall-product-micservere

代码注意点

问题 解决方案
核心原理是什么? 使用Redis的setnx命令:由于setnx的命令都是原子性、单线程的,一次只能设置一次,设置失败了就相当于锁(坑)被占用了。注意:锁的key是一定的,value可以变。
两个线程同时去抢,线程2没有抢到锁,如何处理? 使用自旋像Synchronize是自旋锁,其会自动等待并抢占锁,如果是自己设置的锁必须阻塞循环调用方法等待下次抢锁。
使用while循环
Redis设置好了锁后,Java服务器宕机了,没有执行到删除锁这一逻辑,造成坑位一直被占据了后续逻辑方法一直死锁,如何解决?
- 抢占了锁之后必须要设置过期时间(兜底)。
- 不能把设置锁和设置过期时间的语句在Java里面分隔开,否则中途执行过程中死机了就会死锁。可使用ops.setIfAbsent("lock", uuid, 500, TimeUnit.SECONDS);,这一条语句可保证既可以设置锁又可以同时设置过期时间。

image.png | | 业务还没执行完锁就过期了,别人拿到锁,自己执行完去删了别人的锁,如何处理? |
- 锁自动续期,自动续期自己不好实现。redisson有看门狗机制,推荐使用Redisson分布式锁框架。
- 同时删锁的时候要明确是自己的锁,如key加上uuid,再删除的时候不是直接del掉key,而是要验证value是否一致,这样即使别人上了锁,我去删的时候发现uuid不一样,也是不能删掉该锁。
- 删除锁必须保证原子性(保证判断和删除是原子的):使用redis+Lua脚本完成。脚本是原子的。
为什么判断和删除需要保证原子性?像如下没有保证原子性的代码:
image.png |

使用总结

  • 单主机版:synchronized和Lock在单机版可行,但是一旦多台主机涉及到分布式就不可以。
  • 多主机版&单Redis锁
    • 只加了锁,没有释放锁,出异常的话,可能无法释放锁,必须要在代码层面finally释放锁
    • 宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定
    • 为redis的分布式锁key增加过期时间,此外还必须确保setnx+过期时间必须在同一行
    • 必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴。1删2、2删3
    • note1:要想简单,可以不用自己编码,直接使用Redisson框架
    • note2:如果单个Redis挂了,那分布式锁就失效了,此时必须要上Redis集群使用多分布式锁来控制。
  • 多主机&多Redis锁:redis集群环境下,我们自己写的也不OK,直接上RedLock之Redisson落地实现。

代码演示

  1. //1.0 入口代码
  2. @RequestMapping("/list")
  3. //@RequiresPermissions("product:brand:list")
  4. public R list(@RequestParam Map<String, Object> params) {
  5. PageUtils page = getPageUtilsValueByRedisDisLock(params);
  6. return R.ok().put("page", page);
  7. }
  8. //2.0 分布式锁代码--搭配了lua脚本
  9. public PageUtils getPageUtilsValueByRedisDisLock(Map<String, Object> params) {
  10. ValueOperations<String, String> ops = redisTemplate.opsForValue();
  11. //1.0 使用UUID:确保只能删除本人存入的数据
  12. String uuid = UUID.randomUUID().toString();
  13. //2.0 setIfAbsent:类似于Linux的setnx,如果值为true则代表抢到了锁。
  14. //ops.setIfAbsent语句是原子性的。
  15. Boolean lock = ops.setIfAbsent("lock", uuid, 500, TimeUnit.SECONDS);
  16. if (lock) {
  17. //System.out.println("获取分布式锁成功");
  18. //3.0 抢上了锁,去执行查询语句,该查询语句内部会加本地锁判断。
  19. PageUtils page = null;
  20. try {
  21. page = getDataFromRedisOrDB(params);
  22. } finally {
  23. //4.0 使用Lua脚本删除之前设置的Lock Key值。
  24. //放到finally里面是为了达到如果有异常也可以删除锁(当然Redis里面的锁到期后也会删除)
  25. String lockValue = ops.get("lock");
  26. String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  27. redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), lockValue);
  28. }
  29. return page;
  30. } else {
  31. //System.out.println("获取分布式锁失败...等待重试");
  32. try {
  33. Thread.sleep(100);
  34. } catch (InterruptedException e) {
  35. e.printStackTrace();
  36. }
  37. // 睡眠0.1s后,重新调用 //自旋
  38. return getPageUtilsValueByRedisDisLock(params);
  39. }
  40. }
  41. //3.0 查询数据库的语句
  42. public PageUtils getDataFromRedisOrDB(Map<String, Object> params) {
  43. //1.0 从Redis获取page数据(由于上面已经做了分布锁,所以此处不用再写锁了)
  44. ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
  45. //不能直接使用(String)valueOperations.get("page");
  46. String pageJson = valueOperations.get("page");
  47. if (!StringUtils.isEmpty(pageJson)) {
  48. // 缓存不为null直接返回
  49. PageUtils page = JSON.parseObject(pageJson, new TypeReference<PageUtils>() {
  50. });
  51. return page;
  52. }
  53. //2.0 获取不到再读取数据库并设置Redis
  54. System.out.println("读取数据库");
  55. PageUtils pageDB = brandService.queryPageFromDB(params);
  56. valueOperations.set("page", JSON.toJSONString(pageDB), 1, TimeUnit.DAYS);
  57. return pageDB;
  58. }

使用RedLock-Redisson框架

多主机单Redis下可以直接使用Redisson分布式锁框架来实现。详细使用参考:使用RedLock-Redisson来实现多主机单Redis分布式锁
Redisson框架的实现原理也是通过Lau+Redis来实现,只不过比我们考虑的更全面,功能更强大(如多次可重入、集群等功能)

  1. package com.atguigu.redis.controller;
  2. import com.atguigu.redis.util.RedisUtils;
  3. import org.redisson.Redisson;
  4. import org.redisson.api.RLock;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.beans.factory.annotation.Value;
  7. import org.springframework.core.io.ClassPathResource;
  8. import org.springframework.core.io.support.EncodedResource;
  9. import org.springframework.data.redis.core.RedisTemplate;
  10. import org.springframework.data.redis.core.StringRedisTemplate;
  11. import org.springframework.data.redis.core.script.RedisScript;
  12. import org.springframework.util.FileCopyUtils;
  13. import org.springframework.web.bind.annotation.GetMapping;
  14. import org.springframework.web.bind.annotation.RestController;
  15. import redis.clients.jedis.Jedis;
  16. import java.io.IOException;
  17. import java.util.Collections;
  18. import java.util.List;
  19. import java.util.UUID;
  20. import java.util.concurrent.TimeUnit;
  21. import java.util.concurrent.locks.Lock;
  22. import java.util.concurrent.locks.ReentrantLock;
  23. import org.springframework.core.io.support.EncodedResource;
  24. @RestController
  25. public class GoodController {
  26. @Autowired
  27. private StringRedisTemplate stringRedisTemplate;
  28. public static final String KEY = "goods20220101";
  29. @Value("${server.port}")
  30. private String serverPort;
  31. @Autowired
  32. private Redisson redisson;
  33. @GetMapping("/buy_goods")
  34. public String buy_Goods() throws Exception {
  35. RLock redissonLock = redisson.getLock(KEY);
  36. redissonLock.lock();
  37. try {
  38. String result = stringRedisTemplate.opsForValue().get("goods:001");
  39. int goodsNumber = result == null ? 0 : Integer.parseInt(result);
  40. if (goodsNumber > 0) {
  41. int realNumber = goodsNumber - 1;
  42. stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
  43. System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort);
  44. return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort;
  45. } else {
  46. System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort);
  47. }
  48. return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort;
  49. } finally {
  50. if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {
  51. redissonLock.unlock();
  52. }
  53. }
  54. }
  55. }

3 多主机多Redis锁控制

为什么需要上Redis集群:为了预防某个Redis节点挂了,必须上Redis集群,这个时候就必须上框架了,可以使用RedLock算法之Redisson的落地实现框架。
03 Redis高阶:RedLock-Redisson的使用