前言:java
隨着筆者的顏值不斷提升,用戶量的日益增加,傳統的單機方案已經不能知足產品的需求。筆者在網上尋遍方案,發現均爲人云亦云,一份以毫秒爲精度的輪詢分佈式鎖被轉發轉載上萬次。然,該方案無法知足筆者性能要求。故此,筆者研發ELock插件,併發布本文章。git
其實集羣也好,分佈式服務也好。當咱們不能保證團隊成員的總體素質,那麼在某些業務上,分佈式鎖天然無法避免。redis
公認開發原則:能不使用分佈式鎖的,儘量不使用express
舉個例子,一個商品交易,須要檢查庫存、檢查餘額、扣庫存、扣款、變動訂單狀態。可能不少人以爲,在分佈式環境下必定要分佈式鎖才能安全。緩存
致此,筆者提供一種簡單的方案:安全
訂單處理{ if(訂單狀態!=待支付){ return 該訂單已處理; } if(庫存不足){ return 庫存不足; } if(餘額不足){ return 餘額不足; } 事務管理(rollbackFor = Exception.class){ //修改訂單狀態 int changeLine = 執行語句( update 訂單表 set status=已支付 where status=待支付 and orderId=訂單號); if(changeLine < 1){ return 該訂單已處理; } //扣庫存 changeLine = 執行語句(update 商品表 set 庫存=庫存-訂單信息.購買數量 where 庫存>訂單信息.購買數量 and 商品ID = ?); if(changeLine != 1){ throw CustomRuntimeException("庫存不足"); } //扣款 changeLine = 執行語句(update 用戶餘額表 set 餘額=餘額-訂單信息.訂單金額 where 餘額 > 訂單信息.訂單金額 and 訂單信息.訂單金額 > 0 and 用戶ID = ?); if(changeLine != 1){ throw CustomRuntimeException("餘額不足"); } } }
咱們仔細來分析一下如上的整個邏輯併發
一、當一個業務進入邏輯體,先檢查訂單狀態、餘額和庫存,不知足條件則返回錯誤(可阻擋非併發狀況下的大部分業務流入事物)分佈式
二、進入事物後,先變動訂單狀態,若是變動失敗,直接返回錯誤ide
三、當訂單狀態變動成功,則扣取庫存,扣取庫存失敗必須拋出異常,讓第二步的訂單狀態回滾。工具
四、扣取庫存後,則進行扣款,當扣款失敗,則拋出異常(因爲在業務體走到這裏,已經扣取了庫存,本處不能return,需拋出異常,讓事物回滾)
特別注意:語句中,經過where來進行餘額不足和庫存不足的條件判斷。經過執行語句返回的影響行數,來判斷是否扣取成功。 在以上流程中,咱們發現,即使不使用分佈式鎖,也無併發問題。
===========================================================
以上介紹了在常見的業務中如何規避分佈式鎖,下面介紹一下筆者的高性能分佈式鎖
友情提示:切勿以爲筆者以上理論是拆本身的臺,筆者做爲互聯網技術人,但願各位技術人可以將產品質量作到最好,少加班,多回家陪陪家人
ELock是筆者閒暇之餘寫的一套分佈式鎖插件,代碼很是精簡、而且以非輪詢阻塞的方式進行加鎖控制。適用於面向用戶的互聯網產品,目前用在一套用戶量爲7位數的直播系統中。 源碼地址:https://gitee.com/coodyer/Coody-Framework/tree/original/coody-elock
Maven引用代碼(可關注更新狀況):
<dependency> <groupId>org.coody.framework</groupId> <artifactId>coody-elock</artifactId> <!--更新於2019-01-22 10:23:00 --> <version>alpha-1.2.4</version> </dependency>
初始化JedisPool
//直接傳入鏈接池初始化(注:無密碼請傳null) new ELockCache().initJedisPool(JediPool); //傳入ip、端口、密碼、超時時間初始化 new ELockCache().initJedisPool(host, port, secretKey, timeOut); //傳入ip、端口、密碼、超時時間、配置器初始化 new ELockCache().initJedisPool(host, port, secretKey, timeOut, jedisPoolConfig);
加鎖
ELocker.lock(key, expireSecond);
釋放鎖
ELocker.unLock(key);
注意: 加鎖代碼(ELocker.lock(key, expireSecond);)。需try{}catch{}包圍,並在finally釋放鎖(ELocker.unLock(key);)
try { ELocker.lock(key, 100); for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getId() + ">>" + i); Thread.sleep(100l); } } catch (InterruptedException e) { e.printStackTrace(); } finally { ELocker.unLock(key); }
6. 測試代碼
import java.util.ArrayList; import java.util.List; import org.coody.framework.elock.ELocker; import org.coody.framework.elock.redis.ELockCache; /** * 分佈式鎖測試 * @author Coody * */ public class ELockTest { //要加鎖的key static String key = "TESTLOCK_1"; static { //初始化jedis鏈接 new ELockCache().initJedisPool("127.0.0.1", 16379, "123456", 10000); } public static void main(String[] args) { List<Thread> threads = new ArrayList<Thread>(); for (int i = 0; i < 10; i++) { Thread thread = new Thread(new Runnable() { [@Override](https://my.oschina.net/u/1162528) public void run() { test(); } }); threads.add(thread); } //啓動十個線程 for (Thread thread : threads) { thread.start(); } } //要鎖的方法 private static void test() { try { ELocker.lock(key, 100); for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getId() + ">>" + i); Thread.sleep(100l); } } catch (InterruptedException e) { e.printStackTrace(); } finally { ELocker.unLock(key); } } }
執行效果:
===========================================================
以上介紹了這套鎖的基本使用,下面開始介紹一下這套鎖在Spring下的花樣玩法
一、配置分佈式鎖中使用的緩存
<bean id="eLockCache" class="org.coody.framework.elock.redis.ELockCache" lazy-init="false"> <property name="jedisPool" ref="jedisPool" /> </bean>
二、配置分佈式鎖切面
<!-- 配置切面的bean --> <bean id="eLockInterceptor" class="org.coody.framework.elock.aspect.ELockAspect"></bean> <!-- 配置AOP --> <aop:config> <!-- 配置切面表達式 --> <aop:pointcut expression="@annotation(org.coody.framework.elock.annotation.ELock)" id="eLockPointcut" /> <!-- 配置切面和通知 order:越小優先級越高 --> <aop:aspect id="logAspect" ref="eLockInterceptor"> <aop:around method="rdLockForAspectj" pointcut-ref="eLockPointcut" /> </aop:aspect> </aop:config>
致此,分佈式鎖配置完成,開始進入咱們的花樣玩法。
NO1. 使用註解添加分佈式鎖:
@ELock(name = "USER_MODIFY_LOCK", fields = "userId", waitTime = 20) public void delUser(String userId) { userDao.delUser(userId); }
在ELock註解中,name表明key名字,field表明拼接的字段。
當fields全部字段長度超過32時,elock將會對key進行md5獲取摘要做爲緩存的key,即name:key。
本處fields支持選擇對象的字段,即:方法參數名.字段值(如:userInfo.userId)
本處fields支持多個字段,fields={"userInfo.userId","orderInfo.orderId"}
當不指定key時,elock將會根據包名、類名、方法名和方法參數生成key
當不指定fields時,elock不會拼接任何多餘參數,則該方法變成全局同步方法
如圖:
NO2. 使用鎖執行器添加分佈式鎖
public void delUser(String userId) throws InterruptedException { String key="USER_MODIFY_LOCK"+userId; Integer code=new AbstractLockAble(key,20) { @Override public Object doService() { return userDao.delUser(userId); } }.invoke(); }
經過 返回值=new AbstractLockAble(鎖名稱,超時時間){}.invoke()的方式,覆蓋doService方法,將須要加鎖的代碼塊放置doService方法裏面執行。
如圖:
===========================================================
以上介紹了經過註解進行加鎖和經過執行器進行加鎖的操做,若是在項目中以爲兩種方式不可取,可採用上文中常規方式。
本處介紹下這套鎖爲什麼高性能。
筆者曾經百度搜索Java分佈式鎖實現,發現所提供方案都一模一樣(因爲沒有做圖工具,就隨便寫下流程)。
一、嘗試得到鎖
二、死循環輪詢得到鎖
三、執行業務
四、釋放鎖
在網上查到的方案,相信不少小朋友都知道,不知道是誰經過這種方式來作分佈式鎖,而後被一大堆網友轉載。
這種方案是能夠實現鎖,可是不適用於對外的互聯網產品。
重大問題地雷:當多個線程嘗試得到鎖,只有一個線程會執行,剩下的線程都在輪詢得到鎖。這裏咱們假設時間精度爲1ms,那就意味着每一個線程每秒鐘最多輪詢1000次。然而在分佈式鎖中,咱們須要藉助中介容器去進行嘗試得到鎖的操做,如redis zookeeper。故此,咱們假設這個key有100個線程,第一個線程執行卡住,那麼,1個線程在執行業務,99個線程在以每秒鐘1000的頻次對中間容器發起ddos攻擊。故此,如上方案不適用於對外的互聯網產品。
介紹下筆者的方案:
一、嘗試得到鎖
二、線程入列並暫停
三、執行業務
四、發送消息釋放鎖,並喚醒下一個線程(輪詢至第1步)
咱們知道,redis也好,zookeeper也好,都有消息訂閱機制。當業務流入的時候,獲取鎖失敗的線程,都進入了掛起的狀態,那麼此時有一個線程在執行。當這個線程執行完畢後,發送消息,這時候全部的應用程序都收到了這個消息,並嘗試得到鎖,以此往復,實現業務體執行權限
做者:Coody
版權:©2014-2020 Test404 All right reserved. 版權全部
反饋郵箱:644556636@qq.com
問題反饋羣:Java泛太平洋研究中心 218481849