天天看點

java叢集唯一編号規則_[Java EE]小結:生成全局唯一編号的思路

并發是一個讓人很頭疼的問題,通常會在服務端或資料庫端做處理,保證在并發下資料的準确性。

為此,簡要讨論一下,如何通過解決全局生成唯一編号的并發問題。

1 MySQL資料庫的鎖

1-0 鎖的分類

按鎖定的資料粒度

表級鎖

頁級鎖

行級鎖

按鎖定的方式

共享鎖

排他鎖

讀鎖 := 共享鎖(shared Lock) / 寫鎖:= 排他鎖(exclusive Lock)

1-1 讀鎖/共享鎖

1-1-1 定義

事務A 使用共享鎖 擷取了某條(或者某些)記錄時:

事務B 可以讀取這些記錄; 可以繼續添加共享鎖,但是不能修改或删除這些記錄;

否則,當事務B 對這些資料修改或删除時,會進入阻塞狀态,直至:鎖等待逾時或者事務A送出

1-1-2 用法

[表級讀鎖]

SET AUTOCOMMIT=0;

LOCK TABLES tb_student READ, tb_student READ, ...; -- 加鎖

[do something with tables tb_student and tb_student , ... here];

COMMIT;

UNLOCK TABLES; -- 解鎖

[行級讀鎖]

SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE;

1-1-3 注意事項

當使用讀鎖時,避免産生如下操作

[事務1]

BEGIN;

select * from sys_user where id = 1 LOCK IN SHARE MODE; (步驟1)

update sys_user set username = "taven" where id = 1; (步驟3,發生阻塞)

COMMIT;

[事務2]

BEGIN;

select * from sys_user where id = 1 LOCK IN SHARE MODE; (步驟2)

update sys_user set username = "taven" where id = 1; (步驟4,死鎖)

COMMIT;

根據我們之前對讀鎖定義可知:

當有事務拿到一個結果集的讀鎖時: 其他事務想要更新該結果集,需要拿到讀鎖的事務送出(釋放鎖)。

而上述情況2個事務分别拿到了讀鎖,且都有update 操作,2個事務互相等待造成死鎖(2個事務都在等待對方釋放讀鎖)

1-1-4 使用場景

1) 讀取結果集的最新版本,同時防止其他事務産生更新該結果集。

主要用在需要資料依存關系時,确認某行記錄是否存在,并確定沒有其它人對這個記錄進行UPDATE或者DELETE操作

1-2 寫鎖/排他鎖

1-2-1 定義

1個寫鎖會阻塞其他的讀鎖和寫鎖。

事務A 對某些記錄添加寫鎖時:

事務B 無法向這些記錄添加寫鎖或讀鎖;(注:未添加鎖的資料,是可以讀取的)

事務B 也無法執行對 鎖住的資料進行update / delete操作

1-2-2 使用場景

讀取結果集的最新版本,同時防止其他事務産生讀取或者更新該結果集。

例如:并發下對商品庫存的操作

1-2-3 注意事項

在使用讀鎖、寫鎖時,都需要注意,讀鎖、寫鎖屬于行級鎖。

即 事務1 對商品A 擷取寫鎖,和事務2 對商品B 擷取寫鎖互相不會阻塞的。

需要我們注意的是:我們的SQL要合理使用索引,當我們的SQL全表掃描的時候,行級鎖會變成表鎖。

使用EXPLAIN檢視 SQL是否使用了索引,掃描了多少行。

1-2-4 用法

SELECT * FROM table_name WHERE ... FOR UPDATE

1-3 樂觀鎖(非資料庫鎖的邏輯鎖) [推薦]

上述介紹的是行級鎖,可以最大程度地支援并發處理(同時也帶來了最大的鎖開銷)

樂觀鎖是一種邏輯鎖,通過資料的版本号(vesion)的機制來實作,極大降低了資料庫的性能開銷。

(此處的版本号僅僅是對邏輯标記字段一種代稱)

我們為表添加1個字段 version;

讀取資料行時,将此版本号一同讀出;

更新資料行時:

修改此資料行所在的version: version = version + 1

同時,将送出資料的 version 與資料庫中對應記錄的目前 version 進行比對:

如果送出的資料版本号>資料庫表目前版本号,則:予以更新;否則,認為是過期資料

update t_goods

set status=2,version=version+1

where id=#{id} and version < #{version}; // 更新前将 代碼的#{version} 自增

-- SQL: 先查詢(where), 後修改(update)

或者

update t_goods

set status=2,version=version+1

where id=#{id} and version = #{version}; // 更新前 代碼的#{version} 不自增

-- SQL: 先查詢(where), 後修改(update)

2 解決方案

2-1 方案1: [伺服器端] synchronize(鎖 方法或鎖對象) + Java事務(鎖定業務流程原子性) [推薦]

@Transnational

public synchronize generateOrderCode(){

...

}

2-2 方案2: 資料庫【寫鎖/排他鎖】(鎖定多線程/程序的共享資料) + Java事務(鎖定業務流程原子性)

維護資料庫内1張存儲最大值的表,多行;

每一行為過去所使用的曆史最大值的記錄,最後插入的一行為目前最新最大值所在行。

Thread1 (Lock) 讀取表内目标字段最大值Max前,加寫鎖;(此後,在Thread1未釋放寫鎖前,其它Thread将讀取目标字段最大值失敗)

Thread1 (Query) 讀取表内目标字段最大值Max;

Thread1 (Insert) 插入新增的值————Max=Max+1;

Thread1 (UnLock) 釋放寫鎖;

2-3 方案3:資料庫【樂觀鎖】(鎖定多線程/程序的共享資料) + Java事務(鎖定業務流程原子性)

類似 方案1,需要差別2點: 1)資料庫的鎖的變化;2) 維護資料庫的行的意義/行數的變化

維護資料庫内1張存儲最大值的表,1行;

字段等于目标編号業務屬性的字段所在的行 即為目前最大值的所在行記錄;其它行 為其它編号業務屬性的字段所在的行。

2-4 方案4:Redis [推薦]

基于redis單線程的特點,生成全局唯一id,redis性能高,支援叢集分片。

亦可實作分布式鎖

[核心代碼]

思路:日期(yyyyMMddHHmmss)+redis原子生成的數字(不足6位前面補0) / 類似:20191206221953000001

理論上6位字尾支援每秒最多生成999999個訂單号,具體可以根據業務調整日期格式或日期後面的位數。

核心在于對象RedisAtomicLong (可以想下juc包下的AtomicLong),它對于同一個key會一直自增生成數字,這裡我設定的key過期時間為20s,減輕redis的壓力。

@Resource

private RedisTemplate redisTemplate;

public long generate(String key,Date expireTime) {

RedisAtomicLong counter = new RedisAtomicLong(key, redisTemplate.getConnectionFactory());

Long expire = counter.getExpire();

if(expire==-1){

counter.expireAt(expireTime);

}

return counter.incrementAndGet();

}

public String generateOrderId() {//生成id為目前日期(yyMMddHHmmss)+6位(從000000開始不足位數補0)

LocalDateTime now = LocalDateTime.now();

String orderIdPrefix = getOrderIdPrefix(now);//生成yyyyMMddHHmmss

String orderId = orderIdPrefix+String.format("%1$06d", generate(orderIdPrefix,getExpireAtTime(now)));

return orderId;

}

public static String getOrderIdPrefix(LocalDateTime now){

return now.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));

}

public Date getExpireAtTime(LocalDateTime now){

ZoneId zoneId = ZoneId.systemDefault();

LocalDateTime localDateTime = now.plusSeconds(20);

ZonedDateTime zdt = localDateTime.atZone(zoneId);

Date date = Date.from(zdt.toInstant());

return date;

}

可讀性好,不能太長。一般訂單都是全數字的。可使用redis的incr指令生成訂單号。

優點:可讀性好,不會重複

缺點:需要搭建redis伺服器

2-5 方案5:UUID 或 雪花算法

uuid生成全球唯一id,生成方式簡單粗暴;常用于生成token令牌。

基于雪花算法snowflake 生成全局id,本地生成,沒有網絡開銷,效率高,但是依賴機器時鐘。

個人建議: 存儲最新值到資料庫前,檢查此值是否已存儲,防止極小機率事件發生

優點:簡單,很難很難很難重複(機率極小),本地生成,沒有網絡開銷,效率高;

缺點:長度較長;沒有遞增趨勢性;可讀性差;不易維護;

2-6 方案6:MySQL的ID自動增長

mysql自帶自增生成id,oracle可以用序列生成id,但在資料庫叢集環境下,擴充性不好。

優點:不需要我們自己生成訂單号,mysql會自動生成。

缺點:如果訂單表數量太大時需要分庫分表,此時訂單号會重複。如果資料備份後再恢複,訂單号會變。

2-7 方案7: 日期+随機數

采用毫秒+随機數。

缺點:仍然有重複的可能。不建議采用此方案。在沒有更好的解決方案之前可以使用。

X 參考與推薦文獻