数据库事务

事务基本概念

事务是数据库逻辑执行的基本单位。
什么是事务?
事务可以具象为一组连续的操作,比如生成订单和减库存、消费和账户扣费。

事务有什么特性?
原子性(Atomicity) 事务中的任何操作都是不可分割的。
独立性(Isolation) 事务的独立性,事务执行过程中互相不影响。
另外,事务的特性与一致性(Consistency) 、持久性(Durability)共同构成关系型数据库的基本属性。

显式事务

BEGIN 
<SQL语句>
COMMIT [ROLLBACK]

通常事务以BEGIN开始,以COMMIT或ROLLBACK结尾。
COMMIT代表事务提交,ROLLBACK代表事务回滚。

隐式事务
日常开发很少使用显式事务,事务的管理会上升到service层。
如果方法上不加@Translation注解,insert|update|delete语句都会默认隐式提交事务。

事务隔离级别

读未提交 Read UnCommitted
一个事务可以读取到另一个事务未提交的数据

读已提交 Read Committed
一个事务因为读取到另一个事务已提交的修改数据

可重复读 Repeatable Read
一个事务每次读取的值都是相同的

串行化Serializable
事务串行化执行

对应的问题

读未提交 | 脏读

TIMESession ASession B
T1BEGIN;
T2UPDATE USER SET NAME = “A” WHERE ID = 1;BEGIN;
T3SELECT NAME FROM USER WHERE ID = 1;
T4ROLLBACK;COMMIT;

Session B读取到Session A未提交的数据,Session A进行了回滚,这时Session B读取到的数据就是无效的,也就是脏数据。

读已提交 | 不可重复读

TIMESession ASession B
T1BEGIN;
T2BEGIN;SELECT NAME FROM USER WHERE ID = 1;
T3UPDATE USER SET NAME = “A” WHERE ID = 1;
T4COMMIT;
T5SELECT NAME FROM USER WHERE ID = 1;
T6COMMIT;

Session B读取同一行的两次结果不一致,无法确定以哪次结果作为判断条件。

读可重复读 | 幻读

TIMESession ASession B
T1BEGIN;
T2BEGIN;SELECT NAME FROM USER WHERE ID = 2;
T3INSERT INTO USER (ID,NAME) VALUE (2,“B”);
T4COMMIT;
T5SELECT NAME FROM USER WHERE ID = 2;
T6COMMIT;

与不可重复读有点类似,Session B读取的两次结果不一致,不同的是幻读是针对新增行记录的。

解决方案

MVCC,多版本的并发控制,Multi-Version Concurrency Control。
MVCC使得数据库读不会对数据加锁,普通的SELECT请求不会加锁,提高了数据库的并发处理能力。借助MVCC,数据库可以实现READ COMMITTED,REPEATABLE READ隔离级别,用户可以查看当前数据的前一个或者前几个历史版本,保证了ACID中的隔离性。

MVCC实现逻辑
InnoDB的MVCC是通过在每行记录后面保存两个隐藏的列来实现的。
一个保存了行的事务ID(DB_TRX_ID),一个保存了行的回滚指针(DB_ROLL_PT)。
每开始一个新的事务,都会自动递增产生一个新的事务id。
比如,执行了一条INSERT语句
在这里插入图片描述
第一次修改
在这里插入图片描述
当另一个事务第二次修改当前数据:
在这里插入图片描述
举例说明
初始化ID = 1,NAME = Mbappe

TIMESession 777Session 888Session 999
T1begin;
T2begin;begin;
T3UPDATE USER SET NAME = ‘CR7’ WHERE ID = 1;
T4
T5UPDATE USER SET NAME = ‘Messi’ WHERE ID = 1;SELECT * FROM USER where ID = 1;
T6commit;
T7UPDATE USER SET name = ‘Neymar’ WHERE ID = 1;
T8SELECT * FROM USER WHERE ID = 1;
T9UPDATE user SET name = ‘Dybala’ WHERE id = 1;
T10commit;
T11SELECT * FROM user where id = 1;

READ COMMITTED
每次读取数据前都生成一个ReadView (m_ids列表)
T5时
在这里插入图片描述
所以此时的活跃事务的ReadView的列表情况 m_ids:[777, 888] ,因此查询语句会根据当前版本链中小于m_ids中的最大的版本数据,即查询到的是 Mbappe。

T8时
在这里插入图片描述
因为当前的事务777已经提交,和事务888 未提交,所以此时的活跃事务的ReadView的列表情况 m_ids:[888] ,因此查询语句会根据当前版本链中小于 m_ids 中的最大的版本数据,即查询到的是Messi。

T11时
在这里插入图片描述此时 SELECT 语句执行,当前数据的版本链如上,因为当前的事务777和事务888 都已经提交,所以此时的活跃事务的ReadView的列表为空 ,因此查询语句会直接查询当前数据库最新数据,即查询到的是 Dybala。

REPEATABLE READ
在事务开始后第一次读取数据时生成一个ReadView(m_ids列表)
T5时
在这里插入图片描述
再当前执行select语句时生成一个ReadView,此时 m_ids 内容是:[777,888],所以但前根据ReadView可见版本查询到的数据为 Mbappe。

T8时
在这里插入图片描述
此时在当前的 Transaction 999 的事务里。由于T5的时间点已经生成了ReadView,所以再当前的事务中只会生成一次ReadView,所以此时依然沿用T5时的m_ids:[777,888],所以此时查询数据依然是 Mbappe。

T11时
在这里插入图片描述
此时情况跟T8完全一样。由于T5的时间点已经生成了ReadView,所以再当前的事务中只会生成一次ReadView,所以此时依然沿用T5时的m_ids:[777,888],所以此时查询数据依然是 Mbappe。

思考

拿实际项目来举例,有一个实时订单项目使用mysql的innodb引擎,这里我们没有使用内存缓存数据,生成订单时要随机分配一个座位。现在我们给生成订单的Service加上事务@Translation,再给查询座位和锁定加上原子操作符保证万无一失,当然这一步也可以通过行锁来完成。最后我们使用jmeter对接口进行1秒1000次的压力测试,结果却是每个座位分配了两次。为什么会出现这种情况?
在Java并发编程中提到如果多个线程修改同一个数据的时候,就会形成Read-Then-Modify竞态条件,如果线程修改数据时没有通知其他线程就会引起并发问题,这里的通知其他线程显然破坏了事务的隔离性。尤其我们在设计高并发系统时,通常会通过负载均衡将请求压力分散到多个后端服务上。在这种情况下,我们还应该遵循ACID原则么?
尽管我们给查询和锁定加上了原子操作符,但是mysql innodb引擎的默认隔离级别是读已提交,事务执行过程中读取不到其他事务的修改。如果我们把注解改成@Translation(isolation = READ_UNCOMMITTED)后在进行1000次并发测试,果然每个座位只分配了一次。但是这样操作就破坏事务的独立性,一个事务的执行依赖于和它同时执行的事务的结果,如果其他事务执行失败进行了回滚,那么当前事务获取到的值就是一个脏数据,显然这并不是一个合理的做法。

乐观锁

为了不降低系统性能我们常使用乐观锁来解决这个问题

UPDATE SEATS SET STATE = 1 WHERE STATE = 0

先进行座位锁定,当返回结果为1时,在进行创建订单操作,这样我们就可以在读已提交级别下解决重复分配的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值