引言
多并发引起的超卖问题一直都是秒杀业务的核心,在极短的时间内没有中间件进行过渡,同时向底层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分布式锁的一些理解,欢迎大家指正!







1255





