目录

1、分布式锁理解及手动实现

2、Redisson介绍

3、引入依赖

4、Redis配置

5、RedissonConf配置

6、API介绍

7、效果演示

7.1、lock()方法演示

7.2、演示看门狗机制

7.3、演示无看门狗情况

8、公平锁

9、红锁

10、联锁

11、Lua脚本实现可重入分布式锁

1、分布式锁理解及手动实现

Redis实现分布式锁(SETNX)_mlwsmqq的博客-CSDN博客本文详细介绍了什么是分布式锁、分布式锁的特征、应用场景;一步一步的手动实现分布式锁,分析其中需要特别注意的地方,带着大家理清其中的思路;相信对大家会有所帮助https://blog.csdn.net/mlwsmqq/article/details/127723729

2、Redisson介绍

        Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid),是一款具有诸多高性能功能的综合类开源中间件,提供的功能特性及其在项目中所起的作用远大于原生Redis所提供的各种功能,让开发者对Redis的关注进行分离,可以将更多的精力放在处理业务逻辑上。

        Redisson支持Redis多种连接方式(单机、集群、主从、哨兵等),包含分布式锁、布隆过滤器、分布式对象、分布式集合等多种工具。

3、引入依赖

org.redisson

redisson-spring-boot-starter

3.16.0

4、Redis配置

redis:

host: 192.168.xx.xx

port: xxx

password: xxx

database: 1

timeout: 7200s

jedis:

pool:

max-idle: 500

min-idle: 50

max-wait: -1

max-active: -1

5、RedissonConf配置

package com.example.learningexpansion.conf;

import org.redisson.Redisson;

import org.redisson.api.RedissonClient;

import org.redisson.config.Config;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.cache.annotation.EnableCaching;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

@Configuration

@EnableCaching

public class RedissonConf {

@Value("${spring.redis.host}")

private String host;

@Value("${spring.redis.port}")

private String port;

@Value("${spring.redis.password}")

private String password;

@Bean(destroyMethod = "shutdown")

public RedissonClient redissonClient(){

Config config = new Config();

/**

* 连接哨兵:config.useSentinelServers().setMasterName("myMaster").addSentinelAddress()

* 连接集群:config.useClusterServers().addNodeAddress()

* 连接主从:config.useMasterSlaveServers().setMasterAddress("xxx").addSlaveAddress("xxx")

*/

// 连接单机

config.useSingleServer()

.setAddress("redis://"+host+":"+port)

.setPassword(password);

RedissonClient client = Redisson.create(config);

return client;

}

}

6、API介绍

@Api(tags = "Redis")

@RestController

@RequestMapping("/testRedis")

@Slf4j

public class TestRedisController {

@Resource

private RedissonClient redissonClient;

@GetMapping("/testRedisson")

@ApiOperation("Redisson")

public ResultVO testRedisson(@RequestParam Long goodsId) {

RLock lock = redissonClient.getLock("lock_" + goodsId);

String threadName = Thread.currentThread().getName();

try {

// 注意:若设置了锁的过期时间则没有看门狗机制

// 阻塞,拿不到锁会一直尝试获取;锁的有效期默认30秒,有看门狗机制延长锁的有效期

lock.lock();

// 阻塞,加锁成功后设置指定的有效时间,时间到自动释放锁(无论拿到锁线程是否执行结束),前提是没有调用解锁方法;没有看门狗

lock.lock(10,TimeUnit.SECONDS);

// 尝试获取锁,加锁成功后启动看门狗;非阻塞,失败立马返回;注意释放锁时要判断是否存在及是否被当前线程保持

boolean tryLock = lock.tryLock();

if (!tryLock){

return ResultUtils.error("加锁失败,请稍后重试!");

}

// 在指定时间内尝试获取锁,失败立即返回;有看门狗

boolean tryLock2 = lock.tryLock(5, TimeUnit.SECONDS);

if (!tryLock2){

return ResultUtils.error("加锁失败,请稍后重试!");

}

// 指定时间内尝试获取锁,失败立即返回;成功后设置有效时间为指定值,无看门狗

boolean tryLock1 = lock.tryLock(5, 10, TimeUnit.SECONDS);

if (!tryLock1){

return ResultUtils.error("加锁失败,请稍后重试!");

}

// 注意:异步加锁需要调用get()方法使线程执行完成,否则会造成多个线程同时拿到锁

RFuture voidRFuture = lock.lockAsync();

voidRFuture.get();

// 同lock.tryLock();

RFuture booleanRFuture = lock.tryLockAsync();

Boolean aBoolean = booleanRFuture.get();

if (!aBoolean){

return ResultUtils.error("加锁失败,请稍后成重试!");

}

// 注意重载方法中只有一个long时,要传的是线程ID

RFuture booleanRFuture1 = lock.tryLockAsync(Thread.currentThread().getId());

Boolean aBoolean1 = booleanRFuture1.get();

if (!aBoolean1){

return ResultUtils.error("加锁失败,请稍后重试!");

}

// 同lock.tryLock(5, TimeUnit.SECONDS)

RFuture rFuture = lock.tryLockAsync(3, TimeUnit.SECONDS);

Boolean aBoolean3 = rFuture.get();

if (!aBoolean3){

return ResultUtils.error("加锁失败,请稍后重试!");

}

// 同lock.tryLock(5, 10, TimeUnit.SECONDS)

RFuture booleanRFuture4 = lock.tryLockAsync(3, 10, TimeUnit.SECONDS);

Boolean aBoolean4 = booleanRFuture4.get();

if (!aBoolean4){

return ResultUtils.error("加锁失败,请稍后重试!");

}

// 原理同lock.tryLockAsync(3, 10, TimeUnit.SECONDS),区别在于多个是线程ID的参数

RFuture booleanRFuture5 = lock.tryLockAsync(3, 10, TimeUnit.SECONDS, Thread.currentThread().getId());

Boolean aBoolean5 = booleanRFuture5.get();

if (!aBoolean5) {

return ResultUtils.error("加锁失败,请稍后重试!");

}

log.info("{}:获取到锁", threadName);

TimeUnit.SECONDS.sleep(5);

log.info("{}:业务执行结束", threadName);

} catch (Exception e) {

log.error("testRedisson exception:", e);

return ResultUtils.sysError();

} finally {

// 判断锁是否存在

boolean locked = lock.isLocked();

// 判断锁是否被当前线程保持

boolean heldByCurrentThread = lock.isHeldByCurrentThread();

log.info("{}:获取锁状态:{} 是否当前线程保留:{}", threadName, locked, heldByCurrentThread);

if (locked && heldByCurrentThread) {

lock.unlock();

log.info("{}:释放锁", threadName);

} else {

log.info("{}:未获得锁不用释放", threadName);

}

}

return ResultUtils.success();

}

}

 需要注意的一点是只有在不指定锁的过期时间时,看门狗机制才会生效,从源码可知:

if (leaseTime != -1L) {

this.internalLockLeaseTime = unit.toMillis(leaseTime);

} else {

this.scheduleExpirationRenewal(threadId);

}

7、效果演示

7.1、lock()方法演示

        使用8701、8702端口同时启动两个服务,传入相同的参数,快速向两个服务各调用一次

        8701服务效果:

2022-12-30 11:10:12.111 INFO 4796 --- [nio-8701-exec-3] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-3:获取到锁

2022-12-30 11:10:17.112 INFO 4796 --- [nio-8701-exec-3] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-3:业务执行结束

2022-12-30 11:10:17.114 INFO 4796 --- [nio-8701-exec-3] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-3:获取锁状态:true 是否当前线程保留:true

2022-12-30 11:10:17.115 INFO 4796 --- [nio-8701-exec-3] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-3:释放锁

        8702服务效果:

2022-12-30 11:10:17.122 INFO 10168 --- [nio-8702-exec-6] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-6:获取到锁

2022-12-30 11:10:22.124 INFO 10168 --- [nio-8702-exec-6] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-6:业务执行结束

2022-12-30 11:10:22.126 INFO 10168 --- [nio-8702-exec-6] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-6:获取锁状态:true 是否当前线程保留:true

2022-12-30 11:10:22.128 INFO 10168 --- [nio-8702-exec-6] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-6:释放锁

        从上述日志可看出:8701服务先拿到锁,执行完业务释放锁后8702服务才能拿到锁,达到了分布式锁想要的效果

7.2、演示看门狗机制

        同样使用lock()方法,不指定过期时间(默认30秒),睡眠40s模拟执行业务,看是否自动续期

         8701服务效果:

2022-12-30 14:41:25.898 INFO 13804 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-1:获取到锁

2022-12-30 14:42:05.899 INFO 13804 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-1:业务执行结束

2022-12-30 14:42:05.901 INFO 13804 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-1:获取锁状态:true 是否当前线程保留:true

2022-12-30 14:42:05.905 INFO 13804 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-1:释放锁

          8702服务效果:

2022-12-30 14:42:05.913 INFO 14320 --- [nio-8702-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-1:获取到锁

2022-12-30 14:42:45.914 INFO 14320 --- [nio-8702-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-1:业务执行结束

2022-12-30 14:42:45.917 INFO 14320 --- [nio-8702-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-1:获取锁状态:true 是否当前线程保留:true

2022-12-30 14:42:45.921 INFO 14320 --- [nio-8702-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-1:释放锁

        从日志可看出每个拿到锁的线程都执行了40s,并且8702服务在8701服务执行到第30s的时候仍然没有拿到锁,说明自动续期生效啦

7.3、演示无看门狗情况

        同样使用lock()方法,指定过期时间为5s,睡眠8s模拟执行业务,看是否自动续期

        8701服务效果:

2022-12-30 14:33:49.840 INFO 12276 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-1:获取到锁

2022-12-30 14:33:57.840 INFO 12276 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-1:业务执行结束

2022-12-30 14:33:57.846 INFO 12276 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-1:获取锁状态:true 是否当前线程保留:false

2022-12-30 14:33:57.846 INFO 12276 --- [nio-8701-exec-1] c.e.l.c.testRedis.TestRedisController : http-nio-8701-exec-1:未获得锁不用释放

        8702服务效果:

2022-12-30 14:33:54.844 INFO 11932 --- [nio-8702-exec-5] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-5:获取到锁

2022-12-30 14:34:02.845 INFO 11932 --- [nio-8702-exec-5] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-5:业务执行结束

2022-12-30 14:34:02.848 INFO 11932 --- [nio-8702-exec-5] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-5:获取锁状态:false 是否当前线程保留:false

2022-12-30 14:34:02.848 INFO 11932 --- [nio-8702-exec-5] c.e.l.c.testRedis.TestRedisController : http-nio-8702-exec-5:未获得锁不用释放

        从日志可看出:8701服务先拿到锁执行,5s后锁自动失效被8702服务获取到锁,此时8701的业务仍未执行结束,因此可验证结论--指定过期时间时,不会自动续期

8、公平锁

        Redisson分布式锁支持公平和非公平,上文中使用的是非公平锁

        公平锁遵循先到先得的原则

@GetMapping("/testFairLock")

@ApiOperation("公平锁")

public ResultVO testFairLock(@RequestParam Long goodsId) {

RLock fairLock = redissonClient.getFairLock("fairLock_" + goodsId);

String threadName = Thread.currentThread().getName();

try {

fairLock.lock();

log.info("{}:获得锁,开始执行业务", threadName);

TimeUnit.SECONDS.sleep(3);

log.info("{}:执行结束", threadName);

return ResultUtils.success();

} catch (Exception e) {

log.error("testFairLock exception:", e);

return ResultUtils.sysError();

} finally {

boolean locked = fairLock.isLocked();

boolean heldByCurrentThread = fairLock.isHeldByCurrentThread();

log.info("{}:获取锁状态:{} 是否当前线程保留:{}", threadName, locked, heldByCurrentThread);

if (locked && heldByCurrentThread) {

fairLock.unlock();

log.info("{}:释放锁成功", threadName);

} else {

log.info("{}:未获得锁不用释放", threadName);

}

}

}

9、红锁

        可以使用红锁来解决主从架构锁失效问题:就是说在主从架构系统中,线程A从master中获取到分布式锁,数据还未同步到slave中时master就挂掉了,slave成为新的master,其它线程从新的master获取锁也成功了,就会出现并发安全问题

        红锁算法:

 应用程序获取系统当前时间,毫秒级 应用程序使用相同的key、value值依次从多个Redis实例中获取锁,如果某一个节点超过一定时间仍然没有获取到锁则直接放弃,尽快尝试从下一个Redis节点获取锁,以避免被宕机的节点阻塞计算获取锁的消耗时间=客户端程序当前时间-step1中的时间,获取锁的消耗时间小于总的锁定时间(例如30s)并且半数以上节点(假如有5个节点,则至少有3个节点)获取锁成功,才认为获取锁成功计算剩余锁定时间=总的锁定时间-step3中的消耗时间如果获取锁失败,对所有的Redis节点释放锁(无论加锁是否成功)

// 用于Redis集群架构下,这些节点是完全独立的,所以不使用复制或任何其他隐式协调系统

// 该对象可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例

@GetMapping("/testRedLock")

@ApiOperation("红锁")

public ResultVO testRedLock(@RequestParam Long id) {

String threadName = Thread.currentThread().getName();

RLock one = redissonClient.getLock("one_" + id);

RLock two = redissonClient.getLock("two_" + id);

RLock three = redissonClient.getLock("three_" + id);

RedissonMultiLock redLock = new RedissonRedLock(one, two, three);

try {

redLock.lock();

log.info("{}:获得锁,开始执行业务", threadName);

TimeUnit.SECONDS.sleep(2);

log.info("{}:执行结束", threadName);

return ResultUtils.success();

} catch (Exception e) {

log.error("testRedLock exception:", e);

return ResultUtils.sysError();

} finally {

// 注意:不能使用isLocked()和isHeldByCurrentThread()方法,会抛出UnsupportedOperationException异常

redLock.unlock();

log.info("{}:释放锁成功", threadName);

}

}

10、联锁

        联锁(RedissonMultiLock)对象可以将多个RLock对象关联为一个联锁,实现加锁和解锁功能。每个RLock对象实例可以来自于不同的Redisson实例。

        

@GetMapping("/testMultLock")

@ApiOperation("联锁")

public ResultVO testMultLock(@RequestParam Long id) {

String threadName = Thread.currentThread().getName();

RLock one = redissonClient.getLock("one_" + id);

RLock two = redissonClient.getLock("two_" + id);

RLock three = redissonClient.getLock("three_" + id);

RedissonMultiLock multiLock = new RedissonMultiLock(one, two, three);

try {

// 所有的锁都上锁成功才算成功

multiLock.lock();

log.info("{}:获得锁,开始执行业务", threadName);

TimeUnit.SECONDS.sleep(3);

log.info("{}:执行结束", threadName);

return ResultUtils.success();

} catch (Exception e) {

log.error("testMultLock exception:", e);

return ResultUtils.sysError();

} finally {

// 注意:不能使用isLocked()和isHeldByCurrentThread()方法,会抛出UnsupportedOperationException异常

multiLock.unlock();

log.info("{}:释放锁成功", threadName);

}

}

11、Lua脚本实现可重入分布式锁Lua脚本实现可重入分布式锁_mlwsmqq的博客-CSDN博客提到分布式锁,那一定绕不开Redisson,在深入Redisson源码时发现它使用了大量的lua脚本,为什么要使用lua脚本呢?答案就是它能够保证Redis操作的原子性;受到Redisson的启发,本文将带领大家一步步的通过lua脚本实现可重入分布式锁,还有两篇关于分布式锁的博客供大家参考。https://blog.csdn.net/mlwsmqq/article/details/128472150

        有任何错误,欢迎大家指正!

        转载请注明出处!转载请注明出处!

        若本文对大家有所启示,请动动小手点赞和收藏哦!!!

查看原文

发表评论

取消
扫码支持 支付码
返回顶部暗黑模式