redis扣库存-秒杀活动使用

简单利用redis的LUA脚本功能,一次性操作,实现原子性扣减库存

注释都写得明白,大家凑合着看吧,没有增加库存,直接是初始化一次库存量,后面等过期失效

特别注意一点,就是在集群模式下,需要解决依赖问题

第二个是,序列化的时候,需要把int long类型能转成功

先增加依赖

 <!--redis 两种分布式锁依赖包  lettuce 去掉这两个插件,扣库存的分布式连接有问题-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.4</version>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>

序列化RedisTemplate

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) throws Exception {
        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // 创建 序列化类
        GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer(Object.class);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(genericToStringSerializer);

        return redisTemplate;
    }

下面是业务的代码块,核心 点还是在扣库存的时候,不能超发,也不能扣到负数,

然后再同步到MYSQL里,初始化库存数量,这个可以从DB里取实际的量,

LUA脚本相对简单点,扣减逻辑都在里面,减少IO并且也是保证原子性问题,


/**
 *   RedisTemplate  通过 LUA脚本去库存操作
 * @author sky
 */
@Component
public class RedisTemplateStockUtil implements IStockCallback {

    Logger log = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private RedissonClient redissonClient;
    /**
     * 库存还未初始化
     */
    public static final long UNINITIALIZED_STOCK = -3L;
    /**
     * Redis 客户端
     */
    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;

    private static final String REDIS_KEY = "redis_key:stock:";
    /**
     * 执行扣库存的脚本
     */
    public static final String STOCK_LUA;

    static {
        /*
         *
         * @desc 扣减库存Lua脚本
         * 库存(stock)-1:表示不限库存
         * 库存(stock)0:表示没有库存
         * 库存(stock)大于0:表示剩余库存
         *
         * @params 库存key
         * @return
         *      -3:库存未初始化
         *        -2:库存不足
         *        -1:不限库存
         *        大于等于0:剩余库存(扣减之后剩余的库存),直接返回-1
         */
        StringBuilder sb = new StringBuilder();
        sb.append("if (redis.call('exists', KEYS[1]) == 1) then");
        sb.append("    local stock = tonumber(redis.call('get', KEYS[1]));");
        sb.append("    local num = tonumber(ARGV[1]);");
        sb.append("    if (stock == -1) then");
        sb.append("        return -1;");
        sb.append("    end;");
        sb.append("    if (stock >= num) then");
        sb.append("        return redis.call('incrby', KEYS[1], 0 - num);");
        sb.append("    end;");
        sb.append("    return -2;");
        sb.append("end;");
        sb.append("return -3;");
        STOCK_LUA = sb.toString();
    }

    @Override
    public long getStock(String batchNo, long expire, int num) {
        String key = REDIS_KEY+batchNo;
        // redisTemplate.delete(key);

        // 初始化库存
        long stock = stock(key, num);

        // 没有什么作用,可省略
        /*boolean hasKey = Boolean.TRUE.equals(redisTemplate.hasKey(key));
        if (hasKey){
            Integer batchNoLock = (Integer) redisTemplate.opsForValue().get(key);
            log.info("========== batchNoLock :{}",batchNoLock);
            log.info("======================== hasKey :{}",hasKey);
        }*/

        if (stock == UNINITIALIZED_STOCK) {
            RLock rLock = redissonClient.getLock(REDIS_KEY+":lock");
            try {
                // 获取锁
                if (rLock.tryLock(1, TimeUnit.SECONDS)) {
                    // 双重验证,避免并发时重复回源到数据库
                    stock = stock(key, num);
                    if (stock == UNINITIALIZED_STOCK) {
                        // 获取初始化库存   initStock = 100 参数
                        int initStock = initStock(1);
                        // 将库存设置到redis,  100是初始化库存参数
                        ValueOperations<String, Integer> valueOperations = redisTemplate.opsForValue();
                        valueOperations.set(key, 100, expire, TimeUnit.SECONDS);
                        // 调一次扣库存的操作
                        stock = stock(key, num);
                        Object stockNum = redisTemplate.opsForValue().get(key);
                        Integer batchNoLock = Integer.parseInt((String) stockNum);
                        log.info("batchNoLock key :{}",batchNoLock);
                    }
                }
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            } finally {
                if (rLock != null && rLock.isHeldByCurrentThread()) {
                    rLock.unlock();
                }

            }
        }
        log.info("stock num :{}",stock);
        return stock;
    }

    /**
     * 扣库存   这步特别注意,分布式连接有问题,需要依赖包里,去掉lettuce组件
     *
     * @param key 库存key
     * @param num 扣减库存数量
     * @return 扣减之后剩余的库存【-3:库存未初始化; -2:库存不足; -1:不限库存; 大于等于0:扣减库存之后的剩余库存】
     */
    private Long stock(String key, int num) {
        // 脚本里的KEYS参数
        List<String> keys = new ArrayList<>();
        keys.add(key);
        // 脚本里的ARGV参数
        List<String> args = new ArrayList<>();
        args.add(Integer.toString(num));
        return (Long) redisTemplate.execute((RedisCallback<Long>) connection -> {
            Object nativeConnection = connection.getNativeConnection();
            // 集群模式
            if (nativeConnection instanceof JedisCluster) {
                return (Long) ((JedisCluster) nativeConnection).eval(STOCK_LUA, keys, args);
            }
            // 单机模式
            else if (nativeConnection instanceof Jedis) {
                return (Long) ((Jedis) nativeConnection).eval(STOCK_LUA, keys, args);
            }
            return UNINITIALIZED_STOCK;
        });
    }

    /**
     * 获取初始的库存
     *
     * @return
     */
    private int initStock(long commodityId) {
        // TODO 这里做一些初始化库存的操作
        return 10;
    }

调用 方法

   @RequestMapping(value = "/stock/{batchNo}", method = RequestMethod.GET)
    public ResponseEntity<Object> reduceStock(@PathVariable String batchNo) {
        long numb = iStockCallback.getStock(batchNo,20,1);
        return ResponseEntity.ok("秒杀活动,扣减库存 num:" +numb);
    }

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值