分享一點自己多年編程的經驗,拋磚引玉吧。期望與大家交流。 ================================ ...
這篇文章的主要內容,來自與其他人的討論。
軟體系統的開發或設計時,容易遇到有併發的情況。有時候需要刻意去避免,防止數據錯誤。比如超市賣商品,可能兩個櫃臺同時賣出一款礦泉水,如果軟體系統後臺需要跟蹤每個商品的庫存,此時就需要特別考慮。如果兩個櫃臺,同時採取"讀當前庫存,減一,得到最新庫存,保存"的設計,則可能會導致數據錯誤。比如,兩個櫃臺,讀當前庫存,都得到 100, 減一,都得到99,作為最新數據保存,保存99。最後,儘管同時賣出了兩瓶礦泉水,最後系統的庫存確是99。無疑是有問題的。
一個簡單的解決辦法,就是再設計一個介面表。對於有可能併發的操作,統一插入一條"待處理的操作指令"到此介面表中,然後單獨起一個線程,逐個處理此介面表中待處理數據。
大致步驟如下:
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 補充,
有人提到,可以用純 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 代碼不能起到期望的結果。