分布式锁
- setnx 命令,在finally里面释放锁。
- 为了防止执行完“添加锁”语句后突然宕机,其 finally 中的释放锁代码不执行,为锁添加过期时间。
- 为了防止本线程的业务还没处理完锁就过期了,导致另一个线程B拿到锁,结果本线程继续执行误把线程B设置的锁给删了,所以要为锁添加标识,生成UUID作为锁的 value
- 在finally语句块里面要判断是不是自己的锁,取得锁+判断+删除是非原子性的,在并发场景下可能会出问题。例如,客户端 a 在节点主机 A 中添加了锁后,执行业务逻辑用时 6 秒,此时锁已过期,然后执行到了 finally{}中的判断,并判断结果为真,然后时间片到了,暂停执行。由于节点主机 A 中的锁已经过期,客户端 b 在节点主机 B 中添加锁成功,然后很快执 行到了业务逻辑(未超过锁的过期时间),此时客户端 b 的处理进程时间片到了。此时主机 A 中的代码又获得了处理机,继续执行。此时就会执行对锁的删除语句,删除成功。也就是说主机 A 删除了由主机 B 添加的锁。这就是很严重的问题。解决方法是加Lua脚本
对客户端身份的判断与删除锁操作的合并,是没有专门的原子性命令的。此时可以通过Lua 脚本来实现它们的原子性。而对 Lua 脚本的执行,可以通过 eval 命令来完成。不过, eval 命令在 RedisTemplate 中没有对应的方法,而 Jedis 中具有该同名方法。所以,需要在代码中首先获取到 Jedis 客户端,然后才能调用 jedis.eval() 。
@GetMapping("/sk5")
public String seckillHandler5() {// 为每一个访问的客户端随机生成一个客户端唯一标识String clientId = UUID.randomUUID().toString();try {// 在添加锁的同时为锁指定过期时间,该操作具有原子性// 将锁的value设置为clientIdBoolean 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) {// 修改库存后再写回Redissrt.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 出一个子进程,用于启动一个定时任务。该定时任务的定时时间小于锁的过期时间,其会定时查看处理当前请求的业务进程的锁是否已被删除。如果已被删除,则子进程结束;如果未被删除,说明当前请求的业务还未处理完毕,则将锁的时间重新设置为“原过期时间”。这种方式称为锁续约,也称为锁续命。
Redisson 可重入锁 - 单机Redis下分布式锁
<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) {// 修改库存后再写回Redissrt.opsForValue().set("sk:0008", String.valueOf(--amount));return "库丰剩余" + amount + "台";}} catch (InterruptedException e) {e.printStackTrace();} finally {// 释放锁rLock.unlock();}return "抱歉,您没有抢到";
} Redisson - 红锁 - 集群Redis下分布式锁
@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) {// 修改库存后再写回Redissrt.opsForValue().set("sk:0008", String.valueOf(--amount));return "库丰剩余" + amount + "台";}} finally {// 释放锁rLock.unlock();}return "抱歉,您没有抢到";
} 分段锁
Redisson 详解
@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) {// 修改库存后再写回Redissrt.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个信号,返回辨识IDpermitId = rs.acquire();// 申请1个信号,若没有成功,则最多等待10秒,返回辨识IDpermitId = rs.tryAcquire(10, TimeUnit.SECONDS);// 业务逻辑// ……} catch (Exception e) {e.printStackTrace();} finally {// 对信号量的释放(V操作)// 释放1个信号量,需要携带辨识IDrs.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;}