Read Commited 和 Repeatable Read 隔离级别在通常的使用场景中感知并不明显,通常生产环境大部分使用RC的事务隔离级别,那么RC和RR到底使用起来需要注意什么问题呢?幻读和不可重复读在生产环境又会出现什么样的问题呢?本文通过一个集体的例子,一文完成深入理解。
实现准备,需要自己本地搭建一个MySQL数据库,本文讨论基于Innodb引擎。
表创建语句:
CREATE TABLE `order_discount` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '修改时间',
`order_id` varchar(32) NOT NULL COMMENT '订单号',
`discount_id` varchar(50) NOT NULL COMMENT '优惠ID',
`yn` smallint(6) NOT NULL COMMENT '默认值,有效;1:无效,已删除',
`batch_no` smallint(6) NOT NULL DEFAULT '0' COMMENT '批次号',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_id_discount_id_batch_no` (`order_id`,`discount_id`,`batch_no`),
KEY `idx_order_id_yn` (`order_id`,`yn`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='优惠表';
业务场景:
优惠表(存储订单关联的优惠信息),batch_no 是一个由业务控制的递增批次号,一笔订单只允许最高批次的优惠券作为有效优惠券存在,因此业务设计上对插入新批次优惠券和过期已存在优惠券两个操作开启事务。
因此事务中存在以下三个动作:
要实现这个动作可以这样做:
第一版代码:
start transaction;
--操作1
update order_discount set `yn` = 1 where order_id = '0001';
--操作2
SELECT max(batch_no) from order_discount where order_id = '0001';
-- batch = max(batch) + 1
--操作3
INSERT INTO order_discount (order_id,discount_id,`discount_type`, batch_no, yn, `gmt_create`, `gmt_modified` ) VALUES ('0001','101',0, 1, 0, now(), now());
commit;
以上代码的问题是,当数据库中不存才order_id = 0001 的数据时,
操作1 并无法锁住(RC无gap-lock),在RC的事务隔离级别下操作3对其他事务的操作2 是可见的,这样表设计的唯一索引也失效了,导致了同一个订单号,两个有效批次的插入。
(RR事务隔离级别下不存在这个问题,具体原因大家可以自己做实验,有问题可以找我探讨)
为了修复第一版的问题我们进行了如下修正:
第二版:
start transaction;
--操作1
SELECT max(batch_no) from order_discount where order_id = '0001';
-- batch = max(batch) + 1
--操作2
update order_discount set `yn` = 1 where order_id = '0001';
--操作3
INSERT INTO order_discount (order_id,discount_id,`discount_type`, batch_no, yn, `gmt_create`, `gmt_modified` ) VALUES ('0001','101',0, 1, 0, now(), now());
commit;
这个版本是RC事务中通常的操作顺序即:“读 - 写 - 写”,可以避免第一版中“写 - 读 - 写”的可见性问题,但是问题仍然是存在的,即并发写入场景会由于唯一索引冲突引发事务回滚,那么有没有更好的写法呢?
留作思考吧。
以上脚本,我都做过实验可以跑通,大家可以在本地启动mysql,开启两个查询界面模拟事务并发即可复现。
总结一下,对于RC事务隔离级别会出现幻读和不可重复读的问题,通常在RC事务中应当遵循“读 - 写 - 写”的执行顺序,但是基于业务场景的分析通常可以做到更好的性能提升。
附加命令:
查询数据库事务隔离级别:
select @@global.tx_isolation;
设置事务隔离级别:
set global transaction isolation level read COMMITTED;