Skip to content

数据库事务详解

目录

    1. 数据库事务的定义与特性
    1. 事务隔离级别
    1. 事务操作指南
    1. 并发事务问题
    1. 锁机制

1. 数据库事务的定义与特性

数据库事务是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。事务是数据库操作的最小工作单元,是一系列数据库操作的集合,这些操作要么全部执行,要么全部不执行。

事务具有ACID四个核心特性:

  • 原子性(Atomicity):事务是最小的执行单位,不允许分割。事务包含的所有操作要么全部成功,要么全部失败回滚,不会结束在中间某个环节。

                  通俗解释:就像银行转账一样,从账户A转钱到账户B,这两个操作必须同时成功或同时失败。如果A扣款成功但B没有收到钱,那就不符合原子性。
    
  • 一致性(Consistency):事务必须使数据库从一个一致性状态变换到另一个一致性状态,确保数据的完整性和业务规则得到遵守。

                  通俗解释:数据库在事务执行前后都必须保持一致性。比如,转账前后两个账户的总金额应该保持不变,这就是一种一致性约束。
    
  • 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。

                  通俗解释:就像两个人同时查询同一个账户余额,他们应该看到相同的结果,而不会因为另一个正在转账而导致查询结果不一致。
    
  • 持久性(Durability):事务一旦提交,它对数据库中数据的改变就是永久性的,即使系统出现故障也不会丢失。

                  通俗解释:一旦你确认转账成功,即使此时银行系统突然断电,你的转账记录也不会丢失,重启后依然有效。
    

关键点

  • 事务是数据库操作的逻辑单元,具有ACID特性

  • 原子性确保操作要么全部成功要么全部失败

  • 一致性维护数据的完整性和业务规则

  • 隔离性防止并发事务相互干扰

  • 持久性保证已提交数据的永久保存

2. 事务隔离级别

事务隔离级别定义了一个事务可能受其他并发事务影响的程度。SQL标准定义了四种隔离级别:

2.1 读未提交(Read Uncommitted)

这是最低的隔离级别,允许一个事务读取另一个事务未提交的数据,也被称为"脏读"。这种级别可能导致脏读、不可重复读和幻读等问题。

  • 优点:并发性能最高,因为几乎没有锁定机制。

  • 缺点:数据一致性最差,可能出现各种并发问题。

  • 适用场景:对数据一致性要求极低,追求极致性能的场景。

2.2 读已提交(Read Committed)

一个事务只能读取另一个事务已经提交的数据。可以避免脏读问题,但可能出现不可重复读和幻读。

  • 优点:避免了脏读,大多数数据库系统的默认隔离级别。

  • 缺点:可能出现不可重复读和幻读。

  • 适用场景:大多数应用程序的常见选择。

2.3 可重复读(Repeatable Read)

在一个事务内多次读取同一数据时,结果是一致的。可以避免脏读和不可重复读,但可能出现幻读。

  • 优点:避免了脏读和不可重复读。

  • 缺点:可能出现幻读,性能相对较低。

  • 适用场景:对数据一致性要求较高的场景。

2.4 串行化(Serializable)

最高的隔离级别,强制事务串行执行,避免了脏读、不可重复读和幻读的问题。通过强制事务排序,避免了并发执行。

  • 优点:数据一致性最高,解决了所有并发问题。

  • 缺点:性能最差,严重降低数据库系统的并发性。

  • 适用场景:对数据一致性要求极高,且能接受性能损失的场景。

2.5 查看和更改事务隔离级别

在实际开发中,了解和设置数据库的事务隔离级别是非常重要的。不同的应用场景可能需要不同的隔离级别来平衡数据一致性和系统性能。本节将详细介绍如何查看和更改主流数据库系统的事务隔离级别。

2.5.1 如何查看当前事务隔离级别

不同数据库系统提供了不同的方法来查看当前的事务隔离级别:

        MySQL
sql
-- 查看全局隔离级别
SELECT @@global.transaction_isolation;

-- 查看会话隔离级别
SELECT @@session.transaction_isolation;

-- 查看当前连接的隔离级别
SELECT @@transaction_isolation;
        PostgreSQL
sql
-- 查看当前事务隔离级别
SHOW transaction_isolation;

-- 或者使用
SELECT current_setting('transaction_isolation');
        SQL Server
sql
-- 查看当前会话的隔离级别
SELECT CASE transaction_isolation_level 
WHEN 1 THEN 'Read Uncommitted' 
WHEN 2 THEN 'Read Committed' 
WHEN 3 THEN 'Repeatable Read' 
WHEN 4 THEN 'Serializable' 
WHEN 5 THEN 'Snapshot' 
ELSE 'Unknown' END AS isolation_level 
FROM sys.dm_exec_sessions 
WHERE session_id = @@SPID;
        Oracle
sql
-- Oracle默认使用读已提交隔离级别
-- 可以通过以下查询查看当前会话信息
SELECT s.sid, s.serial#, s.isolation_level 
FROM v$session s 
WHERE s.audsid = SYS_CONTEXT('USERENV','SESSIONID');

2.5.2 如何更改事务隔离级别

更改事务隔离级别有两种方式:临时更改(仅对当前会话有效)和永久更改(对所有新会话有效)。不同数据库系统的语法有所不同:

        MySQL
-- 临时更改当前会话的隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 临时更改全局隔离级别(影响所有新连接)
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- 注意:全局更改不影响已存在的连接
        PostgreSQL
-- 更改当前事务的隔离级别
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 执行事务操作
COMMIT;

-- 更改当前会话的隔离级别
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE;
        SQL Server
-- 设置当前会话的隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 隔离级别选项包括:
-- READ UNCOMMITTED
-- READ COMMITTED (默认)
-- REPEATABLE READ
-- SNAPSHOT
-- SERIALIZABLE
        Oracle
-- Oracle中设置事务隔离级别
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- Oracle支持的隔离级别:
-- READ COMMITTED (默认)
-- SERIALIZABLE
-- READ ONLY

-- 注意:Oracle的设置只对当前事务有效

2.5.3 注意事项和最佳实践

关键点

  • 更改隔离级别通常只影响后续的事务,不会影响已经在执行的事务

  • 全局更改隔离级别会影响所有新建立的连接,但不会影响已存在的连接

  • 某些隔离级别(如SERIALIZABLE)会显著降低数据库性能,应谨慎使用

  • 在生产环境中更改隔离级别前,应充分测试其对应用性能的影响

  • 不同的数据库系统对隔离级别的实现可能存在细微差别

  • 在分布式系统中,需要考虑跨数据库的隔离级别一致性

2.5.4 常见问题解答

FAQ

  • Q: 更改隔离级别后为什么没有立即生效? A: 大多数情况下,更改隔离级别只影响新开始的事务。如果需要立即生效,需要重新开始事务。

  • Q: 为什么设置了SERIALIZABLE隔离级别后性能下降明显? A: SERIALIZABLE是最高级别的隔离,会强制事务串行执行,从而大大降低并发性能。只有在确实需要最高数据一致性时才使用。

  • Q: 不同数据库系统的隔离级别名称是否完全一致? A: 虽然遵循SQL标准,但各数据库系统在实现上可能有细微差别。例如,SQL Server有SNAPSHOT隔离级别,而MySQL没有直接对应的级别。

  • Q: 如何选择合适的隔离级别? A: 应根据应用的具体需求来选择:

                              对数据一致性要求不高的读密集型应用可以选择READ COMMITTED
    
  • 需要避免不可重复读的场景可以选择REPEATABLE READ

  • 对数据一致性要求极高的场景可以选择SERIALIZABLE

  • 需要高性能读取且能容忍轻微不一致的场景可以考虑READ UNCOMMITTED

3. 事务操作指南

在实际开发中,正确使用事务是保证数据一致性的关键。以下是事务操作的基本步骤和最佳实践:

3.1 开启事务

在执行事务操作前,首先需要开启一个事务。不同数据库系统开启事务的语法略有不同:

-- MySQL
START TRANSACTION;

-- PostgreSQL
BEGIN;

-- SQL Server
BEGIN TRANSACTION;

3.2 执行事务操作

在事务中执行一系列的数据库操作,如插入、更新或删除数据:

sql
INSERT INTO users (name, email) VALUES ('张三', 'zhangsan@example.com');
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;

3.3 提交事务

如果所有操作都成功执行,使用COMMIT命令提交事务,使更改永久生效:

COMMIT;

3.4 回滚事务

如果在事务执行过程中出现错误,使用ROLLBACK命令回滚事务,撤销所有未提交的更改:

ROLLBACK;

4. 并发事务问题

当多个事务同时执行时,可能会引发一些并发问题,主要包括以下几种:

4.1 脏读(Dirty Read)

脏读是指一个事务读取了另一个未提交事务的数据。例如,事务A修改了一条数据但尚未提交,事务B此时读取了这条数据。如果事务A随后回滚,事务B读取到的就是无效数据。

关键点

  • 发生在读取未提交数据时

  • 通过读已提交隔离级别可避免

  • 可能导致严重的数据不一致问题

  • 在实际应用中应尽量避免

4.2 不可重复读(Non-repeatable Read)

不可重复读是指在一个事务内多次读取同一数据时,由于其他事务的修改或删除操作,导致每次读取的结果不一致。

关键点

  • 发生在同一数据多次读取结果不一致时

  • 通过可重复读隔离级别可避免

  • 主要由其他事务的UPDATE/DELETE操作引起

  • 在金融系统等对数据一致性要求高的场景需特别注意

4.3 幻读(Phantom Read)

幻读是指在一个事务内多次查询某个范围内的数据时,由于其他事务的插入操作,导致每次查询返回的结果集不一致。

关键点

  • 发生在相同条件查询记录数不一致时

  • 通过串行化隔离级别可避免

  • 主要由其他事务的INSERT操作引起

  • 在统计查询、报表生成等场景中需重点关注

5. 锁机制

在数据库并发控制中,锁机制是保证数据一致性和完整性的重要手段。主要有两种锁机制:乐观锁和悲观锁。

5.1 乐观锁

核心特征:基于数据版本控制机制,假设并发操作不会发生冲突,仅在提交时检查数据是否被修改。

实现方式:

  • 版本号机制:为数据表增加一个version字段,每次更新时将version加1。提交更新时检查version值是否发生变化,若变化则拒绝更新。
sql
-- 查询时获取版本号
SELECT id, name, version FROM users WHERE id = 1;

-- 更新时检查版本号
UPDATE users SET name = 'new_name', version = version + 1 
WHERE id = 1 AND version = @original_version;
  • 时间戳机制:使用时间戳字段代替版本号,原理与版本号机制相同。
sql
-- 查询时获取时间戳
SELECT id, name, timestamp FROM users WHERE id = 1;

-- 更新时检查时间戳
UPDATE users SET name = 'new_name', timestamp = CURRENT_TIMESTAMP 
WHERE id = 1 AND timestamp = @original_timestamp;

关键点

  • 适用于读多写少的应用场景

  • 需要处理版本冲突的情况

  • 不适合长时间运行的事务

  • 假设数据冲突概率低,只在提交时检查冲突

  • 通过版本号或时间戳机制实现

  • 能提供更好的并发性能

  • 可能发生写冲突导致事务重试

5.2 悲观锁

核心特征:基于数据库锁定机制,假设并发操作会发生冲突,在操作开始时就锁定相关资源。

实现方式:

  • 行级锁:通过SELECT ... FOR UPDATE语句实现,锁定特定行直到事务结束。
sql
-- 启动事务
START TRANSACTION;

-- 锁定特定行
SELECT * FROM users WHERE id = 1 FOR UPDATE;

-- 执行更新操作
UPDATE users SET name = 'new_name' WHERE id = 1;

-- 提交事务释放锁
COMMIT;
  • 表级锁:通过LOCK TABLES语句实现,锁定整张表。
sql
-- 锁定表进行写操作
LOCK TABLES users WRITE;

-- 执行操作
UPDATE users SET name = 'new_name' WHERE id = 1;

-- 释放锁
UNLOCK TABLES;

关键点

  • 可能导致死锁,需要合理设计事务顺序

  • 会降低系统并发性能

  • 需要及时释放锁,避免长时间持有

  • 假设数据冲突概率高,提前锁定数据

  • 通过数据库锁机制实现

  • 适用于写多读少或数据竞争激烈的应用场景

  • 能确保数据一致性

  • 需要注意死锁问题

5.3 两种锁机制的对比

                对比项
                乐观锁
                悲观锁
            
            
                适用场景
                读多写少,冲突较少
                写多读少,冲突较多
            
            
                并发性能
                高
                低
            
            
                实现复杂度
                较高
                较低
            
            
                死锁风险
                低
                高
            
            
                优点
                提高并发性能,减少锁开销
                实现简单,数据一致性有保障
            
            
                缺点
                需要处理冲突,可能重试
                降低并发性能,可能死锁

最佳实践建议

  • 根据应用特点选择合适的锁机制:读多写少选乐观锁,写多读少选悲观锁

  • 在使用乐观锁时,要合理设计重试机制,避免无限重试

  • 在使用悲观锁时,要注意锁的粒度,尽量减少锁的范围

  • 对于金融系统等对数据一致性要求极高的场景,优先考虑悲观锁

  • 定期监控锁的使用情况,及时发现和解决性能瓶颈

推荐使用场景:

  • 乐观锁:适用于电商商品库存查询、用户信息查看等读多写少的场景

  • 悲观锁:适用于银行转账、订单处理等对数据一致性要求极高的场景

总结

数据库事务是保证数据一致性和完整性的核心机制。通过理解事务的ACID特性、隔离级别以及并发控制机制,我们可以更好地设计和优化数据库应用。在实际开发中,应根据具体业务需求选择合适的隔离级别和锁机制,平衡数据一致性和系统性能。

基于 VitePress 构建 | 技术知识库