高級JAVA開發 分佈式系統
分佈式系統
參考和摘自:
中華石杉 《Java工程師面試突擊第1季》
分佈式系統接口的冪等性
思路:緩存記錄標識(類似分佈式鎖,要注意保證操作的原子性) 或 基於數據庫unique key。
分佈式鎖
Redis 普通實現
# 加鎖用Set附帶NX(IF NOT EXIST)、PX(WITH EXPIRE TIME)參數保證一個命令的原子性:
SET lockKey randomValue NX PX 10000
# 解鎖用Lua腳本保證原子性,判斷randomValue是否爲加鎖時放進去的
# 只有持有鎖的人才能釋放鎖,
# 鎖過期後被另一進程獲取,原進程嘗試釋放鎖randomValue不等,操作失敗:
if redis.call("get",KEYS[1]) == ARGV[1]
then return redis.call("del",KEYS[1])
else return 0
end
缺點:
1.Redis單機部署,Redis掛掉後鎖失效。
2.Redis高可用主從同步時是異步的,從庫得到“鎖”數據會有延時。
如果slave還沒來得及同步完鎖數據master掛掉了,鎖就失效了。
基於Redis的分佈式鎖框架:Redisson、RedLock
待補充
基於zookeeper的分佈式鎖
實現思想:
基於EPHEMERAL臨時節點的普通鎖:
A線程嘗試在zk上創建一個臨時節點,如果臨時節點不存在,創建成功,拿到鎖。釋放鎖就刪除臨時節點。
B線程嘗試在zk上創建一個相同名稱的臨時節點,如果節點已經存在了,創建失敗,沒拿到鎖,並在臨時節點上註冊監聽器。一旦臨時節點被刪除,監聽器觸發,重複嘗試創建臨時節點。
基於EPHEMERAL_SEQUENTIAL有序臨時節點的公平鎖:
線程創建有序臨時節點(會在zk目錄下創建一個帶編號的臨時節點)並且取回節點集,對節點集排序拿出編號最小節點,如果最小節點是當前線程創建的節點,那麼當前線程持有鎖並返回true,反之獲得鎖失敗。然後取得當前臨時節點的前一個節點添加監聽,然後阻塞,等待前者釋放鎖通知監聽喚醒。這樣根據創建節點順序依次執行。
分佈式系統Session共享
1.Tomcat可以直接配置基於Redis的Session共享:
# 單機Redis:
<Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" />
<Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager"
host="{redis.host}"
port="{redis.port}"
database="{redis.dbnum}"
maxInactiveInterval="60"/>
# Redis:
<Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" />
<Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager"
sentinelMaster="mymaster"
sentinels="<sentinel1-ip>:26379,<sentinel2-ip>:26379,<sentinel3-ip>:26379"
maxInactiveInterval="60"/>
缺點:依賴於web容器,更換web容器還需要另外方案。
2.spring session + redis
# pom.xml配置:
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>1.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.8.1</version>
</dependency>
# spring配置:
<bean id="redisHttpSessionConfiguration" class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
<property name="maxInactiveIntervalInSeconds" value="600"/>
</bean>
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxTotal" value="100" />
<property name="maxIdle" value="10" />
</bean>
<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy">
<property name="hostName" value="${redis_hostname}"/>
<property name="port" value="${redis_port}"/>
<property name="password" value="${redis_pwd}" />
<property name="timeout" value="3000"/>
<property name="usePool" value="true"/>
<property name="poolConfig" ref="jedisPoolConfig"/>
</bean>
# web.xml配置:
<filter>
<filter-name>springSessionRepositoryFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSessionRepositoryFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
# 示例代碼
@Controller
@RequestMapping("/test")
public class TestController {
@RequestMapping("/putIntoSession")
@ResponseBody
public String putIntoSession(HttpServletRequest request, String username){
request.getSession().setAttribute("name", “leo”);
return "ok";
}
@RequestMapping("/getFromSession")
@ResponseBody
public String getFromSession(HttpServletRequest request, Model model){
String name = request.getSession().getAttribute("name");
return name;
}
}
分佈式事務
參考和摘自:
中華石杉 《Java工程師面試突擊第1季》
第一次有人把“分佈式事務”講的這麼簡單明瞭
常用的分佈式事務解決方案
本地事務四大特性 ACID:
- A:原子性(Atomicity),一個事務(transaction)中的所有操作,要麼全部完成,要麼全部不完成,不會結束在中間某個環節。
- C:一致性(Consistency),事務的一致性指的是在一個事務執行之前和執行之後數據庫都必須處於一致性狀態。
- I:隔離性(Isolation),指的是在併發環境中,當不同的事務同時操縱相同的數據時,每個事務都有各自的完整數據空間。
- D:持久性(Durability),指的是隻要事務成功結束,它對數據庫所做的更新就必須永久保存下來。
分佈式事務並不完全嚴格遵循本地事務四大原則,在一定點上做些取捨。
兩階段提交/XA協議
在 XA 協議中分爲兩階段:
- 事務管理器要求每個涉及到事務的數據庫預提交(precommit)此操作,並反映是否可以提交。
- 事務協調器要求每個數據庫提交數據,或者回滾數據。
也就是說:第一階段先判斷一下業務邏輯能否符合要求,執行sql先不提交,如果有的服務說我這裏不行,執行不了,就返回給事務管理器,事務管理器告訴大家全都回滾吧。如果所有服務都執行成功,事務管理器再告訴大家都提交吧。在事務管理器協調過程中大家都是是阻塞的。
Spring + JTA 可以實現。 參考: Spring的全局事務JTA
缺點:
- 單點問題:第一階段已經完成,在第二階段正準備提交的時候事務管理器宕機,資源管理器就會一直阻塞,導致數據庫無法使用。
- 同步阻塞:在準備就緒之後,資源管理器中的資源一直處於阻塞,直到提交完成,釋放資源。
- 數據不一致:第二階段部分參與者收到並執行了Commit操作,其餘參與者未收到通知一直阻塞,此時數據不一致。
XA 協議比較簡單,成本較低,有單點問題,也不支持高併發(同步阻塞)。
三階段提交/TCC機制(Try - Confirm - Cancel)
- Try階段:對各個服務資源做檢測、對資源進行鎖定或者預留。
- Confirm階段:在各個服務中執行實際的操作,並滿足冪等性。
- Cancel階段:如果任何一個服務的業務方法執行出錯,那麼這裏就需要進行補償,執行已經執行成功的業務邏輯的回滾操作。
也就是說:舉例說在執行銀行轉賬業務。Try階段先詢問兩個人的資金賬戶是否滿足轉賬要求(錢夠不夠),如果符合要求就鎖定賬戶(轉出人不能再花錢了,不然不夠轉出了),再加上一個超時時間,接下來的操作如果超時就自動解除鎖定。Confirm階段進行實際的賬戶加減錢操作並直接Commit。Cancel階段,如果有其中一個賬戶操作失敗了(比如檢查鎖定時,發現鎖已經超時解除了,或者數據庫訪問不通),就告訴事務管理器,讓管理器告訴其他服務把之前的操作都回滾吧,把鎖定的資源全部釋放(比如轉出賬戶這時候已經-100元,這時候再給100元加回來)。
優點:強隔離性,嚴格一致性要求的活動業務。執行時間較短的業務。
缺點:實現起來麻煩,執行效率比較低。
本地消息表(最終一致)(最常用)
本地消息表方案是 eBay 提出的:
事務1:
- A系統①操作業務表並②記錄消息處理狀態到消息表,③把消息放入MQ中,事務結束,④在zk上註冊監聽
- 事務2:B系統從MQ取得消息,先⑤插入消息表(唯一鍵保持操作冪等),再⑥操作業務表,事務結束,⑦修改zk的node,觸發監聽通知A系統。
- A系統監聽被觸發,修改A消息表狀態,完成操作。
- 啓動一個定時系統定時查詢A消息表中狀態未完成的操作,重發消息到MQ。B系統收到消息保證消息的冪等性。
特點:保證最終一致性,適合於對一致性要求不高業務。
缺點:如果採用消息表方案,大量依賴消息表,數據庫壓力比較大,對高併發要求欠佳。
可靠消息最終一致方案(基於RocketMQ)
參考: 分佈式開放消息系統(RocketMQ)的原理與實踐
基本流程如下:
- 第一階段 Prepared 消息,會拿到消息的地址。
- 第二階段執行本地事務。
- 第三階段通過第一階段拿到的地址去訪問消息,並修改狀態。消息接受者就能使用這個消息。
如果確認消息失敗,在 RocketMQ Broker 中提供了定時掃描沒有更新狀態的消息。如果有消息沒有得到確認,會向消息發送者發送消息,來判斷是否提交,在 RocketMQ 中是以 Listener 的形式給發送者,用來處理。
如果消費超時,則需要一直重試,消息接收端需要保證冪等。如果消息消費失敗,這時就需要人工進行處理,因爲這個概率較低,如果爲了這種小概率時間而設計這個複雜的流程反而得不償失。
分庫分表
拆分方案
垂直拆分:將一張表垂直拆分成多張表
比如一張表: Id col1 col2 col3 col4 col5 col6
拆成表1:Id col1 col2 col3
表2:Id col4 col5 col6
水平拆分:單表數據量過大,按Id或者createTime拆分成多張表,表結構不變,表名加上序號,分裝到多個數據庫中。利於數據不斷膨脹的表,比如 訂單表、流水錶等。
按照range分表:方便擴容,但是對單庫壓力可能會很大(訪問新數據的頻率比舊數據高)
按照hash分表:不方便擴容,擴容會導致數據遷移,但是可以均攤數據庫壓力。
如何分表需參照具體業務衡量。
全局ID如何生成
- 新建一個全局數據庫專門生成ID,適合併發量低,數據量大的系統
- UUID:uuid太長,作爲主鍵性能太差,不適合用於主鍵。
- 基於當前時間生成:時間+userId+… 保證唯一
- snowflake算法:
0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 | 11001 | 0000 00000000
永遠是0表示整數 | 毫秒時間戳轉換成2進制並填0對齊41位 | 機房ID,最大(2^5)32個機房 | 機器ID,最大(2^5)32個機器 | 當前毫秒生成的Id序號,最大(2 ^ 12 - 1)4096個
最後把拼裝的2進制轉換成10進製得到ID號
Mysql讀寫分離相關問題
主庫宕機數據丟失問題:
semi-sync複製(半同步複製),主庫寫入binlog日誌之後,將強制立即將數據同步到從庫,從庫將日誌寫入自己本地的relay log之後返回一個ack給主庫,主庫接收到至少一個從庫ack後纔會認爲寫操作完成了。
延遲問題:
並行複製,從庫開啓多個線程,並行讀取relay log中不同庫的日誌,然後並行重放不同庫的日誌,這是庫級別的並行。(多庫併發重放)緩解延遲問題。
mysql > show status;
Seconds_Behind_Master,可以看到從庫複製主庫的數據落後了幾ms。
- 拆庫拆表降低單庫併發,提高主從同步效率。
- 打開Mysql並行複製,緩解延遲問題。
- 重寫代碼,儘量避免寫入後立馬讀出來。
- 如果業務需求插入更新數據後立馬讀出來,那麼只能對這個查詢設置直連主庫,但是並不推薦這種做法。