使用ELock實現高性能分佈式鎖(非輪詢)

前言: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介紹

     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>
  1. 初始化JedisPool       

//直接傳入鏈接池初始化(注:無密碼請傳null)
new ELockCache().initJedisPool(JediPool);
//傳入ip、端口、密碼、超時時間初始化
new ELockCache().initJedisPool(host, port, secretKey, timeOut);
//傳入ip、端口、密碼、超時時間、配置器初始化
new ELockCache().initJedisPool(host, port, secretKey, timeOut, jedisPoolConfig);
  1. 加鎖

ELocker.lock(key, expireSecond);
  1. 釋放鎖

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

相關文章
相關標籤/搜索