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";@Resourceprivate 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 thenredis.call('decr', KEYS[1])return stock - 1
elsereturn -1
end
并发测试类
/*** @Author liyue* @date 2024/3/22 15:14**/
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class SecKillProviderTest {@Resourceprivate SecKillProvider secKillProvider;@Testpublic 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");@Overridepublic void run() {secKillProvider.seckill(String.valueOf(num));}}
}
总结
使用这种方式,读取文件可以优化成sha的方式去去读取。后面再进行调整,测试类可以模拟并发情况。测试没有什么问题。
本文由mdnice多平台发布