Redis实现秒杀

Lewis
2022-03-29 / 0 评论 / 134 阅读 / 正在检测是否收录...

思路

  1. 如果没有库存,则说明活动还未开始
  2. 如果库存数量小于1,则说明活动已结束
  3. 如果秒杀成功列表中包含当前用户,则说明该用户已成功参与,不允许多次参与

步骤

  1. 拼接库存和秒杀成功的key
  2. 判断当前用户是否已经参与
  3. 获取库存数量,判断活动是否开始,或者是已结束
  4. 进行减库存和添加用户操作

    Java 功能代码

    @Autowired
     private RedisTemplate<String, Object> redisTemplate;
    
     public boolean doSecondKill(String uid, String goodsId) {
    
         // 1. 参数校验
         if( StringUtils.isEmpty(uid) || StringUtils.isEmpty(goodsId) ) {
             return false;
         }
    
         // 2. 获取Redis操作对象
         ValueOperations<String, Object> strOps =redisTemplate.opsForValue();
         SetOperations<String, Object> setOps = redisTemplate.opsForSet();
    
         // 3. 拼接key
         // 3.1 库存key
         String stockKey = "SK:"+goodsId+":nums";
    
         // 3.2 秒杀用户成功key
         String userKey = "SK:" + goodsId + ":users";
    
         // 4. 获取库存,如果库存为空,秒杀还未开始
         Object stock = strOps.get(stockKey);
         if(StringUtils.isEmpty(stock)) {
             System.out.println("秒杀还未开始,请等待……");
             return false;
         }
    
         // 5. 判断用户是否重复秒杀操作
         if(setOps.isMember(userKey, uid)) {
             System.out.println("已参与");
             return false;
         }
    
         // 6. 判断商品数量,如果商品数量小于1,秒杀结束
         if(Integer.parseInt(stock.toString()) < 1) {
             System.out.println("秒杀已结束");
             return false;
         }
    
         // 7. 秒杀
         // 7.1 库存减一
         strOps.decrement(stockKey);
    
         // 7.2 秒杀成功的用户添加清单里面
         setOps.add(userKey, uid);
    
         return true;
     }

    存在问题 - 超卖问题

解决超卖问题 - Redis事务

public boolean doSecondKill(String uid, String goodsId) {

        // 1. 参数校验
        if( StringUtils.isEmpty(uid) || StringUtils.isEmpty(goodsId) ) {
            return false;
        }

        // 2. 获取Redis操作对象
        ValueOperations<String, Object> strOps =redisTemplate.opsForValue();
        SetOperations<String, Object> setOps = redisTemplate.opsForSet();

        // 3. 拼接key

        // 3.1 库存key
        String stockKey = "SK:"+goodsId+":nums";

        // 3.2 秒杀用户成功key
        String userKey = "SK:" + goodsId + ":users";

        redisTemplate.watch(stockKey);

        // 4. 获取库存,如果库存为空,秒杀还未开始
        Object stock = strOps.get(stockKey);
        if(StringUtils.isEmpty(stock)) {
            System.out.println("秒杀还未开始,请等待……");
            return false;
        }

        // 5. 判断用户是否重复秒杀操作
        if(setOps.isMember(userKey, uid)) {
            System.out.println("已参与");
            return false;
        }

        // 6. 判断商品数量,如果商品数量小于1,秒杀结束
        if(Integer.parseInt(stock.toString()) < 1) {
            System.out.println("秒杀已结束");
            return false;
        }

        // 7. 秒杀
        redisTemplate.multi();

        // 7.1 库存减一
        strOps.decrement(stockKey);

        // 7.2 秒杀成功的用户添加清单里面
        setOps.add(userKey, uid);

        List<Object> execs = redisTemplate.exec();
        if(null == execs || execs.isEmpty()) {
            System.out.println("秒杀失败");
            return false;
        }

        return true;
    }

存在问题 - 库存遗留问题

解决库存遗留问题 - 锁机制 + Lua

private void lock(String lockKey, String lockValue, String uid) {

        Boolean nativeLock = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, Duration.ofSeconds(30));
        System.out.println(uid + "->" + "加锁状态:" + nativeLock);
        if(nativeLock) {    // 加锁成功

            try {
                // 业务代码
                // ... 此处省略
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 解锁
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then redis.call('del', KEYS[1]) else return 0 end";
                Integer ret = redisTemplate.execute(new DefaultRedisScript<Integer>(script, Integer.class), Arrays.asList(lockKey), lockValue);
                System.out.println(uid + "->" + "解锁:" + "\t\t" + ret);
            }
        } else {    // 自旋锁
            System.out.println(uid + "->" + "加锁失败,睡眠100ms ");
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            lock(lockKey, lockValue, uid);
        }
    }

存在问题 - 超时

解决超时问题 - redisson + Lua

local stockKey = KEYS[1];
local userKey = KEYS[2];

local userId = ARGV[1];

-- 判断用户是否已参与
local userExists=redis.call("sismember", userKey, userId);
if type(userExists) == "number" and tonumber(userExists)==1 then
    -- 已参与
    return 2;
end

-- 库存判断 & 减库存操作
local num = redis.call("get", stockKey);
if type(num) == "boolean" then -- 还未开始
    return -1;
elseif type(num) == "number" and tonumber(num) <= 0 then -- 秒杀结束
    return 0;
else
    redis.call("decr", stockKey);
    redis.call("sadd", userKey, userId);
end

-- 秒杀成功
return 1;
private Boolean redissonLock(String lockName, String uid, String goodsId) {
        RLock lock = redissonClient.getLock(lockName);
        lock.lock();

        try {

            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
            redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/sk.lua")));
            redisScript.setResultType(Long.class);

            String stockKey = "SK:" + goodsId + ":stocks";
            String userKey = "SK:" + goodsId + ":users";

            // System.out.println(stockKey);
            // System.out.println(userKey);

            Long ret = redisTemplate.execute(redisScript, Arrays.asList(stockKey, userKey), uid);
            System.out.println(ret);
            return ret == 1;
        } finally {
            lock.unlock();
        }

    }
0

评论 (0)

取消