單臺機器所能承載的量是有限的,用戶的量級上萬,基本上服務都會作分佈式集羣部署。不少時候,會遇到對同一資源的方法。這時候就須要鎖,若是是單機版的,能夠利用java等語言自帶的併發同步處理。若是是多臺機器部署就得要有個中間代理人來作分佈式鎖了。html
經常使用的分佈式鎖的實現有三種方式。java
目前,我已是用了redis和mysql實現了鎖,而且根據應用場景應用在不一樣的線上環境中。zk實現比較複雜,又無應用場景,有興趣的能夠參考他山之石中的《Zookeeper實現分佈式鎖》。mysql
說說心得和體會。redis
沒有什麼完美的技術、沒有萬能鑰匙、不一樣方式不一樣應用場景 CAP原理:一致性(consistency)、可用性(availability)、分區可容忍性(partition-tolerance)三者取其二。算法
基於redis的鎖實現比較簡單,因爲redis的執行是單線程執行,自然的具有原子性操做,咱們能夠利用命令setnx和expire來實現,java版代碼參考以下:sql
package com.fenqile.creditcard.appgatewaysale.provider.util;
import com.fenqile.redis.JedisProxy;
import java.util.Date;
/** * User: Rudy Tan * Date: 2017/11/20 * * redis 相關操做 */
public class RedisUtil {
/** * 獲取分佈式鎖 * * @param key string 緩存key * @param expireTime int 過時時間,單位秒 * @return boolean true-搶到鎖,false-沒有搶到鎖 */
public static boolean getDistributedLockSetTime(String key, Integer expireTime) {
try {
// 移除已經失效的鎖
String temp = JedisProxy.getMasterInstance().get(key);
Long currentTime = (new Date()).getTime();
if (null != temp && Long.valueOf(temp) < currentTime) {
JedisProxy.getMasterInstance().del(key);
}
// 鎖競爭
Long nextTime = currentTime + Long.valueOf(expireTime) * 1000;
Long result = JedisProxy.getMasterInstance().setnx(key, String.valueOf(nextTime));
if (result == 1) {
JedisProxy.getMasterInstance().expire(key, expireTime);
return true;
}
} catch (Exception ignored) {
}
return false;
}
}
複製代碼
包名和獲取redis操做對象換成本身的就行了。數據庫
基本步驟是緩存
步驟2爲最核心的東西, 爲啥設置步驟3?可能應爲獲取到鎖的線程出現什麼移除請求,而沒法釋放鎖,所以設置一個最長鎖時間,避免死鎖。 爲啥設置步驟1?redis可能在設置expire的時候掛掉。設置過時時間不成功,而出現鎖永久生效。併發
線上環境,步驟一、3的問題都出現過。因此要作保底攔截。app
一般redis都是以master-slave解決單點問題,多個master-slave組成大集羣,而後經過一致性哈希算法將不一樣的key路由到不一樣master-slave節點上。
優勢:redis自己是內存操做、而且一般是多片部署,所以有這較高的併發控制,能夠抗住大量的請求。 缺點:redis自己是緩存,有必定機率出現數據不一致請求。
在線上,以前,利用redis作庫存計數器,獎品發放理論上只發放10個的,最後發放了14個。出現了數據的一致性問題。
所以在這以後,引入了mysql數據庫分佈式鎖。
在此以前,在網上搜索了大量的文章,基本上都是 插入、刪除發的方式或是直接經過"select for update"這種形式獲取鎖、計數器。具體能夠參考他山之石中的《分佈式鎖的幾種實現方式~》關於數據庫鎖章節。
一開始,個人實現方式僞代碼以下:
public boolean getLock(String key){
select for update if (記錄存在){
update
}else {
insert
}
}
複製代碼
這樣實現出現了很嚴重的死鎖問題,具體緣由能夠能夠參考他山之石中的《select for update引起死鎖分析》 這個版本中存在以下幾個比較嚴重的問題:
1.一般線上數據是不容許作物理刪除的 2.經過惟一鍵重複報錯,處理錯誤形式是不太合理的。 3.若是appclient在處理中還沒釋放鎖以前就掛掉了,會出現鎖一直存在,出現死鎖。 4.若是以這種方式,實現redis中的計數器(incr decr),當記錄不存在的時候,會出現大量死鎖的狀況。
所以考慮引入,記錄狀態字段、中央鎖概念。
在第二版中完善了數據庫表設計,參考以下:
-- 鎖表,單庫單表
CREATE TABLE IF NOT EXISTS credit_card_user_tag_db.t_tag_lock (
-- 記錄index
Findex INT NOT NULL AUTO_INCREMENT COMMENT '自增索引id',
-- 鎖信息(key、計數器、過時時間、記錄描述)
Flock_name VARCHAR(128) DEFAULT '' NOT NULL COMMENT '鎖名key值',
Fcount INT NOT NULL DEFAULT 0 COMMENT '計數器',
Fdeadline DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '鎖過時時間',
Fdesc VARCHAR(255) DEFAULT '' NOT NULL COMMENT '值/描述',
-- 記錄狀態及相關事件
Fcreate_time DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '建立時間',
Fmodify_time DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '修改時間',
Fstatus TINYINT NOT NULL DEFAULT 1 COMMENT '記錄狀態,0:無效,1:有效',
-- 主鍵(PS:總索引數不能超過5)
PRIMARY KEY (Findex),
-- 惟一約束
UNIQUE KEY uniq_Flock_name(Flock_name),
-- 普通索引
KEY idx_Fmodify_time(Fmodify_time)
)ENGINE=INNODB DEFAULT CHARSET=UTF8 COMMENT '信用卡|鎖與計數器表|rudytan|20180412';
複製代碼
在這個版本中,考慮到再條鎖併發插入存在死鎖(間隙鎖爭搶)狀況,引入中央鎖概念。
基本方式是:
考慮到不一樣公司引入的數據庫操做包不一樣,所以提供僞代碼,以便於理解 僞代碼
// 開啓事務
@Transactional
public boolean getLock(String key){
// 獲取中央鎖
select * from tbl where Flock_name="center_lock"
// 查詢key相關記錄
select for update if (記錄存在){
update
}else {
insert
}
}
複製代碼
/** * 初始化記錄,若是有記錄update,若是沒有記錄insert */
private LockRecord initLockRecord(String key){
// 查詢記錄是否存在
LockRecord lockRecord = lockMapper.queryRecord(key);
if (null == lockRecord) {
// 記錄不存在,建立
lockRecord = new LockRecord();
lockRecord.setLockName(key);
lockRecord.setCount(0);
lockRecord.setDesc("");
lockRecord.setDeadline(new Date(0));
lockRecord.setStatus(1);
lockMapper.insertRecord(lockRecord);
}
return lockRecord;
}
/** * 獲取鎖,代碼片斷 */
@Override
@Transactional
public GetLockResponse getLock(GetLockRequest request) {
// 檢測參數
if(StringUtils.isEmpty(request.lockName)) {
ResultUtil.throwBusinessException(CreditCardErrorCode.PARAM_INVALID);
}
// 兼容參數初始化
request.expireTime = null==request.expireTime? 31536000: request.expireTime;
request.desc = Strings.isNullOrEmpty(request.desc)?"":request.desc;
Long nowTime = new Date().getTime();
GetLockResponse response = new GetLockResponse();
response.lock = 0;
// 獲取中央鎖,初始化記錄
lockMapper.queryRecordForUpdate("center_lock");
LockRecord lockRecord = initLockRecord(request.lockName);
// 未釋放鎖或未過時,獲取失敗
if (lockRecord.getStatus() == 1
&& lockRecord.getDeadline().getTime() > nowTime){
return response;
}
// 獲取鎖
Date deadline = new Date(nowTime + request.expireTime*1000);
int num = lockMapper.updateRecord(request.lockName, deadline, 0, request.desc, 1);
response.lock = 1;
return response;
}
複製代碼
到此,該方案,可以知足個人分佈式鎖的需求。
可是該方案,有一個比較致命的問題,就是全部記錄共享一個鎖,併發並不高。
通過測試,開啓50*100個線程併發修改,5次耗時平均爲8秒。
因爲方案二,存在共享同一把中央鎖,併發不高的請求。參考concurrentHashMap實現原理,引入分段鎖概念,下降鎖粒度。
基本方式是:
僞代碼以下:
// 開啓事務
@Transactional
public boolean getLock(String key){
// 獲取中央鎖
select * from tbl where Flock_name="center_lock"
// 查詢key相關記錄
select for update if (記錄存在){
update
}else {
insert
}
}
複製代碼
/** * 獲取中央鎖Key */
private boolean getCenterLock(String key){
String prefix = "center_lock_";
Long hash = SecurityUtil.crc32(key);
if (null == hash){
return false;
}
//取crc32中的最後兩位值
Integer len = hash.toString().length();
String slot = hash.toString().substring(len-2);
String centerLockKey = prefix + slot;
lockMapper.queryRecordForUpdate(centerLockKey);
return true;
}
/** * 獲取鎖 */
@Override
@Transactional
public GetLockResponse getLock(GetLockRequest request) {
// 檢測參數
if(StringUtils.isEmpty(request.lockName)) {
ResultUtil.throwBusinessException(CreditCardErrorCode.PARAM_INVALID);
}
// 兼容參數初始化
request.expireTime = null==request.expireTime? 31536000: request.expireTime;
request.desc = Strings.isNullOrEmpty(request.desc)?"":request.desc;
Long nowTime = new Date().getTime();
GetLockResponse response = new GetLockResponse();
response.lock = 0;
// 獲取中央鎖,初始化記錄
getCenterLock(request.lockName);
LockRecord lockRecord = initLockRecord(request.lockName);
// 未釋放鎖或未過時,獲取失敗
if (lockRecord.getStatus() == 1
&& lockRecord.getDeadline().getTime() > nowTime){
return response;
}
// 獲取鎖
Date deadline = new Date(nowTime + request.expireTime*1000);
int num = lockMapper.updateRecord(request.lockName, deadline, 0, request.desc, 1);
response.lock = 1;
return response;
}
複製代碼
通過測試,開啓50*100個線程併發修改,5次耗時平均爲5秒。相較於版本二幾乎有一倍的提高。
至此,完成redis/mysql分佈式鎖、計數器的實現與應用。
根據不一樣應用場景,作出以下選擇:
表數據和記錄:
歡迎關注個人簡書博客,一塊兒成長,一塊兒進步。