上一篇 : 16.短信验证码
文章目录
- 1. 相关面试题
- 2. Redis 搭建
- 3. 编码实现分布式锁
- 3.1 建 Model
- 3.2 改 POM
- 3.3 写 YML
- 3.4 主启动
- 3.5 业务类
- 3.6 小测试
- 4. 上述案例存在问题及修改
- 4.1 没有加单机版的锁
- 4.2 分布式部署之后,单机版的锁失效
- 4.3 出现异常时,无法成功释放锁
- 4.4 服务宕机,无法成功释放锁
- 4.5 获取锁和设置过期时间分开,不具备原子性
- 4.6 误删他人的锁
- 4.7 判断是不是自己的锁 和 释放锁不是原子的
- 4.7.1 使用 Redis 事务
- 4.7.2 使用 LUA 脚本
- 4.8 缓存在锁释放前过期,释放锁时无限循环
- 4.8.1 Redisson的使用
- 4.8.2 释放锁的优化
- 5. 分布式锁总结
1. 相关面试题
-
Redis除了拿来做缓存,你还见过基于Redis的什么用法?
-
Redis做分布式锁的时候有需要注意的问题?
-
如果是Redis是单点部署的,会带来什么问题? 那你准备怎么解决单点问题呢?
-
集群模式下,比如主从模式,有没有什么问题呢?
-
那你简单的介绍一下Redlock吧? 你简历上写redisson,你谈谈
-
Redis分布式锁如何续期?看门狗知道吗?
2. Redis 搭建
略,看前文
此文侧重点是分布式锁,其他所需服务的搭建就不过多展开了
3. 编码实现分布式锁
- 使用场景:多个服务在同一时间内同一用户只能有一个请求
3.1 建 Model
搭建两个项目
- Redis-01
- Redis-02
02 直接复制 01 修改端口号
3.2 改 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.oneby</groupId>
<artifactId>boot_redis01</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.redisson/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>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.3 写 YML
server.port=1111
spring.redis.database=0
spring.redis.host=
spring.redis.port=6379
#连接池最大连接数(使用负值表示没有限制)默认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
3.4 主启动
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class BootRedis01Application {
public static void main(String[] args) {
SpringApplication.run(BootRedis01Application.class);
}
}
3.5 业务类
-
RedisConfig ,配置类
@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory){ // 新建 RedisTemplate 对象,key 为 String 对象,value 为 Serializable(可序列化的)对象 RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>(); // key 值使用字符串序列化器 redisTemplate.setKeySerializer(new StringRedisSerializer()); // value 值使用 json 序列化器 redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // 传入连接工厂 redisTemplate.setConnectionFactory(connectionFactory); // 返回 redisTemplate 对象 return redisTemplate; } }
-
GoodController ,业务类
@RestController public class GoodController { // 使用 RedisTemplate 的子类 @Autowired private StringRedisTemplate stringRedisTemplate; // 从配置文件注入端口号配置 @Value("${server.port}") private String serverPort; @GetMapping("/buy_goods") public String buy_Goods() { // 从 redis 中获取商品的剩余数量 String result = stringRedisTemplate.opsForValue().get("goods:001"); int goodsNumber = result == null ? 0 : Integer.parseInt(result); String retStr = null; // 商品数量大于零才能出售 if (goodsNumber > 0) { int realNumber = goodsNumber - 1; stringRedisTemplate.opsForValue().set("goods:001", realNumber + ""); retStr = "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort; } else { retStr = "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort; } System.out.println(retStr); return retStr; } }
3.6 小测试
- 单机情况下分别测试 Redis-01、02:
- http://192.168.1.6:1111/buy_goods 和 http://192.168.1.6:2222/buy_goods
4. 上述案例存在问题及修改
4.1 没有加单机版的锁
- 没有加锁,在高并发情况下会出现库存不准的问题
- 解决
可以通过加锁解决
- 思考:这里到底是加 Synchronized 还是 ReentrantLock?
- Synchronized 只有代码块执行完成或者异常才会释放锁,可能会有很多线程等候
- ReentrantLock 使用 lock.tryLock() 可以在等待一定时间后放弃等待
- 具体使用哪一个 分情况讨论
4.2 分布式部署之后,单机版的锁失效
-
分布式部署之后,单机版的锁失效,单机版的锁还是会导致超卖现象,这时就需要需要分布式锁
-
Nginx 的安装部署略
-
nginx.conf 配置文件
-
使用 Nginx 访问 Redis-01、02,手动点击貌似没有出错
-
使用 jmeter 进行压测
-
发现问题
-
解决
- 使用 Redis 的 SETNX 命令
Redis具有极高的性能,且其命令对分布式锁支持友好,借助 SET 命令即可实现加锁处理
The SET command supports a set of options that modify its behavior:- EX seconds – Set the specified expire time, in seconds.
- PX milliseconds – Set the specified expire time, in milliseconds.
- EXAT timestamp-seconds – Set the specified Unix time at which the key will expire, in seconds.
- PXAT timestamp-milliseconds – Set the specified Unix time at which the key will expire, in milliseconds.
- NX – Only set the key if it does not already exist.
- XX – Only set the key if it already exist.
- KEEPTTL – Retain the time to live associated with the key.
- GET – Return the old value stored at key, or nil when key did not exist.
- 使用 Redis 的 SETNX 命令
4.3 出现异常时,无法成功释放锁
- 如果代码在执行的过程中出现异常,那么就可能无法释放锁
- 解决:因此必须要在代码层面加上 finally 代码块,保证锁的释放
4.4 服务宕机,无法成功释放锁
- 假设部署了微服务 jar 包的服务器挂了,代码层面根本没有走到 finally 这块,也没办法保证解锁。这个 key 没有被删除,其他微服务就一直抢不到锁,因此我们需要加入一个过期时间限定的 key
- 解决:
4.5 获取锁和设置过期时间分开,不具备原子性
- 加锁与设置过期时间的操作分开了,假设服务器刚刚执行了加锁操作,然后宕机了,也没办法保证解锁。
- 解决:
4.6 误删他人的锁
- 线程A抢到了锁,但是中间业务代码执行的时间过长,超过了过期时间,那么 A 的锁将会自动释放掉了
- 线程B在A释放掉之后抢到锁,继续执行业务代码
- 此时 A 业务代码执行完成,执行 finally 中的释放锁,当前释放的锁就会是B的锁
- 解决:
释放锁时,判断当前是不是自己的锁
4.7 判断是不是自己的锁 和 释放锁不是原子的
- 在 finally 代码块中的判断与删除并不是原子操作,假设执行 if 判断的时候,这把锁还是属于当前业务,但是有可能刚执行完 if 判断,这把锁就被其他业务给释放了,还是会出现误删锁的情况
- 解决:(有两个解决思路)
-
官网推荐使用 LUA 脚本
-
使用 Redis 的事务
-
4.7.1 使用 Redis 事务
-
Redis 事务介绍
- Redis的事务是通过MULTl,EXEC,DISCARD和WATCH这四个命令来完成。
- Redis的单个命令都是原子性的,所以这里确保事务性的对象是命令集合。
- Redis将命令集合序列化并确保处于一事务的命令集合连续且不被打断的执行。
- Redis不支持回滚的操作。
-
相关命令
- MULTI
- 用于标记事务块的开始。
- Redis会将后续的命令逐个放入队列中,然后使用EXEC命令原子化地执行这个命令序列。
- 语法:MULTI
- EXEC
- 在一个事务中执行所有先前放入队列的命令,然后恢复正常的连接状态。
- 语法:EXEC
- DISCARD
- 清除所有先前在一个事务中放入队列的命令,然后恢复正常的连接状态。
- 语法:DISCARD
- WATCH
当某个事务需要按条件执行时,就要使用这个命令将给定的键设置为受监控的状态
。- 语法:WATCH key[key……]
注:该命令可以实现redis的乐观锁
- UNWATCH
- 清除所有先前为一个事务监控的键。
- 语法:UNWATCH
- MULTI
-
演示事务的使用
-
将 Redis 事务应用到上面代码中
4.7.2 使用 LUA 脚本
-
使用 lua 脚本可以防止别人动自己的锁
-
执行结果
4.8 缓存在锁释放前过期,释放锁时无限循环
-
因为执行业务的时候可能会调用其他服务,我们并不能保证其他服务的调用时间。如果设置的锁过期了,当前业务还正在执行,那么之前设置的锁就失效了,就有可能出现超卖问题。
-
因此我们需要确保 redisLock 过期时间大于业务执行时间的问题,即面临如何对 Redis 分布式锁进行续期的问题
-
redis 集群环境下,我们自己写的也不OK,直接上 RedLock 之
Redisson
落地实现
4.8.1 Redisson的使用
-
在 RedisConfig 配置类中注入 Redisson 对象
@Configuration public class RedisConfig { @Value("${spring.redis.host}") private String redisHost; @Bean public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory) { // 新建 RedisTemplate 对象,key 为 String 对象,value 为 Serializable(可序列化的)对象 RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>(); // key 值使用字符串序列化器 redisTemplate.setKeySerializer(new StringRedisSerializer()); // value 值使用 json 序列化器 redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // 传入连接工厂 redisTemplate.setConnectionFactory(connectionFactory); // 返回 redisTemplate 对象 return redisTemplate; } // 注入 Redisson @Bean public Redisson redisson() { Config config = new Config(); config.useSingleServer().setAddress("redis://" + redisHost + ":6379").setDatabase(0); return (Redisson) Redisson.create(config); } }
-
修改代码
-
压测
-
此时就基本解决了超卖的情况
4.8.2 释放锁的优化
- 上面释放锁时,是直接使用的
redissonLock.unlock()
- 在超高并发的情况下,有可能会出现当前线程和解锁的线程不是同一个,会报出如下异常
- 为了解决上述异常,释放锁时,最好这样来写
5. 分布式锁总结
- synchronized 锁:单机版 OK,上 nginx分布式微服务,单机锁就不 OK,
- 分布式锁:取消单机锁,上 redis 分布式锁 SETNX
- 如果出异常的话,可能无法释放锁, 必须要在 finally 代码块中释放锁
- 如果宕机了,部署了微服务代码层面根本没有走到 finally 这块,也没办法保证解锁,因此需要有设置锁的过期时间
- 除了增加过期时间之外,还必须要 SETNX 操作和设置过期时间的操作必须为原子性操作
- 规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴,可使用 lua 脚本或者事务
- 判断锁所属业务与删除锁的操作也需要是原子性操作
- redis 集群环境下,我们自己写的也不 OK,直接上 RedLock 之 Redisson 落地实现