數據庫存在即更新的高併發處理 - 轉

這篇文章的主要內容,來自與其餘人的討論。數據庫

  軟件系統的開發或設計時,容易遇到有併發的狀況。有時候須要刻意去避免,防止數據錯誤。好比超市賣商品,可能兩個櫃檯同時賣出一款礦泉水,若是軟件系統後臺須要跟蹤每一個商品的庫存,此時就須要特別考慮。若是兩個櫃檯,同時採起"讀當前庫存,減一,獲得最新庫存,保存"的設計,則可能會致使數據錯誤。好比,兩個櫃檯,讀當前庫存,都獲得 100, 減一,都獲得99,做爲最新數據保存,保存99。最後,儘管同時賣出了兩瓶礦泉水,最後系統的庫存確是99。無疑是有問題的。併發

  一個簡單的解決辦法,就是再設計一個接口表。對於有可能併發的操做,統一插入一條"待處理的操做指令"到此接口表中,而後單獨起一個線程,逐個處理此接口表中待處理數據。測試

  大體步驟以下:ui

1. 併發處理,統一插入一條待處理的操做指令到此接口表中,只 insert:
insert into ti_xxx ....; --process_flag = 0

2. 單獨起一個線程,逐個讀 : ti_xxx 中未處理的數據.
2.1
select top 1 from ti_xxx where process_flag = 0 order by increase_key,created_time;

2.2. insert/update 到 tt_xxx :
if exists(select 1 from tt_xxx where ....)
  update tt_xxx ....
else
  insert into tt_xxx...

2.3 更新 ti_xxx 數據爲已處理:
update ti_xxx set process_flag = 1 where increase_key = xxx;

其中,ti_xxx 表使用自增加主鍵,或使用 uuid 作主鍵。線程

 

  若是隻是單純的超市軟件系統,它的庫存計算,其實不用很實時。讓管理員人員,看當前時間的庫存,與看5分鐘以前的庫存,從純粹的管理層面,並無大的區別。實際上,絕大多數系統,數據的實時性要求,都沒有高到須要徹底實時。另外一方面,此類系統對數據的最終準確性,要求倒是很是高的。好比,客戶不太在乎,9:05 分賣出一款礦泉水,只能在 9:10看到庫存減小。但客戶在乎的是,9:05 分時刻賣出一款礦泉水,至少在下班後(21:00),能看到結果。設計

  若是咱們將以上所述"單獨起一個線程",作成每 0.5秒 運行一次的定時任務,則對於客戶來講,徹底看不到影響。調試

 

-------------------------------日誌

2017/6/3 補充,(2017/6/4發現,如下測試步驟中有不當的地方,請忽略).blog

有人提到,能夠用純 SQL 來處理併發,使用適當的 lock 。但這樣有時並無論用。好比按以下測試,則測試出問題:接口

測試環境: Windows 8.1 64位 + SQL Server 2014 Express.
測試步驟: 
step_1, 建立數據庫 test_db1。

step_2, 運行 SQL 更改數據庫屬性:
ALTER DATABASE test_db1 SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
ALTER DATABASE test_db1 SET ALLOW_SNAPSHOT_ISOLATION ON;
ALTER DATABASE test_db1 SET READ_COMMITTED_SNAPSHOT ON;
ALTER DATABASE test_db1 SET MULTI_USER;

step_3,建立表,
CREATE TABLE [dbo].[Test](
[Id] [bigint] NULL,
[Name] [varchar](50) NULL,
[Counter] [bigint] NULL
) ON [PRIMARY];

step_4,建立存儲過程:
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO


CREATE PROCEDURE [dbo].[sp_test]
@Id [bigint],
@Name [varchar](50)

AS
BEGIN
BEGIN TRANSACTION
--先嚐試更新記錄佔坑
UPDATE Test WITH(HOLDLOCK)
SET [Counter] = [Counter] + 1
WHERE Id = @Id;

WAITFOR DELAY '00:02:00';


--若是更新操做沒有影響行,證實記錄不存在,則插入
IF @@ROWCOUNT<1
BEGIN
INSERT Test
( Id, Name, [Counter] )
VALUES ( @Id@Name, 1 );
END
COMMIT
END


GO

中間加了暫停。

step_5. 開兩個 SQL Server Management studio, 分別運行 sp_test, 參數分別爲:
step_5_1: 
id=1,
name='A',

step_5_2: 
id=1,
name='B',

step_6, 驗證最後數據:
SELECT TOP 1000 * FROM [test_db1].[dbo].[Test];
獲得兩行數據:
Id Name Counter
1 A 2
1 B 1

結論:
純SQL 代碼不能起到指望的結果。

---------------------------------------------

2017/6/4 補充更正

從新測試,結論是 UPDATE...WITH(HOLDLOCK)... 能夠鎖住表的 update 操做,起到"併發時順序處理"的指望結果。但並不須要使用 SET [Counter] = [Counter] + 1 這樣的語句。

測試環境: Windows 8.1 64位 + SQL Server 2014 Express.
測試步驟: 
step_1, 建立數據庫 test_db1。

step_2, 運行 SQL 更改數據庫屬性:
ALTER DATABASE test_db1 SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
ALTER DATABASE test_db1 SET ALLOW_SNAPSHOT_ISOLATION ON;
ALTER DATABASE test_db1 SET READ_COMMITTED_SNAPSHOT ON;
ALTER DATABASE test_db1 SET MULTI_USER;

step_3,建立表,
CREATE TABLE [dbo].[Test](
    [Id] [bigint] NULL,
    [Name] [varchar](50) NULL,
    [Counter0] [bigint] NULL,
    [created_time] [datetime] NULL,
    [updated_time] [datetime] NULL
) ON [PRIMARY];

step_4,建立存儲過程:
CREATE PROCEDURE [dbo].[sp_test]
    @Id  [bigint],
    @Name [varchar](50)

AS
BEGIN
    BEGIN TRANSACTION
    --先嚐試更新記錄佔坑
    print 'a0:'+ convert(varchar(255), getdate(), 121) + ','

    UPDATE  Test WITH(HOLDLOCK)
    SET   --  [Counter] = [Counter] + 1, 
        Name=@Name, updated_time = getdate()
    WHERE   Id = @Id;

    --須要在 WAITFOR DELAY 以前,將 @@ROWCOUNT 中的數值,暫時保存起來。由於 WAITFOR DELAY 以後,@@ROWCOUNT 中的數值會變。
    DECLARE @v_ROWCOUNT bigint
    set @v_ROWCOUNT = @@ROWCOUNT
    print 'a1:'+ convert(varchar(255), getdate(), 121)  + ',ROWCOUNT='+ cast( @v_ROWCOUNT as varchar(255))
    print 'a1.5:'+ convert(varchar(255), getdate(), 121)  + ',ROWCOUNT='+ cast( @@ROWCOUNT as varchar(255))

    WAITFOR DELAY '00:00:20';

    print 'a2:'+ convert(varchar(255), getdate(), 121) + ',ROWCOUNT='+ cast( @@ROWCOUNT as varchar(255))

    --若是更新操做沒有影響行,證實記錄不存在,則插入
    IF @v_ROWCOUNT < 1
    BEGIN
        INSERT  Test
                ( Id, Name
                --, [Counter]
                ,created_time,updated_time )
        VALUES  ( @Id, @Name
            --, 1
            , getdate(), getdate() );
        print 'a3:'+ convert(varchar(255), getdate(), 121) + ',ROWCOUNT='+ cast( @@ROWCOUNT as varchar(255))
        
        WAITFOR DELAY '00:00:05';
    END
        print 'a4:'+ convert(varchar(255), getdate(), 121) 
        WAITFOR DELAY '00:00:02';
        print 'a4.5:'+ convert(varchar(255), getdate(), 121) 
    COMMIT
        print 'a5:'+ convert(varchar(255), getdate(), 121) 
END


GO

中間加了暫停。

step_5. 開兩個 SQL Server Management studio, 分別運行 sp_test, 參數分別爲:
step_5_1: 
id=1,
name='A',

step_5_2: 
id=1,
name='B',

step_6, 驗證最後數據:
SELECT TOP 1000 * FROM [test_db1].[dbo].[Test];
獲得一行數據:
Id    Name    Counter0    created_time    updated_time
1    B    NULL    2017-06-04 14:59:46.517    2017-06-04 14:59:53.520

從調試運行執行存儲過程 SQL 的消息日誌中,能夠看到第二次存儲過程的 update 的執行,確實是在第一次執行的 commit 以後。


結論:
UPDATE...WITH(HOLDLOCK)... 能夠鎖住表的 update 操做,起到"併發時順序處理"的指望結果。但並不須要使用 SET [Counter] = [Counter] + 1 這樣的語句。

 

很抱歉以前的錯誤結論,可能誤導了一些朋友。

實測結果,數據庫屬性中,增長 :

ALTER DATABASE test_db1 SET ALLOW_SNAPSHOT_ISOLATION ON;
ALTER DATABASE test_db1 SET READ_COMMITTED_SNAPSHOT ON;

能夠在 UPDATE...WITH(HOLDLOCK)... 的數據庫事務執行過程當中,select 表 Test 數據。

而不加 ALLOW_SNAPSHOT_ISOLATION + READ_COMMITTED_SNAPSHOT,則此時 select 也堵塞。但 update Test 表都堵塞。

相關文章
相關標籤/搜索