mysql 如何实现事务的隔离级别:
mvcc :
MVCC的全称是“多版本并发控制”。这项技术使得InnoDB的事务隔离级别下执行一致性读操作有了保证,换言之,
就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值。这是一个可以用来增强并发性
的强大的技术,因为这样的一来的话查询就不用等待另一个事务释放锁。这项技术在数据库领域并不是普遍使用的。
一些其它的数据库产品,以及mysql其它的存储引擎并不支持它。
在没有mvcc机制的时候:如果a事务给数据加上了锁,其他所有读写请求只能排队。mvcc通过在不同的时间节点
构建不同的视图来支持多事务读操作。在通过innodb的undolog来实现的
undo log:
在数据修改的时候,不仅记录了redo,还记录了相对应的undo,如果因为某些原因导致事务失败或回滚了,可以借助该undo进行回滚。
undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条
对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。
undo log是采用段(segment)的方式来记录的,每个undo操作在记录的时候占用一个undo log segment。
另外,undo log也会产生redo log,因为undo log也要实现持久性保护。
行锁
1.两阶段锁:行锁是在更新操作下才会加,但是需要持续到事务结束
2.应该尽量减少锁的粒度,减少对锁的持有时间
3.应该提防事务大规模数据更新,容易形成死锁
4.大规模事务更新的时候应该将粒度拆分
可重复读
可重复读:在事务开启时候,读取所有的数据在整个事务期间是相同的,
视图是在事务提交后才失效
基于上面两个标题提出一个疑问
在rr 模式下,一致性读是否还有效果。如果同时两个事务修改一条数据一致性读很明显会让数据错乱?
解答:
首先,需要确认一点,上面问题具有迷惑性。一致性视图是用于读的,这里存在好几个概念:
1.可重复读
2.行锁
3,当前读
事务的可见性:
前面讲过:在事务开启的时候,读取所有数据再事务期间都是相同的,除非事务本身修改的内容,这就是
事务的可重复读。这是数据可见性的前提。
版本:
那么回归到我们的版本:innodb 的每条数据可以共存多个版本,是由row trx_id 这个确定的,这个对我们
来说是不可见的。一个trxid 对应的就是一个版本。 当多事务共同操作相同的数据的时候,可出现多个trxid,
每个trxid 之间存在一些联系 ,这就是undolog。假设一个账户数据 在多个事务运行同时存在:v1、v2、v3,
那么最新的版本数据就是v3,但是存在某个视图读取的肯定是v1,那么在一致性视图层面来讲 v1 =v3回滚+v2
回滚计算出来的,因为数据库不可能同时存在v1,v2,v3 三个版本的数据。v1,v2只能是临时及算出来的。
水位:
当一个事务启动的时候,会同时记录当前正在进行的所有事务,组成一个数组[ t1,t2,t3],
按照事务id顺序排列。数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过
的事务 ID 的最大值加 1 记为高水位。程序更多的是通过高低水位来判断某行应该取那
个时间段的数据。因为水位是不会变的,所以事务整体的视图是不会变的。
总结:
事务可见不可见可以通过:
版本未提交,不可见;
版本已提交,但是是在视图创建后提交的,不可见;
版本已提交,而且是在视图创建前提交的,可见。
总归回来好似讲了一番废话,但是却表述了一致性视图的原理
事务中的update:
案例:
某用户,同时收到3笔转账,第一笔+100,第二
笔+200,第三笔+300,如果按照数据修改是基于一致性视图来的,那么用户的钱只能
是最后一笔提交的。
反证法:
其实这里又要结合行锁来讲解了。反证法:假设事务中的修改基于一致性视图,如果多
个事务修改某一行数据,假设行数据读取的是一致性视图,那么数据将总是以最后修改
的数据为准。但是事实却不是这样的。
当前读:
事实上通过这个例子,我们就可以知道update 不是通过一致性视图来操作的。
“更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)”。
其实select 也可以使用这个: 加锁: share mode 或者 for update 两种锁都可以读取到
当前最新的数据。
一致性读+行锁:
按照案例解析,第一个进行事务开启转账的会获取到行锁,此时其他数据进行修改的事
务就会被停住,当然此时如果其他两个事务读的话也读取不到最新的余额。
举反例:这个例子是很多程序员喜欢写的,但是存在很大安全隐患:
给用户账户加500块,如果用户账户没有直接创建用户账户(例子够看就行)
record ,err:= db.query("select balance from user_account where user_id =10");
if err == db.ErrRecordNotExist {
// 给用户创建账户
....
}else{
new_balance = balance +500
db.query("update user_account set balance =new_balance where user_id= 10")
...
}
其实这个很多场景都能遇到,之前公司业务没考虑到,很多逻辑代码也是这么操作的
之前写php的时候更多都是这么操作的。其实整体逻辑都是有漏洞的,这类逻辑应该在
用户创建的时候就应该使用服务进行相关数据插入。修改数据尽可能使用for update,
或者直接在语句上修改:set balance=balance+500。不然容易出现数据错乱的bug,还
比较难以排查。