前幾天一個人問到了關于流水号重複的問題,我想了下,雖然說這個問題比較簡單,但是具有廣泛性,是以寫了這篇部落格來介紹下,希望對大家有所幫助。
在進行資料庫應用開發時經常會遇到生成流水号的情況,比如說做了一個訂單子產品,要求訂單号是唯一的,規則是:下訂單時的年月日+6位的流水号這樣的規則。
對于這種要生成流水号的系統,我們一般是在資料庫中建立了一個種子表,每次生成新的訂單時:
1.讀取當天種子最大值。
2.根據種子最大值和當時的年月日生成唯一的訂單号。
3.更新種子最大值,使最大值+1。
4.根據生成的訂單号将訂單資料插入到訂單表中。
以上幾步操作是在一個事務中完成,保證了流水号的連續。這個思路是正确的,使用起來好像也沒有什麼問題,但是在業務量比較大的情況下卻經常報錯:“訂單号違反主鍵限制,不能将重複的訂單号插入到訂單表中。”這是怎麼回事?讓我們做一個簡單的demo來重制一下:
1.建立種子表和訂單表,這裡隻是一個簡單的demo,是以就省去了很多字段,而且訂單号假設就是一個流水号,不用再使用年月日+6位流水号了。

create table seek --種子表
(
seekvalue int
)
go
insert into seek values(0)--種子初始值為0
create table orders
orderid int primary key, --訂單号,主鍵
remark varchar(5) not null
)

2.建立一個存儲過程,該存儲過程傳入remark參數,根據生成的流水号插入到訂單表中:

create proc addorder --author:深藍
@remark varchar(5) --傳入的參數
as
declare @seek int
begin tran --開啟一個事務
select @seek=seekvalue --讀取種子表中的最大值作為流水号
from seek
--生成訂單号這一步省略,因為這裡假定的訂單的編号就是流水号
update seek set seekvalue=@seek+1 --更新種子表,使最大值+1
insert into t1 values(@seek,@remark) --插入一條訂單資料
commit --送出事務

3.建立一個查詢視窗,使用以下語句調用建立的存儲過程,不斷的插入新訂單:
while 1=1
exec addorder 'test1' --不斷的插入訂單
4.再建立一個查詢視窗,使用通過的方式,不斷的插入新訂單,這樣用于模拟高并發時候的情況:
exec addorder 'test2'
5.運作了一段時間後,我們停止這兩個死循環,我們可以看到消息視窗中存在大量的異常:
消息 2627,級别 14,狀态 1,過程 addorder,第 11 行
違反了 primary key 限制 'pk__orders__c3905baf08ea5793'。不能在對象 'dbo.orders' 中插入重複鍵。
語句已終止。
為什麼會這樣呢?這得從事務隔離級别和鎖來解釋:
一般我們寫程式時都是使用的是預設的事務隔離級别——已送出讀,在第一步查詢seek表時,系統會為該表放置共享鎖,而鎖的相容性中共享鎖和共享鎖是可以相容的,是以一個事務在讀取seek表最大值時,其他事務也可以讀取出相同的最大值,兩個事務中讀取到了相同的最大值,是以産生了相同的流水号,是以産生了相同的訂單号,是以才會出現違反主鍵限制的錯誤。
既然知道了這其中的原理了,那麼解決辦法也就有了,隻需要先對種子表中的數+1,然後再進行讀取即可,修改存儲過程如下:

alter proc addorder--author:深藍
@remark varchar(5)
declare @seek int
begin tran
update seek set seekvalue=seekvalue+1 --先修改資料
select @seek=seekvalue-1 --已經加了1,是以這裡-1下來
insert into orders values(@seek,@remark)
commit

為什麼這樣寫就可以呢?第一步執行更新操作,系統會請求更新鎖然後再更新為排他鎖,因為更新鎖和更新鎖以及排他鎖都是不相容的,是以一個事務對seek表進行了更新後,其他的事務就不能對表進行更新操作,隻有等到事務送出以後才能繼續。
這裡附上鎖相容性表:
現有授予模式
請求模式
is
s
u
ix
six
x
意向共享 (is)
是
否
共享 (s)
更新 (u)
意向排他 (ix)
意向排他共享 (six)
排他 (x)