对客户端身份的判断与删除锁操作的合并,是没有专门的原子性命令的。此时可以通过Lua 脚本来实现它们的原子性。而对 Lua 脚本的执行,可以通过 eval 命令来完成。不过, eval 命令在 RedisTemplate 中没有对应的方法,而 Jedis 中具有该同名方法。所以,需要在代码中首先获取到 Jedis 客户端,然后才能调用 jedis.eval() 。
@GetMapping("/sk5")
public String seckillHandler5() {
// 为每一个访问的客户端随机生成一个客户端唯一标识
String clientId = UUID.randomUUID().toString();
try {
// 在添加锁的同时为锁指定过期时间,该操作具有原子性
// 将锁的value设置为clientId
Boolean lockOK = srt.opsForValue().setIfAbsent(REDIS_LOCK, clientId, 5, TimeUnit.SECONDS);
if (!lockOK) {
return "没有抢到锁";
}
// 添加锁成功
// 从Redis中获取库存
String stock = srt.opsForValue().get("sk:0008");
int amount = stock == null ? 0 : Integer.parseInt(stock);
if (amount > 0) {
// 修改库存后再写回Redis
srt.opsForValue().set("sk:0008", String.valueOf(--amount));
return "库丰剩余" + amount + "台";
}
} finally {
// 锁续约,或锁续命
JedisPool jedisPool = new JedisPool(redisHost, redisPort);
try (Jedis jedis = jedisPool.getResource()) {
// 定义Lua脚本。注意,每行最后要有一个空格
// redis.call()是Lua中对Redis命令的调用函数
String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
"then return redis.call('del', KEYS[1]) " +
"end " +
"return 0";
// eval()方法的返回值为脚本script的返回值
Object eval = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(clientId));
if ("1".equals(eval.toString())) {
System.out.println("释放锁成功");
} else {
System.out.println("释放锁时发生异常");
}
}
}
return "抱歉,您没有抢到";
}
以上代码仍然是存在问题的:请求 a 的锁过期,但其业务还未执行完毕;请求 b 申请到了锁,其也正在处理业务。如果此时两个请求都同时修改了共享的库存数据,那么就又会出现数据不一致的问题,即仍然存在并发问题。在高并发场景下,问题会被无限放大。对于该问题,可以采用“锁续约”方式解决。即,在当前业务进程开始执行时, fork 出一个子进程,用于启动一个定时任务。该定时任务的定时时间小于锁的过期时间,其会定时查看处理当前请求的业务进程的锁是否已被删除。如果已被删除,则子进程结束;如果未被删除,说明当前请求的业务还未处理完毕,则将锁的时间重新设置为“原过期时间”。这种方式称为锁续约,也称为锁续命。
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.6</version>
</dependency>
@Bean
public Redisson redisson() {
Config Config = new Config();
Config.useSingleServer()
.setAddress(redisHost + ":" + redisPort)
.setDatabase(0);
return (Redisson) Redisson.create(Config);
}
在需要使用的类中注入
使用:
@GetMapping("/sk6")
public String seckillHandler6() {
RLock rLock = redisson.getLock(REDIS_LOCK);
try {
// 添加分布式锁
// Boolean lockOK = rLock.tryLock();
// 指定锁的过期时间为5秒
// Boolean lockOK = rLock.tryLock(5, TimeUnit.SECONDS);
// 指定锁的过期时间为5秒。如果申请锁失败,则最长等待20秒
Boolean lockOK = rLock.tryLock(20, 5, TimeUnit.SECONDS);
if (!lockOK) {
return "没有抢到锁";
}
// 添加锁成功
// 从Redis中获取库存
String stock = srt.opsForValue().get("sk:0008");
int amount = stock == null ? 0 : Integer.parseInt(stock);
if (amount > 0) {
// 修改库存后再写回Redis
srt.opsForValue().set("sk:0008", String.valueOf(--amount));
return "库丰剩余" + amount + "台";
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
rLock.unlock();
}
return "抱歉,您没有抢到";
}
@Bean("redisson-1")
public Redisson redisson1() {
Config Config = new Config();
Config.useSentinelServers()
.setMasterName("mymaster1")
.addSentinelAddress("redis:16380", "redis:16381", "redis:16382");
return (Redisson) Redisson.create(Config);
}
@Bean("redisson-2")
public Redisson redisson2() {
Config Config = new Config();
Config.useSentinelServers()
.setMasterName("mymaster2")
.addSentinelAddress("redis:26380", "redis:26381", "redis:26382");
return (Redisson) Redisson.create(Config);
}
@Bean("redisson-3")
public Redisson redisson3() {
Config Config = new Config();
Config.useSentinelServers()
.setMasterName("mymaster3")
.addSentinelAddress("redis:36380", "redis:36381", "redis:36382");
return (Redisson) Redisson.create(Config);
}
在需要使用的类注入使用
@GetMapping("/sk7")
public String seckillHandler7() {
// 定义三个可重入锁
RLock rLock1 = redisson1.getLock(REDIS_LOCK + "-1");
RLock rLock2 = redisson2.getLock(REDIS_LOCK + "-2");
RLock rLock3 = redisson3.getLock(REDIS_LOCK + "-3");
// 定义红锁
RLock rLock = new RedissonRedLock(rLock1, rLock2, rLock3);
try {
// 添加分布式锁
Boolean lockOK = rLock.tryLock();
if (!lockOK) {
return "没有抢到锁";
}
// 添加锁成功
// 从Redis中获取库存
String stock = srt.opsForValue().get("sk:0008");
int amount = stock == null ? 0 : Integer.parseInt(stock);
if (amount > 0) {
// 修改库存后再写回Redis
srt.opsForValue().set("sk:0008", String.valueOf(--amount));
return "库丰剩余" + amount + "台";
}
} finally {
// 释放锁
rLock.unlock();
}
return "抱歉,您没有抢到";
}
@GetMapping("/sk8")
public String seckillHandler8() {
RSemaphore rs = redisson.getSemaphore("redis_semaphore");
try {
int buy = ThreadLocalRandom.current().nextInt(5) + 1;
Boolean lockOK = rs.tryAcquire(buy, 10, TimeUnit.SECONDS);
if (!lockOK) {
return "没有抢到锁";
}
// 添加锁成功
// 从Redis中获取库存
String stock = srt.opsForValue().get("sk:0008");
int amount = stock == null ? 0 : Integer.parseInt(stock);
if (amount > 0) {
// 修改库存后再写回Redis
srt.opsForValue().set("sk:0008", String.valueOf(--amount));
return "库丰剩余" + amount + "台";
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return "抱歉,您没有抢到";
}
@GetMapping("/test2")
public String test2() {
RPermitExpirableSemaphore rs = redisson.getPermitExpirableSemaphore("redis_semaphore");
String permitId = null;
try {
// 对信号量的申请(P操作)
// 申请1个信号,返回辨识ID
permitId = rs.acquire();
// 申请1个信号,若没有成功,则最多等待10秒,返回辨识ID
permitId = rs.tryAcquire(10, TimeUnit.SECONDS);
// 业务逻辑
// ……
} catch (Exception e) {
e.printStackTrace();
} finally {
// 对信号量的释放(V操作)
// 释放1个信号量,需要携带辨识ID
rs.release(permitId);
boolean releaseOK = rs.tryRelease(permitId);
}
return null;
}
@GetMapping("/test3")
public String test3() {
// 获取闭锁对象(合并线程与条件线程中都需要该代码)
RCountDownLatch latch = redisson.getCountDownLatch("countDownLatch");
// 设置闭锁计数器初值,使用该语句的场景:
// 1)Redis中没有设置该值
// 2)Redis中设置了该值,但已经变为了0,需要重置
latch.trySetCount(10);
// 在合并线程中要等待着闭锁的打开
try {
// 阻塞合并线程,直到锁打开
latch.await();
// 阻塞合并线程,直到锁打开或5秒后
latch.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 条件线程代码
// 使闭锁计数器减一
latch.countDown();
return null;
}