鬼魇举臂围城,覆淹星火,你恰是回头 万人中只一眼,却足以救我 ——《不可谖兮 》 伦桑

一、SetNx+Lua

1.setNx 

SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]
 
必选参数说明:
 
SET:命令
 
key:待设置的key
 
value:设置的key的value,最好为随机字符串
 
可选参数说明:
 
NX:表示key不存在时才设置,如果存在则返回 null
 
XX:表示key存在时才设置,如果不存在则返回NULL
 
PX millseconds:设置过期时间,过期时间精确为毫秒
 
EX seconds:设置过期时间,过期时间精确为秒

2.Lua表达式

减少网络开销:

原先多次请求的逻辑放在 redis 服务器上完成。使用脚本,减少了网络往返时延

原子操作:

Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入(想象为事务)

复用:

客户端发送的脚本会永久存储在Redis中,意味着其他客户端可以复用这一脚本而不需要 使用代码完成同样的逻辑

二、实现

1.SetNx加锁

/**
     * 加锁
     * @param lockKey 加锁的Key
     * @param timeStamp 时间戳:当前时间+超时时间
     * @return
     */
    public boolean lock(String lockKey, String timeStamp, long exper){
        if(stringRedisTemplate.opsForValue().setIfAbsent(lockKey, timeStamp, exper, TimeUnit.SECONDS)){
            // 对应setnx命令,可以成功设置,也就是key不存在,获得锁成功
            return true;
        }
        // 设置失败,获得锁失败
        // 判断锁超时 - 防止原来的操作异常,没有运行解锁操作 ,防止死锁
        String currentLock = stringRedisTemplate.opsForValue().get(lockKey);
        // 如果锁过期 currentLock不为空且小于当前时间
        if(!StrUtil.isEmpty(currentLock) && Long.parseLong(currentLock) < System.currentTimeMillis()){
            //如果lockKey对应的锁已经存在,获取上一次设置的时间戳之后并重置lockKey对应的锁的时间戳
            String preLock = stringRedisTemplate.opsForValue().getAndSet(lockKey, timeStamp);
 
            //假设两个线程同时进来这里,因为key被占用了,而且锁过期了。
            //获取的值currentLock=A(get取的旧的值肯定是一样的),两个线程的timeStamp都是B,key都是K.锁时间已经过期了。
            //而这里面的getAndSet一次只会一个执行,也就是一个执行之后,上一个的timeStamp已经变成了B。
            //只有一个线程获取的上一个值会是A,另一个线程拿到的值是B。
            if(!StrUtil.isEmpty(preLock) && preLock.equals(currentLock)){
                return true;
            }
        }
        return false;
    }

2.释放锁(delete/lua)

2.1.Delete锁

/**
     * 释放锁
     * @param lockKey
     * @param timeStamp
     */
    public void release(String lockKey, String timeStamp){
        try {
            String currentValue = stringRedisTemplate.opsForValue().get(lockKey);
            if(!StrUtil.isEmpty(currentValue) && currentValue.equals(timeStamp) ){
                // 删除锁状态
                stringRedisTemplate.opsForValue().getOperations().delete(lockKey);
            }
        } catch (Exception e) {
            System.out.println("警报!警报!警报!解锁异常");
        }
    }

2.2.通过LUA表达式实现安全解锁

/**
     * 通过LUA脚本来实现安全地解锁
     * @param key
     * @param val
     */
    public void releaseLua(String key, String val) {
        String luaScript3 = "if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end";
        RedisScript<Boolean> redisScript = RedisScript.of(luaScript3, Boolean.class);
        stringRedisTemplate.execute(redisScript, Collections.singletonList(key), val);
    }

 三、示例

定义秒杀扣减库存场景,一秒10000个请求。

/**
     * 秒杀商品
     * @param token
     * @param skuCount
     * @return
     */
    public String seckillTwo(String token, Integer skuCount) {
        String result = "";
        long time = System.currentTimeMillis() + TIMEOUT;
        String key = "tokenSeckill:" + token;
 
        Object stockNum = getStockRedis(STOCK_KEY);
        if (Integer.parseInt(stockNum.toString()) > 0) {
            // 对sku上锁
            boolean flag = redisLock.lock(key, String.valueOf(time), 5);
            if (!flag) {
                log.info("排队人数太多,请稍后再试.");
            }
            try {
                log.info("{}>>上锁时间戳>>{}", token, time);
                // 判断库存,进行扣减
                stockNum = getStockRedis(STOCK_KEY);
                assert stockNum != null;
                Integer stock = Integer.parseInt(stockNum.toString());
                // 上锁后等待10秒,锁过期了,直接超卖
                // Thread.sleep(10000);
                if (stock <= 0) {
                    //log.info("对不起,卖完了");
                } else if (stock < skuCount) {
                    log.info("对不起,库存不足");
                } else {
                    log.info("剩余库存>>{}", stock);
                    log.info("扣减库存>>{}", skuCount);
                    try {
                        // updateStock(skuCount);
                        updateStockRedis(STOCK_KEY, Long.valueOf(skuCount));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    log.info("{}=>购买成功!!!", token);
                    result = token + "购买成功";
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 释放锁
                redisLock.releaseLua(key, String.valueOf(time));
                log.info("{}>>释放锁时间戳>>{}", token, time);
            }
        }
        return result;
    }
 
/**
     * 如果响应延迟(上锁后等待10秒,锁过期了,直接超卖)
     * 上一个线程A未释放锁,锁就过期了。下一个线程B拿到锁,
     * 但是线程A执行完成释放锁,释放的是线程B的锁。
     */
    @SneakyThrows
    @Test
    public void testLock() {
        setStockRedis(STOCK_KEY);
        long startTime = System.currentTimeMillis();
        List<String> strings = new ArrayList<>();
        // 并行
        IntStream.range(0, 1000).parallel().forEach(b -> {
            String result = seckillTwo("sku-" + b, b);
            System.out.println(b);
            if (StrUtil.isNotBlank(result)) {
                strings.add(result);
            }
        });
        long endTime = System.currentTimeMillis() - startTime;
        log.info("耗时->>>>>{}", endTime);
 
        strings.forEach(s -> {
            System.out.println(s);
        });
    }

简约不是少,而是没有多余,足够也不是多,而是刚好你在。