架構簡述
mykit架構中獨立出來的mykit-lock元件,旨在提供高并發架構下分布式系統的分布式鎖架構。
分布式鎖是控制分布式系統之間同步通路共享資源的一種方式。在分布式系統中,常常需要協調他們的動作。如果不同的系統或是同一個系統的不同主機之間共享了一個或一組資源,那麼通路這些資源的時候,往往需要互斥來防止彼此幹擾來保證一緻性,在這種情況下,便需要使用到分布式鎖。
架構結構描述
對高并發下的分布式系統通路共享資源提供分布式鎖操作,使用者隻需要加入簡單的注解便可輕松對共享資源加分布式鎖。
目前主要以Redis的形式實作分布式鎖操作,後續擴充其他方式
- mykit-lock-redis
mykit-lock架構下以Redis方式實作分布式鎖
- mykit-lock-redis-core
mykit-lock-redis 架構下的核心子產品,主要提供通用的注解、自定義異常類和工具類
- mykit-lock-redis-single
mykit-lock-redis 架構下主要以Redis單節點形式實作分布式鎖
- mykit-lock-redis-cluster
mykit-lock-redis 架構下主要以Redis叢集形式實作分布式鎖,待完善
- mykit-lock-test
mykit-lock的測試子產品
- mykit-lock-test-redis-single
主要測試以Redis單節點形式實作分布式鎖
測試的入口為:io.mykit.lock.test.redis.single
使用說明
1、引用mykit-lock-redis-single說明
1)在pom.xml中添加如下配置:
<dependency>
<groupId>io.mykit.lock</groupId>
<artifactId>mykit-lock-redis-single</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
2)在項目的classpath/properties目錄下配置redis-lock.properties
在項目的classpath/properties目錄下建立redis-lock.properties(注意:配置檔案的名稱必須為redis-lock.properties),檔案中的配置項的Key必須包含以下内容:
redis.maxIdle=100
redis.minIdle=1
redis.maxTotal=1000
redis.host=127.0.0.1
redis.port=6379
3)定義需要分布式鎖支援的接口
package io.mykit.lock.test.redis.single.service;
import io.mykit.lock.redis.annotation.CacheLock;
import io.mykit.lock.redis.annotation.LockedObject;
/**
* @author binghe
* @version 1.0.0
* @description 商品秒殺Service
*/
public interface SeckillService {
@CacheLock(lockedPrefix="TEST_PREFIX")
public void secKill(String arg1,@LockedObject Long arg2);
}
4)實作需要分布式鎖支援接口的類
package io.mykit.lock.test.redis.single.service.impl;
import io.mykit.lock.test.redis.single.service.SeckillService;
import java.util.HashMap;
import java.util.Map;
/**
* @author binghe
* @version 1.0.0
* @description 商品秒殺實作
*/
public class SeckillServiceImpl implements SeckillService {
public static Map<Long, Long> inventory ;
static{
inventory = new HashMap<>();
inventory.put(10000001L, 10000l);
inventory.put(10000002L, 10000l);
}
@Override
public void secKill(String arg1, Long arg2) {
reduceInventory(arg2);
}
//模拟秒殺操作,姑且認為一個秒殺就是将庫存減一,實際情景要複雜的多
public Long reduceInventory(Long commodityId){
inventory.put(commodityId,inventory.get(commodityId) - 1);
return inventory.get(commodityId);
}
}
分布式鎖概念補充
業務場景
所謂秒殺,從業務角度看,是短時間内多個使用者“争搶”資源,這裡的資源在大部分秒殺場景裡是商品;将業務抽象,技術角度看,秒殺就是多個線程對資源進行操作,是以實作秒殺,就必須控制線程對資源的争搶,既要保證高效并發,也要保證操作的正确。一些可能的實作
剛才提到過,實作秒殺的關鍵點是控制線程對資源的争搶,根據基本的線程知識,可以不加思索的想到下面的一些方法:
1、秒殺在技術層面的抽象應該就是一個方法,在這個方法裡可能的操作是将商品庫存-1,将商品加入使用者的購物車等等,在不考慮緩存的情況下應該是要操作資料庫的。那麼最簡單直接的實作就是在這個方法上加上synchronized關鍵字,通俗的講就是鎖住整個方法;
2、鎖住整個方法這個政策簡單友善,但是似乎有點粗暴。可以稍微優化一下,隻鎖住秒殺的代碼塊,比如寫資料庫的部分;
3、既然有并發問題,那我就讓他“不并發”,将所有的線程用一個隊列管理起來,使之變成串行操作,自然不會有并發問題。
上面所述的方法都是有效的,但是都不好。為什麼?第一和第二種方法本質上是“加鎖”,但是鎖粒度依然比較高。什麼意思?試想一下,如果兩個線程同時執行秒殺方法,這兩個線程操作的是不同的商品,從業務上講應該是可以同時進行的,但是如果采用第一二種方法,這兩個線程也會去争搶同一個鎖,這其實是不必要的。第三種方法也沒有解決上面說的問題。那麼如何将鎖控制在更細的粒度上呢?可以考慮為每個商品設定一個互斥鎖,以和商品ID相關的字元串為唯一辨別,這樣就可以做到隻有争搶同一件商品的線程互斥,不會導緻所有的線程互斥。分布式鎖恰好可以幫助我們解決這個問題。
何為分布式鎖
我們來假設一個最簡單的秒殺場景:資料庫裡有一張表,column分别是商品ID,和商品ID對應的庫存量,秒殺成功就将此商品庫存量-1。現在假設有1000個線程來秒殺兩件商品,500個線程秒殺第一個商品,500個線程秒殺第二個商品。我們來根據這個簡單的業務場景來解釋一下分布式鎖。
通常具有秒殺場景的業務系統都比較複雜,承載的業務量非常巨大,并發量也很高。這樣的系統往往采用分布式的架構來均衡負載。那麼這1000個并發就會是從不同的地方過來,商品庫存就是共享的資源,也是這1000個并發争搶的資源,這個時候我們需要将并發互斥管理起來。這就是分布式鎖的應用。
而key-value存儲系統,如redis,因為其一些特性,是實作分布式鎖的重要工具。
具體的實作
先來看看一些redis的基本指令:
- SETNX key value
如果key不存在,就設定key對應字元串value。在這種情況下,該指令和SET一樣。當key已經存在時,就不做任何操作。SETNX是”SET if Not eXists”。
- expire KEY seconds
設定key的過期時間。如果key已過期,将會被自動删除。
- del KEY
删除key
需要考慮的問題
1、用什麼操作redis?幸虧redis已經提供了jedis用戶端用于java應用程式,直接調用jedis API即可。
2、怎麼實作加鎖?“鎖”其實是一個抽象的概念,将這個抽象概念變為具體的東西,就是一個存儲在redis裡的key-value對,key是與商品ID相關的字元串來唯一辨別,value其實并不重要,因為隻要這個唯一的key-value存在,就表示這個商品已經上鎖。
3、如何釋放鎖?既然key-value對存在就表示上鎖,那麼釋放鎖就自然是在redis裡删除key-value對。
4、阻塞還是非阻塞?筆者采用了阻塞式的實作,若線程發現已經上鎖,會在特定時間内輪詢鎖。
5、如何處理異常情況?比如一個線程把一個商品上了鎖,但是由于各種原因,沒有完成操作(在上面的業務場景裡就是沒有将庫存-1寫入資料庫),自然沒有釋放鎖,這個情況筆者加入了鎖逾時機制,利用redis的expire指令為key設定逾時時長,過了逾時時間redis就會将這個key自動删除,即強制釋放鎖(可以認為逾時釋放鎖是一個異步操作,由redis完成,應用程式隻需要根據系統特點設定逾時時間即可)