引言
多并发引起的超卖问题一直都是秒杀业务的核心,在极短的时间内没有中间件进行过渡,同时向底层DB发起请求,DB很容易崩溃,引入缓存中间件又引起了两者双写一致的问题,真令人头大,紧接着又引入一个异步处理的消息中间件。解决超卖的问题大致流程是,并发下单,先在缓存中对库存进行预减,然后向消息中间件推送成功的消息,然后更新DB数据的接口从消息队列中消费消息更新库存(这是性能较优的一种方案)。本文章是专注于探讨分布式锁的案例。
引入依赖以及yml配置文件
- pom文件
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
- yml文件(根据自身环境修改)
server: port: 9000 spring: application: name: redis redis: host: 192.168.136.130 port: 6379 password: 123456 lettuce: pool: max-active: 10 max-idle: 10 min-idle: 1 time-between-eviction-runs: 10s
配置Redis序列化
package redis.chaomai.test.config; 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.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.stereotype.Component; /* * redis序列化配置 * */ @Configuration public class RedisConfig { @Bean public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory) { //缓存序列化配置避免存储乱码 RedisTemplate<String,Object> redisTemplate=new RedisTemplate<>(); redisTemplate.setConnectionFactory(factory); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return redisTemplate; } }
数据准备
- 在Redis中添加一条Key为Apple,Value为30000的数据。
set Apple 30000
不加锁的接口
- 代码
@RequestMapping("test") void cherkAndReduceStock(){ String stock = redisTemplate.opsForValue().get("Apple").toString(); if (stock!=null) { Integer value = Integer.valueOf(stock); if (value>0){ redisTemplate.opsForValue().set("Apple",String.valueOf(--value)); } } }
- 使用Jmeter压测
- 压测结果 ,库存不为0
添加分布式锁
- redis命令setnx,同时为了避免死锁,同时设置锁的过期时间,redis命令set key value ex 2 nx
@RequestMapping("testAddsetxAddFinally") void cherkAndReduceStockAddSetnxAddFinally() { Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock-stock", "0000",2, TimeUnit.SECONDS); //获取锁失败,停止50ms,递归调用 if (!lock){ try { Thread.sleep(50); this.cherkAndReduceStockAddSetnxAddFinally(); } catch (InterruptedException e) { e.printStackTrace(); } }else { try { String stock = redisTemplate.opsForValue().get("Apple").toString(); if(stock!=null&&stock.length()!=0) { Integer valueOf = Integer.valueOf(stock); if (valueOf>0) { redisTemplate.opsForValue().set("Apple",String.valueOf(--valueOf)); }else { System.out.println("商品售罄!!!"); } } }finally { redisTemplate.delete("lock-stock"); } } }
- 使用Jmeter压测 ,查看结果
库存为0,问题解决
- 锁过期被删除,代码还没结束
等同于在半路锁过期被删除了,如下模拟场景
- 使用Jmeter压测后依旧存在超卖
- 解决方案:1.使用Redisson提供的自动延期机制(看门狗机制)。
2.配合使用Lua脚本原子性指令操作。
总结
本人对redis分布式锁的一些理解,欢迎大家指正!