技术整理:SpringBoot+Redis+lua脚本防止超卖

SpringBoot+redis+lua 防止超卖

一、背景

工作中遇到了有人用 RedisTemplateincrement去做总库存的加减,但是这种方式是保证不了原子性的还是会超卖。

  • redis 是可以保证原子性,但是 RedisTemplate 里面的方法去调用redis是不能保证原子性

二、优化方案

使用 lua 脚本,去执行 加减操作,执行 redis 的命令,来保证原子性

三、重点代码

RedisTemplate 注入

java 复制代码
@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;
    }
抢购帮助类并支持集群模式
java 复制代码
/**
 * @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 脚本
java 复制代码
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
并发测试类
java 复制代码
/**
 * @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多平台发布

相关推荐
JustHappy8 小时前
古法编程秘籍(二):什么是代码模块化?别背概念,把房间收拾明白就够了
前端·后端
小江的记录本9 小时前
【JVM虚拟机】堆内存分代模型:年轻代(Eden+Survivor)、老年代、元空间Metaspace(附《思维导图》+《面试高频考点清单》)
java·前端·jvm·后端·python·spring·面试
IT_陈寒12 小时前
Python闭包里藏的这个坑,差点让我加班到凌晨
前端·人工智能·后端
IT_陈寒12 小时前
Java注解空指针?这个坑我踩得莫名其妙
前端·人工智能·后端
土狗TuGou13 小时前
SQL内功笔记 · 第8篇:事务的四大特性与隔离级别
数据库·笔记·后端·sql·mysql·oracle
ZengLiangYi13 小时前
React Query + REST API 最佳实践
javascript·后端·react.js
星浩AI13 小时前
项目实战:合同智能审批 · LangGraph + HITL 人机协同方案 [有源码]
后端·langchain·agent
JavaGuide13 小时前
Codex 接入第三方模型 DeepSeek、GLM、Kimi 教程:CC-Switch 和 Codex++ 两种方案对比
后端·ai编程
ZengLiangYi13 小时前
Fastify 加 Electron:把 Web 服务嵌进桌面应用
前端·javascript·后端
李白你好14 小时前
页面资产梳理 · 技术指纹识别 · Spring 端点探测
java·后端·spring