概述
目前幾乎很多大型網站及應用都是分布式部署的,分布式場景中的數據一致性問題一直是一個比較重要的話題。分布式的CAP理論告訴我們“任何一個分布式系統都無法同時滿足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance),最多只能同時滿足兩項。”所以,很多系統在設計之初就要對這三者做出取舍。在互聯網領域的絕大多數的場景中,都需要犧牲強一致性來換取系統的高可用性,系統往往只需要保證“最終一致性”,只要這個最終時間是在用戶可以接受的范圍內即可。
在很多場景中,我們為了保證數據的最終一致性,需要很多的技術方案來支持,比如分布式事務、分布式鎖等。有的時候,我們需要保證一個方法在同一時間內只能被同一個線程執行。
基於數據庫實現分布式鎖;
基於緩存(Redis等)實現分布式鎖;
基於Zookeeper實現分布式鎖;
盡管有這三種方案,但是不同的業務也要根據自己的情況進行選型,他們之間沒有最好只有更適合!但由於操作數據庫需要一定的開銷,會照成一定的性能問題。並且使用數據庫行級鎖並不一定靠譜,尤其是當我們鎖表並不大的時候,所以實際開發過程中使用Redis或Zookeeper的比較多。
分布式鎖特性
當我們在設計分布式鎖的時候,我們應該考慮分布式鎖至少要滿足的一些條件,同時考慮如何高效的設計分布式鎖,這里我認為以下幾點是必須要考慮的。
1、互斥
在分布式高並發的條件下,我們最需要保證,同一時刻只能有一個線程獲得鎖,這是最基本的一點。
2、防死鎖
在分布式高並發的條件下,比如有個線程獲得鎖的同時,還沒有來得及去釋放鎖,就因為系統故障或者其它原因使它無法執行釋放鎖的命令,導致其它線程都無法獲得鎖,造成死鎖。所以分布式非常有必要設置鎖的有效時間
,確保系統出現故障后,在一定時間內能夠主動去釋放鎖,避免造成死鎖的情況。
3、性能
高並發分布式系統中,線程互斥等待會成為性能瓶頸,需要好的中間件和實現來保證性能。
4、重入
我們知道ReentrantLock是可重入鎖,那它的特點就是:同一個線程可以重復拿到同一個資源的鎖。重入鎖非常有利於資源的高效利用。關於這點之后會做演示。針對以上Redisson都能很好的滿足,下面就來分析下它。
基於Redisson的實現
為了更好的理解分布式鎖的原理,我這邊自己畫張圖通過這張圖來分析。
加鎖機制:
線程去獲取鎖,獲取成功: 執行lua腳本,保存數據到redis數據庫。
線程去獲取鎖,獲取失敗: 一直通過while循環嘗試獲取鎖,獲取成功后,執行lua腳本,保存數據到redis數據庫。
watch dog看門狗:
在一個分布式環境下,假如一個線程獲得鎖后,突然服務器宕機了,那么這個時候在一定時間后這個鎖會自動釋放,你也可以設置鎖的有效時間(不設置默認30秒),這樣的目的主要是防止死鎖的發生。但在實際開發中會有下面一種情況:
//設置鎖1秒過去 redissonLock.lock("redisson", 1); /** * 業務邏輯需要咨詢2秒 */ redissonLock.release("redisson"); /** * 線程1 進來獲得鎖后,線程一切正常並沒有宕機,但它的業務邏輯需要執行2秒,這就會有個問題,在 線程1 執行1秒后,這個鎖就自動過期了, * 那么這個時候 線程2 進來了。那么就存在 線程1和線程2 同時在這段業務邏輯里執行代碼,這當然是不合理的。 * 而且如果是這種情況,那么在解鎖時系統會拋異常,因為解鎖和加鎖已經不是同一線程了,具體后面代碼演示。 */
所以這個時候看門狗
就出現了,它的作用就是 線程1 業務還沒有執行完,時間就過了,線程1 還想持有鎖的話,就會啟動一個watch dog后台線程,不斷的延長鎖key的生存時間。注意
正常這個看門狗線程是不啟動的,還有就是這個看門狗啟動后對整體性能也會有一定影響,所以不建議開啟看門狗。
代碼實現
首先在pom里依賴Redisson
<!-- redisson --> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>${org-redisson.version}</version> </dependency>
在公共模塊配置Redisson
import org.redisson.Redisson; import org.redisson.config.Config; import org.redisson.config.SingleServerConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.util.ResourceBundle; /** * <p>實例Redisson類</p> * * @author lft * @version 1.0 * @date 2019/6/28 0028 * @since jdk1.8 */ @Component public class RedissonManager { private static Logger logger = LoggerFactory.getLogger(RedissonManager.class); private static String REDIS_IP; private static String REDIS_PORT; private static String REDIS_PASSWD; private static String PROJECT_ENV; @Value("${spring.profiles.active}") public void setProjectEnv(String projectEnv) { PROJECT_ENV = projectEnv; } private static Config config = new Config(); private static Redisson redisson = null; private static void init() { ResourceBundle properties = ResourceBundle.getBundle("application-"+PROJECT_ENV); REDIS_IP = properties.getString("spring.redis.host").trim(); REDIS_PORT = properties.getString("spring.redis.port").trim(); if(properties.containsKey("spring.redis.password")){ REDIS_PASSWD = properties.getString("spring.redis.password"); } SingleServerConfig singleServerConfig = config.useSingleServer(); singleServerConfig.setAddress("redis://"+REDIS_IP+":"+REDIS_PORT); if(null != REDIS_PASSWD && !"".equals(REDIS_PASSWD)){ singleServerConfig.setPassword(REDIS_PASSWD); } //得到redisson對象 redisson = (Redisson) Redisson.create(config); } //實例化redisson public static Redisson getInstance(){ init(); return redisson; } }
加鎖和釋放鎖的方法
import org.redisson.Redisson; import org.redisson.api.RLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.TimeUnit; /** * <p>Redisson的使用</p> * * @author lft * @version 1.0 * @date 2019/6/28 0028 * @since jdk1.8 */ public class DistributedRedisLock { private static Logger logger = LoggerFactory.getLogger(DistributedRedisLock.class); private static Redisson redisson = RedissonManager.getInstance(); private static final String LOCK_TITLE = "redis_lock_"; //加鎖 public static boolean lockup(String lockName){ //聲明key對象 String key = LOCK_TITLE + lockName; //獲取鎖對象 RLock mylock = redisson.getLock(key); //設置過期時間,防止死鎖 mylock.lock(60, TimeUnit.SECONDS); logger.info("======lock======"+Thread.currentThread().getName()); return true; } //鎖的釋放 public static void release(String lockName){ String key = LOCK_TITLE + lockName; RLock mylock = redisson.getLock(key); //釋放鎖(解鎖) mylock.unlock(); logger.info("======unlock======"+Thread.currentThread().getName()); } }
業務邏輯使用鎖
/** * 測試Redis鎖 * @param request * @return * @throws Exception */ @PostMapping("/lockTest") public Object lockTest(HttpServletRequest request) throws Exception{ String key = "test"; //加鎖 DistributedRedisLock.lockup(key); //TODO 業務代碼 LOGGER.info("=========================執行測試任務"); //釋放鎖 DistributedRedisLock.release(key); return ResultUtils.success("鎖測試結束!"); }
zookeeper分布式鎖參考:https://cloud.tencent.com/developer/article/1462302