SpringBoot+redis+lua 防止超卖
一、背景
工作中遇到了有人用 RedisTemplate
的 increment
去做总库存的加减,但是这种方式是保证不了原子性的还是会超卖。
- redis 是可以保证原子性,但是 RedisTemplate 里面的方法去调用redis是不能保证原子性
二、优化方案
使用 lua 脚本,去执行 加减操作,执行 redis 的命令,来保证原子性
三、重点代码
RedisTemplate 注入
@Configuration
public class RedisConfig {
@SuppressWarnings({"rawtypes", "unchecked"})
@Bean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
抢购帮助类并支持集群模式
/**
* @Author liyue
* @date 2024/3/22 14:31
**/
@Component
public class SecKillProvider {
private static final String PRODUCT_KEY = "{cluster:}productstock";
private static final String SECKILL_SCRIPT = "lua/seckill.lua";
@Resource
private RedisTemplate<String, Object> redisTemplate;
public void initStock(int stock) {
//24小时过期
RedisUtils.setIfAbsentTimeout(PRODUCT_KEY, stock, 86400);
}
//+ DateUtil.format(new Date(), DatePattern.NORM_DATE_PATTERN))
public boolean seckill(String userId) {
//调用lua脚本并执行
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setResultType(Long.class);//返回类型是Long
//lua文件存放在resources目录下的redis文件夹内
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(SECKILL_SCRIPT)));
Integer result = (Integer) redisTemplate.opsForValue().get(PRODUCT_KEY);
System.out.println(result);
System.out.println(PRODUCT_KEY);
Long stock = redisTemplate.execute(redisScript, Arrays.asList(PRODUCT_KEY));
System.out.println("执行完成--=--stock=" + stock);
if (stock >= 0) {
// 抢购成功,可以继续处理订单等逻辑
System.out.println("User " + userId + " seckill success!");
return true;
} else {
// 抢购失败,库存不足
System.out.println("User " + userId + " seckill failed!");
return false;
}
}
}
lua 脚本
local stock = tonumber(redis.call('get', KEYS[1]))
if stock and stock > 0 then
redis.call('decr', KEYS[1])
return stock - 1
else
return -1
end
并发测试类
/**
* @Author liyue
* @date 2024/3/22 15:14
**/
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class SecKillProviderTest {
@Resource
private SecKillProvider secKillProvider;
@Test
public void t() throws InterruptedException {
secKillProvider.initStock(10);
for (int i = 0; i < 2000; i++) {
new Thread(new Mythread(i)).start();
}
Thread.sleep(10000);
}
class Mythread implements Runnable {
private int num;
Mythread(int num) {
this.num = num;
}
@SuppressWarnings("unchecked")
SecKillProvider secKillProvider = SpringUtil.getBean("secKillProvider");
@Override
public void run() {
secKillProvider.seckill(String.valueOf(num));
}
}
}
总结
使用这种方式,读取文件可以优化成sha的方式去去读取。后面再进行调整,测试类可以模拟并发情况。测试没有什么问题。
本文由mdnice多平台发布