鬼魇举臂围城,覆淹星火,你恰是回头 万人中只一眼,却足以救我 ——《不可谖兮 》 伦桑
一、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);
});
}
简约不是少,而是没有多余,足够也不是多,而是刚好你在。