自己写分布式锁--基于redission
之前的文章中,我们利用Redis实现了分布式限流组件,文章链接:自己写分布式限流组件-基于Redis的RateLimter ,不得不感叹Redis功能的强大,本文中我们继续利用Redis的特性,基于Redission组件,实现一款能注解支持的可靠分布式锁组件。
项目已经发布到GitHub,到目前有41个star,地址为https://github.com/TaXueWWL/redis-distributed-lock 。
项目简介
该分布式锁名称为redis-distributed-lock,是一个多module的工程。提供纯Java方式调用,支持传统Spring工程, 为spring boot应用提供了starter,开箱即用。
项目的目录结构及其描述如下:
项目 | 描述 |
---|---|
redis-distributed-lock-core | 原生redis分布式锁实现,支持注解,不推荐项目中使用,仅供学习使用 |
redis-distributed-lock-demo-spring | redis-distributed-lock-core 调用实例,仅供学习 |
redis-distributed-lock-starter | 基于Redisson的分布式锁spring starter实现,可用于实际项目中 |
redis-distributed-lock-starter-demo | redis-distributed-lock-starter调用实例 |
由于篇幅限制, redis-distributed-lock-core 及 redis-distributed-lock-demo-spring这两个工程我就不在本文中间介绍了,感兴趣的同学可以看我这篇文章的redis分布式锁部分,就是介绍的这两个工程的原理实现。 分布式锁的多种实现
本文主要讲解一下redis-distributed-lock-starter的使用及实现机制,首先说一下如何使用吧,这样能够直观的对它进行一个较为全面的了解,后面讲到代码实现能够更好的理解其机制。
如何使用?
redis-distributed-lock-starter是一个spring-boot-starter类的类库,关于starter的实现机制,可以看我另一篇文章 Springboot自动配置魔法之自定义starter 。
- 首先保证当前工程为一个springboot项目,然后添加starter的坐标依赖。(需要下载源码在本地构建,并执行mvn clean install -DskipTests)
坐标为:
当前最新版为1.2.0 截止到2019.4.19
<!--分布式锁redisson版本-->
<dependency>
<groupId>com.snowalker</groupId>
<artifactId>redis-distributed-lock-starter</artifactId>
<version>1.2.0</version>
</dependency>
在项目的application.properties中增加redis连接配置,这里以单机模式为例。目前支持单机、集群、哨兵、主从等全部的链接方式,总有适合你的一个。注意配置密码及需要使用的数据库,密码如果没有默认为空
######################################################################## # # redisson配置 # ######################################################################### redisson.lock.server.address=127.0.0.1:6379 redisson.lock.server.password= redisson.lock.server.database=1 redisson.lock.server.type=standalone
在启动类添加注解,@EnableRedissonLock 打开Redisson分布式锁自动装配,如:
@EnableRedissonLock @EnableScheduling @SpringBootApplication public class RedisDistributedLockStarterDemoApplication { public static void main(String[] args) throws Exception { SpringApplication.run(RedisDistributedLockStarterDemoApplication.class, args); } }
有两种调用方式,一种是直接java代码编程方式调用,一种是注解支持。
直接编程方式调用如下,在需要加锁的定时任务中,注入 RedissonLock 实体,即可进行加锁、解锁等操作。 锁自动释放时间默认为10秒,这个时间需要你根据自己的业务执行时间自行指定。
@Autowired RedissonLock redissonLock; @Scheduled(cron = "${redis.lock.cron}") public void execute() throws InterruptedException { if (redissonLock.lock("redisson", 10)) { LOGGER.info("[ExecutorRedisson]--执行定时任务开始,休眠三秒"); Thread.sleep(3000); System.out.println("=======业务逻辑==============="); LOGGER.info("[ExecutorRedisson]--执行定时任务结束,休眠三秒"); redissonLock.release("redisson"); } else { LOGGER.info("[ExecutorRedisson]获取锁失败"); } }
注解方式调用如下,在需要加锁的定时任务的执行方法头部,添加 @DistributedLock(value = “redis-lock”, expireSeconds = 11) 即可进行加锁、解锁等操作(value表示锁在redis中存放的key值,expireSeconds表示加锁时间)。锁自动释放时间默认为10秒,这个时间需要你根据自己的业务执行时间自行指定。我这里以spring schedule定时任务为例,用其他的定时任务同理,只需要添加注解。
@Scheduled(cron = "${redis.lock.cron}") @DistributedLock(value = "redis-lock", expireSeconds = 11) public void execute() throws InterruptedException { LOGGER.info("[ExecutorRedisson]--执行定时任务开始,休眠三秒"); Thread.sleep(3000); System.out.println("======业务逻辑======="); LOGGER.info("[ExecutorRedisson]--执行定时任务结束,休眠三秒"); }
- 你可以改变测试demo的端口,起多个查看日志,能够看到同一时刻只有一个实例获取锁成功并执行业务逻辑
调用日志如下所示,可以看出,多个进程同一时刻只有一个运行,表明我们的锁添加成功且生效。
2018-07-11 09:48:06.330 |-INFO [main] com.snowalker.RedisDistributedLockStarterDemoApplication [57] -|
Started RedisDistributedLockStarterDemoApplication in 3.901 seconds (JVM running for 4.356)
2018-07-11 09:48:10.006 |-INFO [pool-3-thread-1] com.snowalker.lock.redisson.annotation.DistributedLockHandler [32] -|
[开始]执行RedisLock环绕通知,获取Redis分布式锁开始
2018-07-11 09:48:10.622 |-INFO [pool-3-thread-1] com.snowalker.lock.redisson.RedissonLock [35] -|
获取Redisson分布式锁[成功],lockName=redis-lock
2018-07-11 09:48:10.622 |-INFO [pool-3-thread-1] com.snowalker.lock.redisson.annotation.DistributedLockHandler [39] -|
获取Redis分布式锁[成功],加锁完成,开始执行业务逻辑...
2018-07-11 09:48:10.625 |-INFO [pool-3-thread-1] com.snowalker.executor.ExecutorRedissonAnnotation [22] -|
[ExecutorRedisson]--执行定时任务开始,休眠三秒
=======================业务逻辑=============================
2018-07-11 09:48:13.625 |-INFO [pool-3-thread-1] com.snowalker.executor.ExecutorRedissonAnnotation [25] -|
[ExecutorRedisson]--执行定时任务结束,休眠三秒
2018-07-11 09:48:13.627 |-INFO [pool-3-thread-1] com.snowalker.lock.redisson.annotation.DistributedLockHandler [46] -|
释放Redis分布式锁[成功],解锁完成,结束业务逻辑...
2018-07-11 09:48:13.628 |-INFO [pool-3-thread-1] com.snowalker.lock.redisson.annotation.DistributedLockHandler [50] -|
[结束]执行RedisLock环绕通知
使用还是比较简单的,接下来我们走进代码细节,看一下如何实现一个易用的分布式锁组件。
代码及实现原理分析
为了符合开放封闭原则,所以我们只要把编码方式的分布式锁实现设计好,那么将其扩张成注解形式的就很容易。
1.redis连接配置,RedissonManager
由于我们使用的是Redission对Redis操作,因此首先建立一个RedissonManager类,用于提供初始化的redisson实例给核心业务使用。
代码如下
public class RedissonManager {
private static final Logger LOGGER = LoggerFactory.getLogger(Redisson.class);
private Config config = new Config();
private Redisson redisson = null;
public RedissonManager() {}
public RedissonManager (String connectionType, String address) {
try {
config = RedissonConfigFactory.getInstance().createConfig(connectionType, address);
redisson = (Redisson) Redisson.create(config);
} catch (Exception e) {
LOGGER.error("Redisson init error", e);
e.printStackTrace();
}
}
public Redisson getRedisson() {
return redisson;
}
/**
* Redisson连接方式配置工厂
*/
static class RedissonConfigFactory {
private RedissonConfigFactory() {}
private static volatile RedissonConfigFactory factory = null;
public static RedissonConfigFactory getInstance() {
if (factory == null) {
synchronized (RedissonConfigFactory.class) {
factory = new RedissonConfigFactory();
}
}
return factory;
}
private Config config = new Config();
/**
* 根据连接类型及连接地址参数获取对应连接方式的配置,基于策略模式
* @param connectionType
* @param address
* @return Config
*/
Config createConfig(String connectionType, String address) {
Preconditions.checkNotNull(connectionType);
Preconditions.checkNotNull(address);
/**声明配置上下文*/
RedissonConfigContext redissonConfigContext = null;
if (connectionType.equals(RedisConnectionType.STANDALONE.getConnection_type())) {
redissonConfigContext = new RedissonConfigContext(new StandaloneRedissonConfigStrategyImpl());
} else if (connectionType.equals(RedisConnectionType.SENTINEL.getConnection_type())) {
redissonConfigContext = new RedissonConfigContext(new SentinelRedissonConfigStrategyImpl());
} else if (connectionType.equals(RedisConnectionType.CLUSTER.getConnection_type())) {
redissonConfigContext = new RedissonConfigContext(new ClusterRedissonConfigStrategyImpl());
} else if (connectionType.equals(RedisConnectionType.MASTERSLAVE.getConnection_type())) {
redissonConfigContext = new RedissonConfigContext(new MasterslaveRedissonConfigStrategyImpl());
} else {
throw new RuntimeException("创建Redisson连接Config失败!当前连接方式:" + connectionType);
}
return redissonConfigContext.createRedissonConfig(address);
}
}
}
很好理解,通过构造方法,我们将Redis连接类型(包括:单机STANDALONE、集群CLUSTER、主从MASTERSLAVE、哨兵SENTINEL)以及rredis地址注入,并调用内部类RedissonConfigFactory工厂,生产出对应的Redis连接配置。
这里我使用了策略模式,根据构造方法传递的连接类型选择不同的连接实现,从配置上下文RedissonConfigContext中取出对应的模式的连接。这里不是我们的重点,感兴趣的同学们可以自行查看代码实现。
2. redis连接类型枚举RedisConnectionType
public enum RedisConnectionType {
STANDALONE("standalone", "单节点部署方式"),
SENTINEL("sentinel", "哨兵部署方式"),
CLUSTER("cluster", "集群方式"),
MASTERSLAVE("masterslave", "主从部署方式");
private final String connection_type;
private final String connection_desc;
private RedisConnectionType(String connection_type, String connection_desc) {
this.connection_type = connection_type;
this.connection_desc = connection_desc;
}
public String getConnection_type() {
return connection_type;
}
public String getConnection_desc() {
return connection_desc;
}
}
该枚举为穷举出的目前支持的四种Redis连接方式。
3. 分布式锁核心实现RedissonLock
RedissonLock是本工程的核心实现类,我们边看代码边解释
public class RedissonLock {
private static final Logger LOGGER = LoggerFactory.getLogger(RedissonLock.class);
RedissonManager redissonManager;
public RedissonLock(RedissonManager redissonManager) {
this.redissonManager = redissonManager;
}
这里通过构造方法将之前定义的RedissonManager注入锁实例中,用于在建立好的连接上获取RLock进行进一步的操作。
RLock是Redisson的分布式锁实现,原理也是基于setnx,只不过Redisson包装的更加优雅易用。
Redisson的分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口,同时还支持自动过期解锁。感兴趣的可以自行找资料学习,本文不展开讲解了。
public RedissonLock() {}
/**
* 加锁操作
* @return
*/
public boolean lock(String lockName, long expireSeconds) {
RLock rLock = redissonManager.getRedisson().getLock(lockName);
boolean getLock = false;
try {
getLock = rLock.tryLock(0, expireSeconds, TimeUnit.SECONDS);
if (getLock) {
LOGGER.info("获取Redisson分布式锁[成功],lockName={}", lockName);
} else {
LOGGER.info("获取Redisson分布式锁[失败],lockName={}", lockName);
}
} catch (InterruptedException e) {
LOGGER.error("获取Redisson分布式锁[异常],lockName=" + lockName, e);
e.printStackTrace();
return false;
}
return getLock;
}
lock(String lockName, long expireSeconds) 方法是核心加锁实现,我们设置了锁的名称,用于对应用进行区分,从而支持多应用的多分布式锁实现。
进入方法中,从连接中获取到RLock实现,调用boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) 设置传入的锁过期时间,并立即尝试获取锁。如果返回true则表明加锁成功,否则为加锁失败。
注意:加锁的时间要大于业务执行时间,这个时间需要通过测试算出最合适的值,否则会造成加锁失败或者业务执行效率过慢等问题。
/**
* 解锁
* @param lockName
*/
public void release(String lockName) {
redissonManager.getRedisson().getLock(lockName).unlock();
}
这个方法就比较好理解,在需要解锁的位置调用该方法,对存在的锁做解锁操作,内部实现为对setnx的值做过期处理。
注解支持
有了基本的java编程式实现,我们就可以进一步实现注解支持。
1. 注解定义
首先定义注解,支持方法级、类级限流。
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface DistributedLock {
/**分布式锁名称*/
String value() default "distributed-lock-redisson";
/**锁超时时间,默认十秒*/
int expireSeconds() default 10;
}
定义两个属性,value表示标注当前锁的key,建议命名规则为:应用名:模块名:方法名:版本号,从而更细粒度的区分。expireSeconds表示锁超时时间,默认10秒,超过该时间锁自动释放,可以用于下一次争抢。
2. 注解解析类DistributedLockHandler
接着我们定义一个注解解析类,这里使用aspectj实现。
@Aspect
@Component
public class DistributedLockHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(DistributedLockHandler.class);
@Pointcut("@annotation(com.snowalker.lock.redisson.annotation.DistributedLock)")
public void distributedLock() {}
@Autowired
RedissonLock redissonLock;
@Around("@annotation(distributedLock)")
public void around(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) {
LOGGER.info("[开始]执行RedisLock环绕通知,获取Redis分布式锁开始");
/**获取锁名称*/
String lockName = distributedLock.value();
/**获取超时时间,默认十秒*/
int expireSeconds = distributedLock.expireSeconds();
if (redissonLock.lock(lockName, expireSeconds)) {
try {
LOGGER.info("获取Redis分布式锁[成功],加锁完成,开始执行业务逻辑...");
joinPoint.proceed();
} catch (Throwable throwable) {
LOGGER.error("获取Redis分布式锁[异常],加锁失败", throwable);
throwable.printStackTrace();
}
redissonLock.release(lockName);
LOGGER.info("释放Redis分布式锁[成功],解锁完成,结束业务逻辑...");
} else {
LOGGER.error("获取Redis分布式锁[失败]");
}
LOGGER.info("[结束]执行RedisLock环绕通知");
}
}
我们使用环绕切面在业务逻辑之前进行加锁操作,如果加锁成功则执行业务逻辑,执行结束后,进行锁释放工作。这里需要优化一下,就是将解锁放到finally中。保证业务逻辑执行完成必定会释放锁。
到这里,我们就基本完成springboot支持的分布式锁实现,还差一点步骤。
添加自动装配支持
我们在resources下建立一个目录,名为META-INF , 并在其中定义一个文件,名为spring.factories,并在其中添加如下内容:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
com.snowalker.lock.redisson.config.RedissonAutoConfiguration
这样做之后,当依赖该starter的项目启动之后,会自动装配我们的分布式锁相关的实体,从而实现自动化的配置。等应用启动完成之后,就会自动获取锁配置。
附录:redis配置方式
前文已经提到,我们的分布式支持各种形式的redis连接方式,下面展开说明一下,实际使用的时候可以参考这里的配置,结合实际的redis运行模式进行配置。
redisson分布式锁配置–单机
redisson.lock.server.address=127.0.0.1:6379
redisson.lock.server.type=standalone
redisson分布式锁配置–哨兵
redisson.lock.server.address 格式为: sentinel.conf配置里的sentinel别名,sentinel1节点的服务IP和端口,sentinel2节点的服务IP和端口,sentinel3节点的服务IP和端口
比如sentinel.conf里配置为sentinel monitor my-sentinel-name 127.0.0.1 6379 2,那么这里就配置my-sentinel-name
redisson.lock.server.address=my-sentinel-name,127.0.0.1:26379,127.0.0.1:26389,127.0.0.1:26399
redisson.lock.server.type=sentinel
redisson分布式锁配置–集群方式
cluster方式至少6个节点(3主3从,3主做sharding,3从用来保证主宕机后可以高可用)
地址格式为: 127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381,127.0.0.1:6382,127.0.0.1:6383,127.0.0.1:6384
redisson.lock.server.address=127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381,127.0.0.1:6382,127.0.0.1:6383,127.0.0.1:6384
redisson.lock.server.type=cluster
redisson分布式锁配置–主从
地址格式为主节点,子节点,子节点
比如:127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381
代表主节点:127.0.0.1:6379,从节点127.0.0.1:6380,127.0.0.1:6381
redisson.lock.server.address=127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381
redisson.lock.server.type=masterslave
具体的实现过程,请参考源码的com.snowalker.lock.redisson.config.strategy 包,我在这里使用了策略模式进行各个连接方式的实现工作。