SQL 服务器中的并发执行

Concurrent execution in SQL Server

Table 模式(SQL 服务器 2012)

Create Table InterestBuffer
(
    AccountNo CHAR(17) PRIMARY KEY,
    CalculatedInterest MONEY,
    ProvisionedInterest MONEY,
    AccomodatedInterest MONEY,
)

Create Table #tempInterestCalc
(
    AccountNo CHAR(17) PRIMARY KEY,
    CalculatedInterest MONEY
)

我正在做更新插入。更新存在的行并插入其他行。

UPDATE A
SET A.CalculatedInterest = A.CalculatedInterest + B.CalculatedInterest
FROM InterestBuffer A
INNER JOIN #tempInterestCalc B ON A.AccountNo = B.AccountNo

INSERT INTO InterestBuffer
SELECT A.AccountNo, A.CalculatedInterest, 0, 0
FROM #tempInterestCalc A
LEFT JOIN InterestBuffer B ON A.AccountNo = B.AccountNo
WHERE B.AccountNo IS NULL

一切正常。在并发执行期间出现问题。我通过连接其他各种 table 将数据插入 #tempInterestCalc,包括与 InterestBuffer table 的左连接,并将不同的数据集插入 #tempInterestCalc每个并发执行。

我的问题是有时执行会被另一个执行锁定,直到我连续提交它们。

我的问题是,因为我提供了不同的数据集,所以它不应该对其他并发操作产生行锁的任何影响。任何建议将不胜感激。

更新 1: 我已将 SP_LOCK 用于 InterestBuffer table。它说 IndId = 1, Type = KEY, Mode = X, Status = GRANT.

我认为更新和插入会阻止其他事务进行幻读。

更新 2:抱歉!之前我告诉过更新没问题。但现在我意识到第一个事务写入正在阻止第二个事务写入。在第一笔交易中,我 运行 更新和插入。在第二笔交易中,在我将数据插入#tempInterestCalc table 后,我只是按照以下步骤操作,它运行良好。

--INSERT DATA INTO #tempInterestCalc 

SELECT * FROM #tempInterestCalc 
RETURN

--UPDATE InterestBuffer

--INSERT InterestBuffer

更新 3: 我想我的问题是在更新期间从 InterestBuffer 中读取数据并插入到 InterestBuffer 中。

更新 4: 如果我 REBUILD INDEX InterestBuffer table 中的 BranchCode,我下面的回答有时会起作用。批处理 insert/update 是否有任何原因导致索引出现问题???

更新 5: 我读到如果需要锁定页面的最大行数以进行批量更新,那么 SQL 服务器可能会锁定该页面。有什么办法可以查看哪一行被哪个页面包含,或者哪个页面在执行过程中要锁定和释放??

更新 6: 我正在提供我的场景。

CREATE TABLE [dbo].[Account](
        [AccountNo] [char](17) NOT NULL,
        [BranchCode] [char](4) NOT NULL,
     CONSTRAINT [PK_Account] PRIMARY KEY CLUSTERED 
    (
        [AccountNo] ASC
    )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
    ) ON [PRIMARY]

CREATE TABLE [dbo].[InterestBuffer](
    [AccountNo] [char](17) NOT NULL,
    [BranchCode] [char](4) NOT NULL,
    [CalculatedInterest] [money] NOT NULL,
 CONSTRAINT [PK_Buffer] PRIMARY KEY CLUSTERED 
(
    [AccountNo] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

分支 0001 的查询:

BEGIN TRAN

Declare @BranchCode AS Char(4) = '0001'
Declare @CalculatedInterestNew MONEY = 10

CREATE TABLE #tempInterestCalc
(
    AccountNo Char(17),
    BranchCode Char(4),
    CalculatedInterestNew MONEY,
    CalculatedInterestOld MONEY
)

INSERT INTO #tempInterestCalc
SELECT A.AccountNo, A.BranchCode, ISNULL(B.CalculatedInterest, 0), B.CalculatedInterest
FROM Account A 
LEFT JOIN InterestBuffer B ON A.AccountNo = B.AccountNo AND A.BranchCode = B.BranchCode
WHERE A.BranchCode = @BranchCode

UPDATE A
SET A.CalculatedInterest = B.CalculatedInterestNew + @CalculatedInterestNew
FROM InterestBuffer A
INNER JOIN #tempInterestCalc B ON A.AccountNo = B.AccountNo AND A.BranchCode = B.BranchCode
WHERE A.BranchCode = @BranchCode

INSERT INTO InterestBuffer
SELECT A.AccountNo, A.BranchCode, A.CalculatedInterestNew + @CalculatedInterestNew
FROM #tempInterestCalc A
WHERE A.CalculatedInterestOld IS NULL

DROP TABLE #tempInterestCalc
--ROLLBACK
--COMMIT TRAN

对于Branch 0002, 0003只需将@BranchCode变量值改为0002 &0003,同时运行他们。

我刚刚找到了解决办法。由于我正在按分支并发执行查询,因此我对 tables 进行了如下轻微修改;

Create Table InterestBuffer
(
    AccountNo CHAR(17) PRIMARY KEY,
    BranchCode CHAR(4),
    CalculatedInterest MONEY,
    ProvisionedInterest MONEY,
    AccomodatedInterest MONEY,
)

Create Table #tempInterestCalc
(
    AccountNo CHAR(17) PRIMARY KEY,
    BranchCode CHAR(4),
    CalculatedInterest MONEY
)

现在我将数据插入由 Branch 过滤的#tempInterestCalc。

--INSERT DATA INTO #tempInterestCalc 

SELECT * 
into #temp
FROM InterestBuffer A WITH (NOLOCK)
Where A.BranchCode = MY_BRANCH

UPDATE A
SET A.CalculatedInterest = C.CalculatedInterest + B.CalculatedInterest
FROM InterestBuffer A
INNER JOIN #tempInterestCalc B ON A.AccountNo = B.AccountNo
INNER JOIN #temp C ON A.AccountNo = C.AccountNo AND A.BranchCode = C.BranchCode

INSERT INTO InterestBuffer
SELECT A.AccountNo, A.CalculatedInterest, 0, 0
FROM #tempInterestCalc A
LEFT JOIN #temp B ON A.AccountNo = B.AccountNo AND A.BranchCode = B.BranchCode
WHERE B.AccountNo IS NULL

我的问题是在 update/insert 期间,我试图从同一个 table 中读取,但被其他事务写入锁定。

这里使用NOLOCK是安全的,因为单个分支的数据不能被另一个事务修改,只能被它自己的事务修改(没有脏读的机会)。

仍在寻找其他不使用 NOLOCK 的更好方法。

i) 看脏读没问题那你可以用Nolock,没问题或者你上面的SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED proc.There没问题,两者都可以是相同的。在使用 nolock 之前,您应该只考虑 "Problem of dirty read" 数据。

ii) 你没有解释你的问题,所以 well.what 是 #tempInterestCalc 和 #temp 的使用。

iii) #tempInterestCalc 从哪里填充?

iv) 在插入过程中 #temp B 的记录未被使用,因此您可以删除左连接并使用 exists.But 这取决于以上几点是否清楚。

iv) 您正在从临时 table 中的 InterestBuffer 获取记录,然后再次更新然后再次插入相同的 table.This 不清楚。

您可能会遇到潜在的死锁问题,因为您在写入后正在对 InterestBuffer table 进行另一次读取。如果另一个事务已为更新阻止了 InterestBuffer table 的一部分,并且您的事务正尝试再次从中读取执行插入所需的 select,则事务可能会死锁。

您说您在计算 #tempInterestCalc table 时已经加入 InterestBuffer...为什么不使用它来缓存 [=14= 所需的一些数据] 这样你就不用再读一遍了?

将温度 table 更改为:

Create Table #tempInterestCalc
(
    AccountNo CHAR(17) PRIMARY KEY,
    CalculatedInterestNew MONEY,
    CalculatedInterestOld MONEY
)

您可能希望在开始事务之前设置 repeatable 读取隔离级别:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

它是更严格的锁定,但会阻止其他事务尝试同时处理相同的记录,您可能需要这样做,因为您正在组合旧值和新值。考虑这种情况:

  • 事务 1 读取数据并希望将 0.03 添加到现有 CalculatedInterest 共 5.0。
  • 事务 2 读取数据并希望将 0.02 添加到 5.0。
  • 事务 1 将 CalculatedInterest 更新为 5.03。
  • 事务 2 的更新将事务 1 中的值覆盖为 5.03(而不是添加到它并提出 5.05)。

如果您确定事务永远不会触及相同的记录,也许您不需要这个,但如果是这样,已提交的读取将不会让事务 2 读取值,直到事务 1 已完成。

然后将您的事务分成不同的读取阶段,然后是写入阶段:

--insert data into #tempInterestCalc and include the previous interest value
insert into #tempInterestCalc
select AccountNo, 
    Query.CalculatedInterest CalculatedInterestNew, 
    InterestBuffer.CalculatedInterest CalculatedInterestOLD
from 
    (
    ...
    ) Query
left join InterestBuffer
on Query.AccountNo = InterestBuffer.AccountNo

UPDATE A
SET A.CalculatedInterest = B.CalculatedInterestNew + B.CalculatedInterestOld
FROM InterestBuffer A
INNER JOIN #tempInterestCalc B ON A.AccountNo = B.AccountNo

INSERT INTO InterestBuffer
SELECT A.AccountNo, A.CalculatedInterestNew, 0, 0
FROM #tempInterestCalc A
--no join here needed now to read from InterestBuffer
WHERE CalculatedInterestOld is null

这不应该死锁...但是您可能会看到 "unnecessary" 由于 Lock Escalation, particularly if you are updating a large number of rows. Once there are more than 5000 locks on a table it will escalate to a table. No other transactions will then be able to continue until the transaction completes. This isn't necessarily a bad thing... you just want to make sure that your transactions are as short as possible so as to not lock other transactions for too long. If lock escalation is causing you problems, there are some things you can do to mitigate this 而阻塞,例如:

  • 将您的事务分解为更小的工作块,从而创建更少的锁。
  • 确保您有一个有效的查询计划。
  • 明智地使用锁提示。

检查你的查询计划,看看在任何语句中是否有任何 table 扫描 InterestBuffer ......特别是你的初始人口 #tempInterestCalc 因为你没有显示你是如何构建它的。

如果您绝不会同时更新一个分支中的帐户,那么您可以考虑保持主键不变,但将聚簇索引更改为 Branch, Account number(顺序很重要)。这将使同一分支机构的所有记录在物理上彼此相邻,并减少您的计划执行 table 扫描或锁定其他交易可能需要的页面的机会。然后,您还可以使用 PAGLOCK 提示,这将鼓励 SQL 服务器按页而不是按行锁定,并防止达到触发锁定升级的阈值。为此,从问题中的 UPDATE 6 修改代码如下所示:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN TRAN

Declare @BranchCode AS Char(4) = '0001'
Declare @CalculatedInterestNew MONEY = 10

CREATE TABLE #tempInterestCalc
(
    AccountNo Char(17),
    BranchCode Char(4),
    CalculatedInterestNew MONEY,
    CalculatedInterestOld MONEY
)

INSERT INTO #tempInterestCalc
SELECT A.AccountNo, A.BranchCode, ISNULL(B.CalculatedInterest, 0), B.CalculatedInterest
FROM Account A
LEFT JOIN InterestBuffer B
ON A.AccountNo = B.AccountNo AND A.BranchCode = B.BranchCode
WHERE A.BranchCode = @BranchCode

UPDATE A WITH (PAGLOCK)
SET A.CalculatedInterest = B.CalculatedInterestNew + @CalculatedInterestNew
FROM InterestBuffer A
INNER JOIN #tempInterestCalc B ON A.AccountNo = B.AccountNo AND A.BranchCode = B.BranchCode
WHERE A.BranchCode = @BranchCode

INSERT INTO InterestBuffer WITH (PAGLOCK)
SELECT A.AccountNo, A.BranchCode, A.CalculatedInterestNew + @CalculatedInterestNew
FROM #tempInterestCalc A
WHERE A.CalculatedInterestOld IS NULL

DROP TABLE #tempInterestCalc
--ROLLBACK
--COMMIT TRAN

因为记录是按物理顺序排列在一起的,所以即使更新数千条记录,也应该只锁定几页。然后,您可以 运行 分支 0003 的事务与 0001 同时进行,而不会出现任何阻塞问题。但是,如果您尝试同时执行相邻分支(例如 0002),则可能会遇到阻塞问题。这是因为分支 0001 和 0002 的某些记录可能共享同一页。

如果你真的需要分开你的分支,你可以考虑使用 Partitioned Table or Index。我对它们知之甚少,但听起来它可能对你正在尝试做的事情有用,但它也可能伴随着它自己的一系列并发症。