Redisson实现分布式锁剖析
Redission实现分布式锁:
【1小时掌握Redisson实现Redis分布式锁以及Redlock分布式锁】 https://www.bilibili.com/video/BV1Yy4y1k79y/?share_source=copy_web&vd_source=045f1e1c5f0609a50870e499d44940af
Redission是什么?
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid),它充分利用了Redis键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类,让使用Redis更加简单、便捷,从而让使用者能够将更多精力集中到业务逻辑处理上。
Redission能解决什么问题?
解决分布式环境下锁的竞争问题,导致的比如说超卖等
基本环境搭建:
实现的需求就是,每访问一次controller:localhost:8080/deductStock,Redis存的数据就会-1
1、创建一个SpringBoot项目,添加pom依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | <dependencies> <!--Redis依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--springboot2.x以后用得是lettuce:lettuce默认连接池使用 common-pool2 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <!--Redission实现分布式锁--> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version> 3.13 . 6 </version> </dependency> <!--SpringBoot依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--Lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> |
2、application.properties
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | # 应用名称 spring.application.name=redission-demo # 应用服务 WEB 访问端口 server.port= 8080 ##########################Redis配置################################### # 连接的那个数据库(默认为 0 ) spring.redis.database= 1 # redis服务的ip地址(默认是本机- 127.0 . 0.1 ) spring.redis.host= 127.0 . 0.1 # redis端口号(默认) spring.redis.port= 6379 # redis的密码,没设置过密码,可为空 spring.redis.password= # 连接超时时间 spring.redis.timeout=10s # 连接池中的最小空闲连接 spring.redis.lettuce.pool.min-idle= 0 # 连接池中的最大空闲连接 spring.redis.lettuce.pool.max-idle= 8 # 连接池中的最大连接数 spring.redis.lettuce.pool.max-active= 8 # 连接池中的最大等待时间(- 1 表示没有限制) spring.redis.lettuce.pool.max-wait=-1ms |
3、Redis配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | package com.zhixi.config.redis; import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.io.File; /** * @ClassName RedisConfig * @Author zhangzhixi * @Description Redis配置类 * @Date 2022-4-29 10:23 * @Version 1.0 */ @Configuration public class RedisConfig { /** * @param redisConnectionFactory:配置不同的客户端,这里注入的redis连接工厂不同: JedisConnectionFactory、LettuceConnectionFactory * @功能描述 :配置Redis序列化,原因如下: * (1) StringRedisTemplate的序列化方式为字符串序列化, * RedisTemplate的序列化方式默为jdk序列化(实现Serializable接口) * (2) RedisTemplate的jdk序列化方式在Redis的客户端中为乱码,不方便查看, * 因此一般修改RedisTemplate的序列化为方式为JSON方式【建议使用GenericJackson2JsonRedisSerializer】 */ @Bean (name = "redisTemplate" ) public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = serializer(); RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); // key采用String的序列化方式 redisTemplate.setKeySerializer(StringRedisSerializer.UTF_8); // value序列化方式采用jackson redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer); // hash的key也采用String的序列化方式 redisTemplate.setHashKeySerializer(StringRedisSerializer.UTF_8); //hash的value序列化方式采用jackson redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer); redisTemplate.setConnectionFactory(redisConnectionFactory); return redisTemplate; } /** * 此方法不能用@Ben注解,避免替换Spring容器中的同类型对象 */ public GenericJackson2JsonRedisSerializer serializer() { return new GenericJackson2JsonRedisSerializer(); } @Value ( "${spring.redis.host}" ) private String redisHost; @Value ( "${spring.redis.port}" ) private String redisPort; @Value ( "${spring.redis.database}" ) private Integer redisDatabase; /** * 注册Redission对象 * * @return Redission客户端对象 */ @Bean public RedissonClient redissonClient() { Config config = new Config(); config.useSingleServer().setAddress( "redis://" + redisHost + ":" + redisPort).setDatabase(redisDatabase); return Redisson.create(config); } } |
4、Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | package com.zhixi.controller; import lombok.extern.slf4j.Slf4j; import org.redisson.api.RedissonClient; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; /** * @ClassName RedissionController * @Author zhangzhixi * @Description * @Date 2022-12-12 23:09 * @Version 1.0 */ @Slf4j @RestController public class RedissionController { /** * 商品在Redis中的key名称 */ private static final String KEY_STOCK = "stock" ; @Resource RedisTemplate<String, String> redisTemplate; @Resource RedissonClient redissonClient; @RequestMapping ( "/deductStock" ) public String deductStock() { // 从redis中获取商品库存 int stock = Integer.parseInt(Objects.requireNonNull(redisTemplate.opsForValue().get(KEY_STOCK))); AtomicInteger atomicInteger = new AtomicInteger(stock); if (atomicInteger.get() > 0 ) { // 库存-1 int remainingStock = atomicInteger.decrementAndGet(); // 更新库存 redisTemplate.opsForValue().set(KEY_STOCK, String.valueOf(remainingStock)); log.info( "扣减成功,剩余库存:" + remainingStock); } else { log.info( "扣减失败,库存不足" ); } return "end" ; } } |
5、向Redis中设置值
1 2 3 4 5 6 7 8 9 10 11 | @SpringBootTest class RedissionDemoApplicationTests { @Resource RedisTemplate<String, String > redisTemplate; @Test void contextLoads() { redisTemplate.opsForValue().set( "stock" , "100" ); } } |
出现的问题以及解决方案
商品超卖(并发问题)
问题出现:
比如有三个线程同时来访问这个请求,比如三个线程同时获得商品库存(100),然后-1,正常来说剩余库存是97,但是在并发环境下,可能会出现超卖问题。
下面准备三个线程,同时访问deductStock这个方法,看下是否会出现超卖问题?
1 2 3 4 5 6 7 8 | @GetMapping ( "/test" ) public void testThread() { for ( int i = 0 ; i < 3 ; i++) { new Thread(()->{ deductStock(); }).start(); } } |
解决办法:
对代码进行加锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @RequestMapping ( "/deductStock" ) public String deductStock() { // 在单机情况下,有且仅有一个线程能够访问 synchronized ( this ) { // 从redis中获取商品库存 int stock = Integer.parseInt(Objects.requireNonNull(redisTemplate.opsForValue().get(KEY_STOCK))); AtomicInteger atomicInteger = new AtomicInteger(stock); if (atomicInteger.get() > 0 ) { // 库存-1 int remainingStock = atomicInteger.decrementAndGet(); // 更新库存 redisTemplate.opsForValue().set(KEY_STOCK, String.valueOf(remainingStock)); log.info( "扣减成功,剩余库存:" + remainingStock); } else { log.info( "扣减失败,库存不足" ); } return "end" ; } } |
通过下图可以看到,解决了问题
模拟高并发下商品超卖问题
在上个例子中,我们通过了Synchronized锁,成功解决了多个线程争抢导致的超卖问题,但是有个问题,假设后期公司为了保证服务可用性。
将单击的应用,升级称为了集群的模式,那么是否会有超卖问题呢?
模拟两台应用服务器:使用Nginx做负载均衡
1、修改application.properties配置文件,将端口分别修改为8080、8090,并启动应用。
2、下载Nginx:http://nginx.org/download/nginx-1.18.0.zip
3、编辑Nginx配置文件:config/nginx.config
1 2 3 4 5 6 7 8 9 10 11 12 13 | upstream redislock{ server localhost: 8080 weight= 1 ; server localhost: 8090 weight= 1 ; } server { listen 80 ; server_name localhost; location / { root html; index index.html index.htm; proxy_pass http: //redislock; } |
4、启动nginx
双击nginx.exe文件即可
5、访问应用:http://localhost/deductStock
就可以看到了IDEA控制台打印了日志。
模拟高并发:使用Jmeter
1、添加线程组
2、添加HTTP请求
3、添加聚合报告
4、执行压测
IDEA控制台输出内容:
可以发现,通过Synchronized没有锁住,还是出现了超卖的问题,Synchronized只在JVM进程内部有效,也就是一个Tomcat有效。
如果在一个分布式环境下,我们要控制一段资源的争抢问题,应该怎么做呢?
使用Redis命令解决并发环境下超卖问题
在上个例子中,模拟了在高并发环境下的商品超卖问题,那么应该如何通过Redis来解决这个问题呢?
setnx命令:
将key的值设置为value,当且仅当key不存在的情况下。
若给定的key已经存在,则setnx不做任何动作。
Redis简单命令式分布式锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | @RequestMapping ( "/deductStock" ) public String deductStock() { // 分布式锁名称,随便什么都可以,重要的是两个应用要共享这一个Redis的key String lockKey = "lockDeductStock" ; // redis实现基础版的分布式锁 Boolean lockResult = redisTemplate.opsForValue().setIfAbsent(lockKey, lockKey); // 加锁不成功,返回给前端错误码,前端给用户友好提示 if (Boolean.FALSE.equals(lockResult)) { log.info( "系统繁忙,请稍后再试!" ); return "系统繁忙,请稍后再试!" ; } // 从redis中获取商品库存 int stock = Integer.parseInt(Objects.requireNonNull(redisTemplate.opsForValue().get(KEY_STOCK))); AtomicInteger atomicInteger = new AtomicInteger(stock); if (atomicInteger.get() > 0 ) { // 库存-1 int remainingStock = atomicInteger.decrementAndGet(); // 更新库存 redisTemplate.opsForValue().set(KEY_STOCK, String.valueOf(remainingStock)); log.info( "扣减成功,剩余库存:" + remainingStock); } else { log.info( "扣减失败,库存不足" ); } // 业务执行完成,删除这个锁 redisTemplate.delete(lockKey); return "end" ; } |
修改Jmeter参数
最开始设置的Jmeter参数Ramp-up时间是0,为了明显看到效果,这里修改成2,表示2s发送完毕这些800个请求
测试并发访问
使用Redis做分布式锁时候出现的问题?
锁永久失效问题
假如我们在setnx分布式锁的时候,正常执行,但是如果代码执行到业务逻辑地方的时候。
这时候8080程序挂掉了,那么这时候这个key就无法删除,另外一个8090服务器发现这个分布式锁的key还在,就在那边排队等着
给用户提示“系统繁忙,请稍后再试!”。显然这是不合理的。
如果我们在程序挂了,但是这个key是一直存在Redis中的,其他的用户访问这个请求也进不到后面的业务处理逻辑代码中去,应该怎么办呢?那么这时候就需要使用到了redis原生API:
即使程序挂掉了,没有删除key,10S钟后,这个key也会自动被删除。
1 2 | // redis实现基础版的分布式锁 Boolean lockResult = redisTemplate.opsForValue().setIfAbsent(lockKey, lockKey, 30 , TimeUnit.SECONDS); |
但是这一个代码,虽然比上个分布式超卖的问题优化了一点点,但是还是有问题的,比如说,
- 问题1:如果程序的执行时间,大于了锁的释放时间。这就会导致程序在最后删除的key为空,显然这个失效时间确实不容易写的比较“合理”。
- 问题2:8090给8080程序的锁释放了,如果9090这个程序进来了,拿到了锁,但是8090的锁无法释放,导致了锁一直失效,这也是导致了超卖问题。
如何解决线程之间锁释放错了?
上个问题,问题2锁释放错了应该如何解决呢?通过UUID解决,每次释放锁的时候,判断是不是当前请求的UUID,如果是则可以正常释放锁。如果不是,则释放锁失败!
@RequestMapping("/deductStock") public String deductStock() { // 分布式锁名称,随便什么都可以,重要的是两个应用要共享这一个Redis的key String lockKey = "lockDeductStock"; // 分布式锁的值 String lockValue = UUID.randomUUID().toString().replaceAll("-", ""); try { // redis实现基础版的分布式锁,设置了key的过期时间 Boolean lockResult = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS); // 加锁不成功,返回给前端错误码,前端给用户友好提示 if (Boolean.FALSE.equals(lockResult)) { log.info("系统繁忙,请稍后再试!"); return "系统繁忙,请稍后再试!"; } // 从redis中获取商品库存 int stock = Integer.parseInt(Objects.requireNonNull(redisTemplate.opsForValue().get(KEY_STOCK))); AtomicInteger atomicInteger = new AtomicInteger(stock); if (atomicInteger.get() > 0) { // 库存-1 int remainingStock = atomicInteger.decrementAndGet(); // 更新库存 redisTemplate.opsForValue().set(KEY_STOCK, String.valueOf(remainingStock)); log.info("扣减成功,剩余库存:" + remainingStock); } else { log.info("扣减失败,库存不足"); } } finally { // 判断是不是当前请求的UUID,如果是则可以正常释放锁。如果不是,则释放锁失败! if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) { redisTemplate.delete(lockKey); } } return "end"; }
-------------------------------------------------使用Redis实现分布式锁有两个问题:----------------------------------------------------------------------------------------------
分别是:
- 1、锁超时问题
- 2、锁被误释放问题
在上个代码中我们通过UUID解决了锁被误释放的问题,还有一个锁超时的问题应该如何解决呢?
写一个定时任务,分线程每隔十秒去查看一次主线程是否持有这把锁,如果这个锁存在,重新将这个锁的超时时间设置为30S,对锁续命~
当然自己写上面的代码肯定无比麻烦的,当前市面上有很多开源框架已经帮助我们封装好了这些逻辑!
使用redisson解决超卖问题
Redission来帮我们实现锁续命的分布式锁。
@RequestMapping("/deductStock") public String deductStock() { // 分布式锁名称,随便什么都可以,重要的是两个应用要共享这一个Redis的key String lockKey = "lockDeductStock"; // 获取锁对象 RLock redissonLock = redissonClient.getLock(lockKey); try { // 加锁 // 类似于Redis的一条setnx命令:Boolean lockResult = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS); redissonLock.lock(); // 从redis中获取商品库存 int stock = Integer.parseInt(Objects.requireNonNull(redisTemplate.opsForValue().get(KEY_STOCK))); AtomicInteger atomicInteger = new AtomicInteger(stock); if (atomicInteger.get() > 0) { // 库存-1 int remainingStock = atomicInteger.decrementAndGet(); // 更新库存 redisTemplate.opsForValue().set(KEY_STOCK, String.valueOf(remainingStock)); log.info("扣减成功,剩余库存:" + remainingStock); } else { log.info("扣减失败,库存不足"); } } finally { // 释放锁 redissonLock.unlock(); } return "end"; }
Redisson分布式锁实现原理:
Redisson分布式锁底层
Lua脚本
if (redis.call('exists', KEYS[1]) == 0) then " + "redis.call('hincrby', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " + "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + "redis.call('hincrby', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " + "return redis.call('pttl', KEYS[1]);
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
2020-12-12 使用response下载文件