高併發核心技術 - 冪等性 與 分佈式鎖

高併發核心技術之 - 冪等性

1. 什麼是冪等性

冪等性就是指:一個冪等操做任其執行屢次所產生的影響均與一次執行的影響相同。 用數學的概念表達是這樣的: f(f(x)) = f(x). 就像 nx1 = n 同樣, x1 就是一個冪等操做。不管是乘以多少次結果都同樣。redis

2. 常見的冪等性問題

冪等性問題常常會是由網絡問題引發的,還有重複操做引發的。算法

場景一:好比點贊功能,一個用戶只能對同一片文章點贊一次,重複點贊提示已經點過讚了。

示例代碼:服務器

public void like(Article article,User user) {
	//檢查是否點過贊
    if (checkIsLike(article,user)) {
	//點過讚了
    throw new ApiException(CodeEnums.SYSTEM_ERR);
}
else {
	//保存點贊
    saveLike(article,user);
}
}
</pre>

看上去好像沒有什麼問題,保存點贊以前已經檢查過是否點讚了,理論上同一我的不會對同一篇文章重複點贊。但實際不是這樣的。由於網絡請求不是排隊進來的,而是一窩蜂涌進來的。微信

某些時候,用戶網絡很差,可能很短的時間內點擊了屢次,因爲網絡傳輸問題,這些請求可能會同時來到咱們的服務器。網絡

  • 第一個請求 checkIsLike() 返回 false , 正在執行 saveLike() 操做,還沒來的及提交事務
  • 第二個請求過來了 ,checkIsLike() 返回 也是 false , 並去 執行了 saveLike() 操做

這樣子,就形成了一個用戶同時對一篇文章進行了屢次點贊操做。併發

這就是典型的冪等性問題, 操做了一次和操做了兩次結果不同,由於你多點了一次贊,按照冪等性原則 無論你點擊了多少次結果都同樣,只點了一次贊。分佈式

不少場景都是這樣形成的,好比用戶重複下單,重複評論,重複提交表單等。高併發

那怎麼解決呢? 假設網絡的請求是排隊進來的就不會出現這個問題了。性能

因而咱們能夠改爲這樣:設計

public synchronized void like(Article article,User user) {
	//檢查是否點過贊
if (checkIsLike(article,user)) {
	//點過讚了
throw new ApiException(CodeEnums.SYSTEM_ERR);
}
else {
	//保存點贊
saveLike(article,user);
}
}
</pre>

synchronized 同步鎖 這樣咱們的請求就會乖乖的排隊進來了。

PS :這樣作是效率比較低的作法,不建議這麼作,只是舉例子,synchronized 也不適合分佈式集羣場景。

場景二 : 第三方回調

咱們系統常常須要和第三方系統打交道,好比微信充值,支付寶充值什麼的,微信和支付寶經常會以回調你的接口通知你支付結果。爲了保證你能收到回調,每每可能會回調屢次。

有時候咱們也爲了保證數據的準確性會有個定時器去查詢支付結果未知的流水,並執行響應的處理。 若是定時器的輪訓和回調恰好是在同時進行,這可能又出BUG了,又進行了兩次重複操做。

那麼問題來了: 假設我是一個充值操做, 回調回來的時候 ,會作業務處理,成功了給用戶帳戶加錢。這是後就要保證冪等性了, 假設微信同一筆交易給你回調了兩次,若是你給用戶充值了兩次,這顯然不合理(我是老闆確定扣你工資),因此要保證 無論微信回調你多少次 ,同一筆交易你只能給用戶充一次錢。這就冪等性。

解決冪等性問題方案

  • synchronized 適合單機應用,不追求性能 ,不追求併發。
  • 分佈式鎖 可是每每咱們的應用是分佈式的集羣,而且很講究性能,併發,因此咱們須要用到 分佈式鎖 來解決這個問題。

Redis 分佈式鎖:

/**
* setNx
*
*  @param key
*  @param value
*  @return
*/
public Boolean setNx(String key,Object value) {
	return redisTemplate.opsForValue().setIfAbsent(key,value);
}
/**
*  @param key 鎖
*  @param waitTime 等待時間  毫秒
*  @param expireTime 超時時間  毫秒
*  @return
*/
public Boolean lock(String key,Long waitTime,Long expireTime) {
	String vlaue =  UUIDUtil.mongoObjectId();
	Boolean flag = setNx(key,vlaue);
	//嘗試獲取鎖  成功返回
if (flag) {
	redisTemplate.expire(key,expireTime,TimeUnit.MILLISECONDS);
	return flag;
}
else {
	//失敗
//如今時間
long newTime =  System.currentTimeMillis();
	//等待過時時間
long loseTime = newTime + waitTime;
	//不斷嘗試獲取鎖成功返回
while (System.currentTimeMillis()  < loseTime) {
	Boolean testFlag = setNx(key,vlaue);
	if (testFlag) {
	redisTemplate.expire(key,expireTime,TimeUnit.MILLISECONDS);
	return testFlag;
}
//休眠100毫秒
try {
	Thread.sleep(100);
}
catch (InterruptedException e) {
	e.printStackTrace();
}
}}return false;}/**
*  @param key
*  @return
*/
public Boolean lock(String key) {
	return lock(key,1000L,60  *  1000L);
}
/**
*  @param key
*/
public void unLock(String key) {
	remove(key);
}
</pre>

利用Redis 分佈式鎖 咱們的代碼能夠改爲這樣:

public void like(Article article,User user) {
	String key =  "key:like"  + article.getId()  +  ":"  + user.getUserId();
	//  等待鎖的時間  0  ,  過時時間  一分鐘防止死鎖
boolean flag = redisService.lock(key,0,60  *  1000L);
	if(!flag) {
	//獲取鎖失敗  說明前面的請求已經獲取了鎖
throw new ApiException(CodeEnums.SYSTEM_ERR);
}
//檢查是否點過贊
if (checkIsLike(article,user)) {
	//點過讚了
throw new ApiException(CodeEnums.SYSTEM_ERR);
}
else {
	//保存點贊
saveLike(article,user);
}
//刪除鎖
redisService.unLock(key);
}
</pre>

key 的設計也很講究: 數據不衝突的兩個業務場景,key不能衝突,不一樣人的key也不同,不一樣的文章Key也不同。 根據場景業務設定。

一個原則: 儘量的縮小key的範圍。 這樣才能加強咱們的併發。

首先咱們先獲取鎖,獲取鎖成功 執行完操做,保存數據 ,刪除鎖。獲取不到鎖返回失敗。設置過時時間是爲了防止‘死鎖’,好比機器獲取到了 鎖,沒有設置過時時間,可是他死機了,沒有刪除釋放鎖。

  • 版本號控制 CAS 算法: CAS有3個操做數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然什麼都不作。這個比較繁雜,有興趣的同窗能夠去看看。
相關文章
相關標籤/搜索