以前的文章中,咱們利用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 包,我在這裏使用了策略模式進行各個鏈接方式的實現工做。