以前的文章中,咱们利用Redis实现了分布式限流组件,文章连接:本身写分布式限流组件-基于Redis的RateLimter ,不得不感叹Redis功能的强大,本文中咱们继续利用Redis的特性,基于Redission组件,实现一款能注解支持的可靠分布式锁组件。java
项目已经发布到GitHub,到目前有41个star,地址为github.com/TaXueWWL/re… 。git
该分布式锁名称为redis-distributed-lock,是一个多module的工程。提供纯Java方式调用,支持传统Spring工程, 为spring boot应用提供了starter,开箱即用。github
项目的目录结构及其描述以下:redis
项目 | 描述 |
---|---|
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分布式锁部分,就是介绍的这两个工程的原理实现。 分布式锁的多种实现spring
本文主要讲解一下redis-distributed-lock-starter的使用及实现机制,首先说一下如何使用吧,这样可以直观的对它进行一个较为全面的了解,后面讲到代码实现可以更好的理解其机制。数据库
redis-distributed-lock-starter是一个spring-boot-starter类的类库,关于starter的实现机制,能够看我另外一篇文章 Springboot自动配置魔法之自定义starter 。编程
坐标为:springboot
当前最新版为1.2.0 截止到2019.4.19app
<!--分布式锁redisson版本-->
<dependency>
<groupId>com.snowalker</groupId>
<artifactId>redis-distributed-lock-starter</artifactId>
<version>1.2.0</version>
</dependency>复制代码
########################################################################
#
# 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
@EnableScheduling
@SpringBootApplication
public class RedisDistributedLockStarterDemoApplication {复制代码
public static void main(String[] args) throws Exception {
SpringApplication.run(RedisDistributedLockStarterDemoApplication.class, args);
}
}复制代码
@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]获取锁失败");
}
}
2. 注解方式调用以下,在须要加锁的定时任务的执行方法头部,添加 **@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]--执行定时任务结束,休眠三秒");
}
3. 你能够改变测试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环绕通知复制代码
使用仍是比较简单的,接下来咱们走进代码细节,看一下如何实现一个易用的分布式锁组件。
为了符合开放封闭原则,因此咱们只要把编码方式的分布式锁实现设计好,那么将其扩张成注解形式的就很容易。
因为咱们使用的是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中取出对应的模式的链接。这里不是咱们的重点,感兴趣的同窗们能够自行查看代码实现。
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链接方式。
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编程式实现,咱们就能够进一步实现注解支持。
首先定义注解,支持方法级、类级限流。
@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秒,超过该时间锁自动释放,能够用于下一次争抢。
接着咱们定义一个注解解析类,这里使用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运行模式进行配置。
redisson.lock.server.address=127.0.0.1:6379
redisson.lock.server.type=standalone复制代码
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复制代码
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复制代码
地址格式为主节点,子节点,子节点
好比: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 包,我在这里使用了策略模式进行各个链接方式的实现工做。