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

Eureka集群:AP
Redis集群:AP
详见RedLock算法之Redisson的落地实现框架。
Redis实现分布式锁的原理
实现逻辑
# set sku:1:info “OK” NX PX 10000EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。XX :只在键已经存在时,才对键进行设置操作。

1. 多个客户端同时获取锁(setnx)
2. 获取成功,执行业务逻辑{从db获取数据,放入缓存},执行完成释放锁(del)
3. 其他客户端等待重试
XShell下测试
在xsehll下的撰写栏输入:setnx name fly,可以看到只有一个设置成功了。
2 多主机单Redis锁控制
使用Lua+Redis自己编码
copy机器模拟负载均衡:在Services->Copy Configuration,在program arguments: --server.port=10003


注意网关必须配置了才能达到负载均衡的效果//当然如果只是简易的测试,可以使用nginx做轮询
spring:cloud:gateway:routes:- id: product_routeuri: lb://gulimall-product-micserverepredicates:- Path=/api/product/**filters:- RewritePath=/api/(?<segment>.*),/$\{segment}其中:gulimall-product-micservere在项目间的配置spring:application:name: gulimall-product-micservere
代码注意点
| 问题 | 解决方案 |
|---|---|
| 核心原理是什么? | 使用Redis的setnx命令:由于setnx的命令都是原子性、单线程的,一次只能设置一次,设置失败了就相当于锁(坑)被占用了。注意:锁的key是一定的,value可以变。 |
| 两个线程同时去抢,线程2没有抢到锁,如何处理? | 使用自旋像Synchronize是自旋锁,其会自动等待并抢占锁,如果是自己设置的锁必须阻塞循环调用方法等待下次抢锁。 使用while循环 |
| Redis设置好了锁后,Java服务器宕机了,没有执行到删除锁这一逻辑,造成坑位一直被占据了后续逻辑方法一直死锁,如何解决? | - 抢占了锁之后必须要设置过期时间(兜底)。 - 不能把设置锁和设置过期时间的语句在Java里面分隔开,否则中途执行过程中死机了就会死锁。可使用 ops.setIfAbsent("lock", uuid, 500, TimeUnit.SECONDS);,这一条语句可保证既可以设置锁又可以同时设置过期时间。 |
|
| 业务还没执行完锁就过期了,别人拿到锁,自己执行完去删了别人的锁,如何处理? |
- 锁自动续期,自动续期自己不好实现。redisson有看门狗机制,推荐使用Redisson分布式锁框架。
- 同时删锁的时候要明确是自己的锁,如key加上uuid,再删除的时候不是直接del掉key,而是要验证value是否一致,这样即使别人上了锁,我去删的时候发现uuid不一样,也是不能删掉该锁。
- 删除锁必须保证原子性(保证判断和删除是原子的):使用redis+Lua脚本完成。脚本是原子的。
为什么判断和删除需要保证原子性?像如下没有保证原子性的代码:
|
使用总结
- 单主机版: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.0 入口代码@RequestMapping("/list")//@RequiresPermissions("product:brand:list")public R list(@RequestParam Map<String, Object> params) {PageUtils page = getPageUtilsValueByRedisDisLock(params);return R.ok().put("page", page);}//2.0 分布式锁代码--搭配了lua脚本public PageUtils getPageUtilsValueByRedisDisLock(Map<String, Object> params) {ValueOperations<String, String> ops = redisTemplate.opsForValue();//1.0 使用UUID:确保只能删除本人存入的数据String uuid = UUID.randomUUID().toString();//2.0 setIfAbsent:类似于Linux的setnx,如果值为true则代表抢到了锁。//ops.setIfAbsent语句是原子性的。Boolean lock = ops.setIfAbsent("lock", uuid, 500, TimeUnit.SECONDS);if (lock) {//System.out.println("获取分布式锁成功");//3.0 抢上了锁,去执行查询语句,该查询语句内部会加本地锁判断。PageUtils page = null;try {page = getDataFromRedisOrDB(params);} finally {//4.0 使用Lua脚本删除之前设置的Lock Key值。//放到finally里面是为了达到如果有异常也可以删除锁(当然Redis里面的锁到期后也会删除)String lockValue = ops.get("lock");String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), lockValue);}return page;} else {//System.out.println("获取分布式锁失败...等待重试");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}// 睡眠0.1s后,重新调用 //自旋return getPageUtilsValueByRedisDisLock(params);}}//3.0 查询数据库的语句public PageUtils getDataFromRedisOrDB(Map<String, Object> params) {//1.0 从Redis获取page数据(由于上面已经做了分布锁,所以此处不用再写锁了)ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();//不能直接使用(String)valueOperations.get("page");String pageJson = valueOperations.get("page");if (!StringUtils.isEmpty(pageJson)) {// 缓存不为null直接返回PageUtils page = JSON.parseObject(pageJson, new TypeReference<PageUtils>() {});return page;}//2.0 获取不到再读取数据库并设置RedisSystem.out.println("读取数据库");PageUtils pageDB = brandService.queryPageFromDB(params);valueOperations.set("page", JSON.toJSONString(pageDB), 1, TimeUnit.DAYS);return pageDB;}
使用RedLock-Redisson框架
多主机单Redis下可以直接使用Redisson分布式锁框架来实现。详细使用参考:使用RedLock-Redisson来实现多主机单Redis分布式锁
Redisson框架的实现原理也是通过Lau+Redis来实现,只不过比我们考虑的更全面,功能更强大(如多次可重入、集群等功能)
package com.atguigu.redis.controller;import com.atguigu.redis.util.RedisUtils;import org.redisson.Redisson;import org.redisson.api.RLock;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.core.io.ClassPathResource;import org.springframework.core.io.support.EncodedResource;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.data.redis.core.script.RedisScript;import org.springframework.util.FileCopyUtils;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import redis.clients.jedis.Jedis;import java.io.IOException;import java.util.Collections;import java.util.List;import java.util.UUID;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;import org.springframework.core.io.support.EncodedResource;@RestControllerpublic class GoodController {@Autowiredprivate StringRedisTemplate stringRedisTemplate;public static final String KEY = "goods20220101";@Value("${server.port}")private String serverPort;@Autowiredprivate Redisson redisson;@GetMapping("/buy_goods")public String buy_Goods() throws Exception {RLock redissonLock = redisson.getLock(KEY);redissonLock.lock();try {String result = stringRedisTemplate.opsForValue().get("goods:001");int goodsNumber = result == null ? 0 : Integer.parseInt(result);if (goodsNumber > 0) {int realNumber = goodsNumber - 1;stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort);return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort;} else {System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort);}return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort;} finally {if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {redissonLock.unlock();}}}}
3 多主机多Redis锁控制
为什么需要上Redis集群:为了预防某个Redis节点挂了,必须上Redis集群,这个时候就必须上框架了,可以使用RedLock算法之Redisson的落地实现框架。
03 Redis高阶:RedLock-Redisson的使用
