REDIS09_分布式锁的概述加锁使用sexnu解锁使用lua脚本保证原子性引发的问题思考
Posted 所得皆惊喜
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了REDIS09_分布式锁的概述加锁使用sexnu解锁使用lua脚本保证原子性引发的问题思考相关的知识,希望对你有一定的参考价值。
文章目录
- ①. 分布式锁的概述
- ②. 分布式锁的案例搭建
- ③. 为何要使用sexnx+lua脚本解决
- ④. 问题总结
- ⑤. Redis单机CP、集群AP、EurekaAP、Zookeeper集群CP
- ⑥. 单机的Redis案例加锁、解锁
①. 分布式锁的概述
- ①. 锁的种类
- 单机版同一个JVM虚拟机内,synchronized或者Lock接口
- 分布式不同个JVM虚拟机内,单机的线程锁机制不再起作用,资源类在不同的服务器之间共享了
- ②. 一个靠谱分布式锁需要具备的条件和刚需 掌握
- 独占性:任何时刻只能有且仅有一个线程持有
- 高可用:若redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况
- 防死锁:杜绝死锁,必须有超时控制机制或者撤销操作,有个兜底终止跳出方案
- 不乱抢:防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放。
- 重入性:同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁。
- ③. setnx key value(差评,setnx+expire不安全,两条命令非原子性的)
set key value [EX seconds] [PX milliseconds] [NX|XX]
②. 分布式锁的案例搭建
-
①. 建Module boot_redis01、boot_redis02
-
②. 改POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.10.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.xiaozhi.redis</groupId>
<artifactId>boot_redis01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--guava-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
<!--web+actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringBoot与Redis整合依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
<!-- springboot-aop 技术-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
<!--一般通用基础配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- ③. 写yml
# 端口号
server.port=1111
# ========================redis相关配置=====================
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=192.168.56.10
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=-1
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=0
- ④. 配置类config、
@Configuration
public class RedisConfig
@Bean
public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory)
RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
- ⑤. 业务类
@RestController
public class GoodController
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("$server.port")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods()
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;
③. 为何要使用sexnx+lua脚本解决
-
①. 没有加锁,并发下数字不对,出现超卖现象,可以加上lock和synchronized来解决,不适合分布式的情况
-
②. 使用分布式锁setIfAbsent来解决
@GetMapping("/buy_goods")
public String buy_Goods()
String key = "RedisLock";
String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
if(!flagLock)
return "抢夺锁失败";
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 + "");
stringRedisTemplate.delete(key);
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\\t 服务器端口:"+serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\\t 服务器端口:"+serverPort;
else
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\\t 服务器端口:"+serverPort);
return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\\t 服务器端口:"+serverPort;
- ③. 在②的情况下,如果出异常的话,可能无法释放锁,必须要在代码层面finally释放锁
- ④. 部署了微服务jar包的机器挂了,代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要加入一个过期时间限定key
Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value);//1
stringRedisTemplate.expire(key,10L,TimeUnit.SECONDS);//2
- ⑤. 设置key+过期时间分开了,必须要合并成一行具备原子性(加锁必须确保原子性)
在④中,如果某一个时刻,刚刚执行完//1,这个时候发生宕机,那么就造成了死锁
stringRedisTemplate.opsForValue().setIfAbsent(key,value,10L,TimeUnit.SECONDS);
- ⑥. 张冠李戴,删除了别人的锁
如果线程A拿到分布式锁,设置的过期时间小于业务代码执行的时间,当A线程分布式锁刚刚过期,这个时候B线程获取到了分布式锁,A线程执行完业务逻辑,进行删除,就可能删除的是B的分布式锁
finally
if (stringRedisTemplate.opsForValue().get(key).equals(value))
stringRedisTemplate.delete(key);
- ⑦. 基于⑥的操作也有问题,finally块的判断+del删除操作不是原子性的
- ⑧. 用Lua脚本,删除保证原子性
//1.占分布式锁,去redis占坑 setIfAbsent==sexnx
//Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
//redisTemplate.expire("lock",30,TimeUnit.SECONDS);
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if(lock)
Map<String, List<Catelog2Vo>> dataFromDb=null;
try
//加锁成功,执行业务
dataFromDb = getDataFromDb();
finally
//原子删锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
"then " +
"return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
//删除成功返回1,删除不成功返回0
redisTemplate.execute(
new DefaultRedisScript<Long>(script,Long.class),
Arrays.asList("lock"),uuid);
return dataFromDb;
else
//加锁失败....重试
try TimeUnit.SECONDS.sleep(1); catch (InterruptedException e) e.printStackTrace();
return getCatalogJsonFromDbWithRedisLock();
④. 问题总结
问题总结,推出使用分布式锁
(1). synchronized单机版OK,上分布式
(2). nginx分布式微服务,单机锁不行
(3). 取消单机锁,上redis分布式锁setnx
(4). 只加了锁,没有释放锁,出异常的话,可能无法释放锁,必须要在代码层面finally释放锁
(5). 宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定
(6). 为redis的分布式锁key,增加过期时间此外,还必须要setnx+过期时间必须同一行
(7). 必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴,1删2,2删3
(8). redis集群环境下,我们自己写的也不OK,直接上RedLock之Redisson落地实现
⑤. Redis单机CP、集群AP、EurekaAP、Zookeeper集群CP
-
①. Redis单机是CP集群是AP
-
②. Redis集群:AP
(redis异步复制造成的锁丢失,比如:主节点没来的及把刚刚set进来这条数据给从节点,master就挂了,从机上位但从机上无该数据) -
③. Zookeeper集群的CP
-
④. Eureka是AP
⑥. 单机的Redis案例加锁、解锁
-
①. 加锁:加锁实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间
-
②. 解锁:将Key键删除。但也不能乱删,不能说客户端1的请求将客户端2的锁给删除掉,只能自己删除自己的锁
(为了保证解锁操作的原子性,我们用LUA脚本完成这一操作。先判断当前锁的字符串是否与传入的值相等,是的话就删除Key,解锁成功)
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end
-
③. 超时:锁key要注意过期时间,不能长期占用
-
④. 单机模式中,一般都是用set/setnx+lua脚本搞定,想想它的缺点是什么?
如果redis发生了宕机,所有的请求压力都指向了数据库
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock()
String uuid = UUID.randomUUID().toString();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
Boolean lock = ops.setIfAbsent("lock", uuid,500, TimeUnit.SECONDS);
if (lock)
Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();
String lockValue = ops.get("lock");
// get和delete原子操作
String script = "if redis.call(\\"get\\",KEYS[1]) == ARGV[1] then\\n" +
以上是关于REDIS09_分布式锁的概述加锁使用sexnu解锁使用lua脚本保证原子性引发的问题思考的主要内容,如果未能解决你的问题,请参考以下文章