事务允许将多个 SQL 语句分组到单个工作单元中,该工作单元作为一个原子单元提交到数据库。 如果事务中的任何语句失败,可以回滚前面语句所做的更改。 启动事务时数据库的初始状态将保留。 一次对数据库进行大量更改时,使用事务还可以提高 SQLite 的性能。
并发
在 SQLite 中,数据库中一次只允许有一个事务具有挂起的更改。 因此,如果另一个事务需要很长时间才能完成,则对 BeginTransaction 调用 Execute 和 SqliteCommand 方法可能会超时。
有关锁定、重试和超时的详细信息,请参阅 数据库错误。
隔离级别
默认情况下,事务在 SQLite 中 可序列化 。 此隔离级别可确保完全隔离事务中所做的任何更改。 在事务之外执行的其他语句不受事务更改的影响。
SQLite 还支持使用共享缓存时读取未提交的数据。 此级别允许脏读、不可重复读和幻像:
当事务外部的查询返回一个事务中挂起的更改,但事务中的更改被回滚时,会发生“脏读”。 结果包含从不实际提交到数据库的数据。
当事务查询同一行两次时,会发生 不可重复的读取 ,但结果不同,因为两个查询之间发生了另一个事务的更改。
幻像在事务过程中为满足查询的 where 子句而更改或添加的行。 如果允许,在同一事务中执行两次时,同一查询可能会返回不同的行。
Microsoft.Data.Sqlite 将传递的 BeginTransaction IsolationLevel 视为最低级别。 实际隔离级别将提升为读取未提交内容或可序列化。
下面的代码模拟脏读。 请注意,连接字符串必须包含 Cache=Shared。
using (var firstTransaction = firstConnection.BeginTransaction())
{
var updateCommand = firstConnection.CreateCommand();
updateCommand.CommandText =
@"
UPDATE data
SET value = 'dirty'
";
updateCommand.ExecuteNonQuery();
// Without ReadUncommitted, the command will time out since the table is locked
// while the transaction on the first connection is active
using (secondConnection.BeginTransaction(IsolationLevel.ReadUncommitted))
{
var queryCommand = secondConnection.CreateCommand();
queryCommand.CommandText =
@"
SELECT *
FROM data
";
var value = (string)queryCommand.ExecuteScalar();
Console.WriteLine($"Value: {value}");
}
firstTransaction.Rollback();
}
延期的交易
从 Microsoft.Data.Sqlite 版本 5.0 开始,事务可以延迟。 这会延迟在数据库中创建实际事务,直到执行第一个命令。 它还会导致事务从读取事务逐步升级到其命令所需的写入事务。 这可用于在事务期间启用对数据库的并发访问。
using (var transaction = connection.BeginTransaction(deferred: true))
{
// Before the first statement of the transaction is executed, both concurrent
// reads and writes are allowed
var readCommand = connection.CreateCommand();
readCommand.CommandText =
@"
SELECT *
FROM data
";
var value = (long)readCommand.ExecuteScalar();
// After a the first read statement, concurrent writes are blocked until the
// transaction completes. Concurrent reads are still allowed
var writeCommand = connection.CreateCommand();
writeCommand.CommandText =
@"
UPDATE data
SET value = $newValue
";
writeCommand.Parameters.AddWithValue("$newValue", value + 1L);
writeCommand.ExecuteNonQuery();
// After the first write statement, both concurrent reads and writes are blocked
// until the transaction completes
transaction.Commit();
}
警告
如果延迟事务中的命令导致事务在数据库锁定时从读取事务升级到写入事务,则这些命令可能会失败。 发生这种情况时,应用程序需要重试整个事务。
保存点
Microsoft.Data.Sqlite 版本 6.0 支持保存点。 保存点可用于创建嵌套事务。 保存点可以在不影响事务的其他部分的情况下回滚,即使保存点可能已提交(释放),其更改也可能在稍后作为其父事务的一部分回滚。
以下代码演示了如何使用“乐观离线锁”模式来检测并发更新,并将保存点内的冲突解析为更大的事务的一部分。
using (var transaction = connection.BeginTransaction())
{
// Transaction may include additional statements before the savepoint
var updated = false;
do
{
// Begin savepoint
transaction.Save("optimistic-update");
var insertCommand = connection.CreateCommand();
insertCommand.CommandText =
@"
INSERT INTO audit
VALUES (datetime('now'), 'User updates data with id 1')
";
insertCommand.ExecuteScalar();
var updateCommand = connection.CreateCommand();
updateCommand.CommandText =
@"
UPDATE data
SET value = 2,
version = $expectedVersion + 1
WHERE id = 1
AND version = $expectedVersion
";
updateCommand.Parameters.AddWithValue("$expectedVersion", expectedVersion);
var recordsAffected = updateCommand.ExecuteNonQuery();
if (recordsAffected == 0)
{
// Concurrent update detected! Rollback savepoint and retry
transaction.Rollback("optimistic-update");
// TODO: Resolve update conflicts
}
else
{
// Update succeeded. Commit savepoint and continue with the transaction
transaction.Release("optimistic-update");
updated = true;
}
}
while (!updated);
// Additional statements may be included after the savepoint
transaction.Commit();
}