fgedu.net.cn
目录
一、基础概念
1.1 事务定义
事务是一组原子性的SQL操作,要么全部执行成功,要么全部失败回滚。TiDB支持ACID特性:
- 原子性(Atomicity):事务作为一个整体执行,要么全部成功,要么全部失败
- 一致性(Consistency):事务执行前后数据库状态保持一致
- 隔离性(Isolation):不同事务之间相互隔离,互不影响
- 持久性(Durability):事务提交后,数据更改永久保存
1.2 隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 描述 |
|---|---|---|---|---|
| READ UNCOMMITTED | √ | √ | √ | 允许读取未提交的数据 |
| READ COMMITTED | × | √ | √ | 只读取已提交的数据 |
| REPEATABLE READ | × | × | √ | 保证同一事务中多次读取结果一致 |
| SERIALIZABLE | × | × | × | 完全串行执行事务 |
1.3 并发控制机制
- 乐观并发控制(OCC):TiDB默认使用的并发控制机制,通过MVCC和冲突检测实现
- 悲观并发控制(PCC):通过锁机制实现,适用于高冲突场景
- MVCC(多版本并发控制):为每个数据版本维护多个快照,实现无锁读
二、规划建议
2.1 事务设计原则
- 保持事务简短:尽量减少事务持续时间,避免长时间占用锁资源
- 避免大事务:大事务会增加锁竞争和回滚成本
- 合理设置隔离级别:根据业务需求选择合适的隔离级别
- 避免死锁:合理安排操作顺序,避免循环依赖
- 使用批量操作:对于大量数据操作,考虑使用批量处理
2.2 隔离级别选择
- READ COMMITTED:适用于对一致性要求不高的场景,如报表查询
- REPEATABLE READ:TiDB默认隔离级别,适用于大多数业务场景
- SERIALIZABLE:适用于对一致性要求极高的场景,如金融交易
2.3 并发控制策略
- 乐观锁:适用于低冲突场景,并发性能高
- 悲观锁:适用于高冲突场景,保证数据一致性
- 混合锁:根据业务场景灵活选择
风哥提示:
三、实施方案
3.1 事务操作
开始事务
START TRANSACTION;
提交事务
COMMIT;
回滚事务
ROLLBACK;
3.2 隔离级别设置
设置全局隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
设置会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
设置事务隔离级别
START TRANSACTION ISOLATION LEVEL SERIALIZABLE;
3.3 锁机制
悲观锁
-- 使用FOR UPDATE获取行级锁
START TRANSACTION;
SELECT * FROM fgedudb.fgedu_orders WHERE id = 1 FOR UPDATE;
UPDATE fgedudb.fgedu_orders SET status = 2 WHERE id = 1;
COMMIT;
乐观锁
-- 使用版本号实现乐观锁
START TRANSACTION;
SELECT id, status, version FROM fgedudb.fgedu_orders WHERE id = 1;
-- 应用程序检查版本号
UPDATE fgedudb.fgedu_orders SET status = 2, version = version + 1 WHERE id = 1 AND version = 1;
-- 检查影响行数
IF ROW_COUNT() = 0 THEN
ROLLBACK;
-- 处理冲突
ELSE
COMMIT;
END IF;
3.4 死锁处理
查看死锁信息
SHOW ENGINE INNODB STATUS;
===================================== 2024-04-09 10:00:00 0x7f1234567890 INNODB MONITOR OUTPUT ===================================== ... LATEST DETECTED DEADLOCK ------------------------ 2024-04-09 10:00:00 0x7f1234567890 *** (1) TRANSACTION: TRANSACTION 12345, ACTIVE 1 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s) MySQL thread id 10, OS thread handle 1397134567890, query id 12345 192.168.1.10 root updating UPDATE fgedudb.fgedu_orders SET status = 2 WHERE id = 1 *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 123 page no 4 n bits 72 index PRIMARY of table `fgedudb`.`fgedu_orders` trx id 12345 lock_mode X locks rec but not gap waiting Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0 ... *** (2) TRANSACTION: TRANSACTION 12346, ACTIVE 2 sec starting index read mysql tables in use 1, locked 1 2 lock struct(s), heap size 1136, 1 row lock(s) MySQL thread id 11, OS thread handle 1397134567900, query id 12346 192.168.1.10 root updating UPDATE fgedudb.fgedu_orders SET status = 3 WHERE id = 2 *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 123 page no 4 n bits 72 index PRIMARY of table `fgedudb`.`fgedu_orders` trx id 12346 lock_mode X locks rec but not gap Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0 ... *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 123 page no 5 n bits 72 index PRIMARY of table `fgedudb`.`fgedu_orders` trx id 12346 lock_mode X locks rec but not gap waiting Record lock, heap no 3 PHYSICAL RECORD: n_fields 6; compact format; info bits 0 ... *** WE ROLL BACK TRANSACTION (1)
学习交流加群风哥QQ113257174
避免死锁的方法
-- 方法1:按相同顺序访问资源
-- 事务1
START TRANSACTION;
UPDATE fgedudb.fgedu_orders SET status = 2 WHERE id = 1;
UPDATE fgedudb.fgedu_orders SET status = 3 WHERE id = 2;
COMMIT;
-- 事务2
START TRANSACTION;
UPDATE fgedudb.fgedu_orders SET status = 2 WHERE id = 1;
UPDATE fgedudb.fgedu_orders SET status = 3 WHERE id = 2;
COMMIT;
-- 方法2:使用超时机制
SET SESSION innodb_lock_wait_timeout = 30;
-- 方法3:使用悲观锁
START TRANSACTION;
SELECT * FROM fgedudb.fgedu_orders WHERE id IN (1, 2) FOR UPDATE;
UPDATE fgedudb.fgedu_orders SET status = 2 WHERE id = 1;
UPDATE fgedudb.fgedu_orders SET status = 3 WHERE id = 2;
COMMIT;
四、实战案例
4.1 电商订单处理
场景:电商平台处理订单,需要保证订单状态的一致性和并发安全。
步骤1:创建订单表
CREATE TABLE fgedudb.fgedu_orders (
id BIGINT PRIMARY KEY,
order_no VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL,
total_amount DECIMAL(12,2) NOT NULL,
status TINYINT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
);
步骤2:处理订单状态更新
-- 使用悲观锁处理订单状态更新
START TRANSACTION;
-- 锁定订单记录
SELECT * FROM fgedudb.fgedu_orders WHERE id = ? FOR UPDATE;
-- 检查订单状态
IF (SELECT status FROM fgedudb.fgedu_orders WHERE id = ?) = 0 THEN
-- 更新订单状态为已支付
UPDATE fgedudb.fgedu_orders SET status = 1, updated_at = NOW() WHERE id = ?;
-- 记录支付日志
INSERT INTO fgedudb.fgedu_payment_logs (order_id, amount, payment_method, created_at) VALUES (?, ?, ?, NOW());
COMMIT;
SELECT 'success' AS result;
ELSE
ROLLBACK;
SELECT 'failed' AS result, '订单状态错误' AS message;
END IF;
步骤3:并发测试
-- 会话1
START TRANSACTION;
SELECT * FROM fgedudb.fgedu_orders WHERE id = 1 FOR UPDATE;
-- 模拟处理时间
DO SLEEP(5);
UPDATE fgedudb.fgedu_orders SET status = 1 WHERE id = 1;
COMMIT;
-- 会话2(在会话1执行后立即执行)
START TRANSACTION;
SELECT * FROM fgedudb.fgedu_orders WHERE id = 1 FOR UPDATE;
-- 这里会等待会话1释放锁
UPDATE fgedudb.fgedu_orders SET status = 1 WHERE id = 1;
COMMIT;
4.2 库存管理
场景:电商平台库存管理,需要保证库存数据的一致性。
步骤1:创建库存表
CREATE TABLE fgedudb.fgedu_inventory (
id BIGINT PRIMARY KEY,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
version INT NOT NULL DEFAULT 1,
updated_at DATETIME NOT NULL
);
步骤2:使用乐观锁更新库存
-- 乐观锁更新库存
START TRANSACTION;
-- 获取当前库存和版本号
SELECT quantity, version FROM fgedudb.fgedu_inventory WHERE product_id = ?;
-- 检查库存是否足够
IF quantity >= ? THEN
-- 更新库存,使用版本号作为乐观锁
UPDATE fgedudb.fgedu_inventory SET
quantity = quantity - ?,
version = version + 1,
updated_at = NOW()
WHERE product_id = ? AND version = ?;
-- 检查更新是否成功
IF ROW_COUNT() = 1 THEN
-- 记录库存变动
INSERT INTO fgedudb.fgedu_inventory_logs (product_id, quantity_change, type, created_at) VALUES (?, -?, 'out', NOW());
COMMIT;
SELECT 'success' AS result;
ELSE
ROLLBACK;
SELECT 'failed' AS result, '库存更新冲突' AS message;
END IF;
ELSE
ROLLBACK;
SELECT 'failed' AS result, '库存不足' AS message;
END IF;
步骤3:高并发测试
-- 模拟高并发场景
-- 会话1
START TRANSACTION;
SELECT quantity, version FROM fgedudb.fgedu_inventory WHERE product_id = 1;
-- 模拟处理时间
DO SLEEP(2);
UPDATE fgedudb.fgedu_inventory SET quantity = quantity - 1, version = version + 1 WHERE product_id = 1 AND version = 1;
COMMIT;
-- 会话2(在会话1执行后立即执行)
START TRANSACTION;
SELECT quantity, version FROM fgedudb.fgedu_inventory WHERE product_id = 1;
-- 此时获取到的版本号仍然是1
UPDATE fgedudb.fgedu_inventory SET quantity = quantity - 1, version = version + 1 WHERE product_id = 1 AND version = 1;
-- 由于版本号不匹配,更新失败
COMMIT;
五、经验总结
5.1 事务管理最佳实践
- 保持事务简短:尽量减少事务持续时间,避免长时间占用锁资源
- 避免大事务:将大事务拆分为多个小事务
- 合理设置隔离级别:根据业务需求选择合适的隔离级别
- 使用批量操作:对于大量数据操作,使用批量处理减少事务次数
- 定期提交:避免长时间未提交的事务
5.2 并发控制最佳实践
- 选择合适的锁机制:低冲突场景使用乐观锁,高冲突场景使用悲观锁
- 避免死锁:按相同顺序访问资源,设置合理的锁超时
- 使用索引:确保锁操作能够快速定位到目标记录
- 监控锁等待:定期检查锁等待情况,优化热点数据访问
- 使用分布式锁:对于跨节点的并发控制,使用分布式锁
5.3 常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 死锁 | 事务间循环等待锁资源 | 按相同顺序访问资源,设置锁超时 |
| 锁等待超时 | 锁竞争激烈或事务执行时间过长 | 优化事务逻辑,减少事务持续时间 |
| 乐观锁冲突 | 并发更新同一记录 | 实现重试机制,或使用悲观锁 |
| 大事务性能问题 | 事务操作数据量过大 | 拆分为小事务,使用批量处理 |
| 隔离级别导致的问题 | 隔离级别设置不当 | 根据业务需求选择合适的隔离级别 |
5.4 性能优化建议
- 使用索引:确保WHERE子句中的列有索引,减少锁范围
- 避免全表扫描:全表扫描会导致锁表,影响并发性能
- 使用分区表:将数据分散到多个分区,减少锁竞争
- 优化SQL语句:减少事务中的复杂查询
- 监控事务性能:定期分析事务执行时间和锁等待情况
- 合理使用连接池:控制并发连接数,避免连接耗尽
更多视频教程www.fgedu.net.cn
© 2024 TiDB数据库培训文档
本文由风哥教程整理发布,仅用于学习测试使用,转载注明出处:http://www.fgedu.net.cn/10327.html
