在第一次的版本中,使用redis模拟秒杀,最终结果,单个线程可以执行,使用了阿帕奇的ab工具,进行了压力测试后,出现了超卖问题,本代码中,针对此问题进行解决。

在redis中,提供了事务的概念,redis的事务在执行过程中,不会被打断,multi开启事务,在此之后的才做将被添加至操作队列中,如图

java中redis怎么解决商品超卖的问题 java 秒杀 redis 超卖_缓存


添加完成后,可以使用exec进行执行

java中redis怎么解决商品超卖的问题 java 秒杀 redis 超卖_System_02


如果想放弃,则可以使用discard取消执行的事务

java中redis怎么解决商品超卖的问题 java 秒杀 redis 超卖_java_03


要想完成秒杀超卖的问题,当然还有一个非常重要的点,redis的watch方法,它是一个乐观锁的命令,会未监控的key增加版本号,在事务执行前,监控key,如果执行中,key的版本号发生变化,则事务取消,不会执行

import com.lixl.redis.utils.RandomUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @author lixl
 * @description 秒杀相关controller
 * @date 2022/2/10
 */
@RestController
public class SecKillController {

    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping("doSecKill")
    public String doSeckill(String goodId){
        String userId = RandomUtils.getUserId()+"";
        if (!StringUtils.hasText(goodId)){
            System.out.println("没有该类商品的秒杀");
            return "没有该类商品的秒杀";
        }
        // 拼接库存key
        String kcKey = "good:"+goodId+":kc";
        // 拼接秒杀成功用户key
        String userKey = "good:"+goodId+":user";
        // 判断是否有库存
        Boolean hasKey = redisTemplate.hasKey(kcKey);
        if (!hasKey){
            System.out.println("没有该类商品的秒杀");
            return "没有该类商品的秒杀";
        }
        // 判断用户是否已经完成过秒杀
        Boolean isMember = redisTemplate.opsForSet().isMember(userKey, userId);
        if (isMember){
            System.out.println("您已经秒杀成功,请勿重复秒杀!");
            return "您已经秒杀成功,请勿重复秒杀!";
        }
        // 开启redis的事务,使用redisTemplate开启事务,需要用到SessionCallback,具体事务在它的模块中实现
        SessionCallback sessionCallback = new SessionCallback() {
            @Override
            public Object execute(RedisOperations redisOperations) throws DataAccessException {
                // 使用redis提供的监控数量功能(乐观锁,给每个数据一个版本号,不符合版本号的,无法执行)
                redisOperations.watch(kcKey);
                // 判断库存数量是否够
                String goodsNum = redisOperations.opsForValue().get(kcKey).toString();
                System.out.println(goodsNum);
                if (Integer.parseInt(goodsNum)<=0){
                    return "秒杀已经结束,抢购失败!";
                }
                redisOperations.multi();
                // 库存数量-1
                redisOperations.opsForValue().decrement(kcKey);
                // 设置用户
                redisOperations.opsForSet().add(userKey,userId);
                // 执行事务
                List exec = redisOperations.exec();
                if (exec == null || exec.size() == 0){
                    return "很遗憾,您未秒杀到该商品";
                } else {
                    return "恭喜您,秒杀成功!";
                }
            }
        };
        // 执行事务
        Object execute = redisTemplate.execute(sessionCallback);
        System.out.println(execute.toString());
        return execute.toString();
    }

}

使用ab工具

ab -n 1000 -c 100 http://localhost:8080/doSecKill?goodId=1001

发送1000次请求 并发100

java中redis怎么解决商品超卖的问题 java 秒杀 redis 超卖_System_04


可以看到,最后只有0个,并未出现超卖问题,但是在情况下,如果设置多一点的库存,多一点的并发,又会怎样(500库存,1000请求,500并发)

java中redis怎么解决商品超卖的问题 java 秒杀 redis 超卖_System_05


java中redis怎么解决商品超卖的问题 java 秒杀 redis 超卖_redis_06


此时可以看到,还有342个商品,未被秒杀到,还需要接着完善代码

java中redis怎么解决商品超卖的问题 java 秒杀 redis 超卖_redis_07