看完上一个章节,相信你已经充分的掌握代理的套路,猿人工厂君也知道,内容对于新手而言,理解起来还是比较很吃力的,文中提到的其他方式,大家可以去尝试实现,猿人工厂君就不一一赘述了。今天我们将开启一个新的方向,让大家思考一些新的问题。不过上一章节涉及编译原理、类加载机制和一点点 jvm 的知识,很重要,请务必掌握其中的过程和概念。
猿思考是一个原创系列文章,帮助你从一个小白快速掌握基础知识,很多基础知识,在于思考的变通,更多精彩内容,敬请大家关注公主号 猿人工厂 ,点击 猿人养成获取!
数据库事务:一般来讲是指数据库访问逻辑中一个最小的不可再分的工作单元;是一组对数据库的相对完整的逻辑单元操作。一个数据库事务,可以是一条 SQL 语句,也可以是一组 SQL 语句。
我们来看一个例子,比如,账户 A 向账户 B 转账 100 元,那么账户 A 必须减少 100 元,而账户 B 必须增加 100 元。这个操作是不可能分开的,是一个原子操作。账户 A 必须减少 100 元而账户 B 必须增加 100 元,不能出现 A 减少 100 元,B 没有增加 100 元,也不能出现 A 没有减少 100 元,B 增加了 100 元。操作必须是同时成功的或者同时失败的,必须是一致的。A 和 B 的操作,是不能受到其他事务的干扰的从而影响结果的正确执行,并发的事务之间是需要隔离起来的,而且这个操作进行之后,A 账户,和 B 账户的操作结果不可改变,必须把操作数据记录到磁盘,这是一个持久状态。
以上就是数据库事务必须具备的四大特性。原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability),俗称 ACID 四大特性或四大原则。
关于 MYSQL 事务的操作有几个语句你也要了解一下:
开启事务:starttransaction;(开启显示提交事务)
提交事务:commit;(提交事务,执行后,数据固化到磁盘,不可改变)
回滚事务:rollback;(回滚事务,撤销之前的 DML 语句操作)
在很多人的观念里,提到数据事务就是 ACID 四大原则,但是这四大原则是由谁来保障的呢?答案当然是数据库了。比如 MYSQL 数据库,你用 MYISAM 引擎的表搞搞事务试试?所以,要做数据库事务得有一个大前提,数据库必须是支持事务的。
如果使用的是 INNODB 引擎的表, 默认情况下是开启了事务自动提交的,在不使用 start transation 语句的情况下,任何一个 insert update 语句都是自动提交的,另外,insert update 语句都是一个原子级操作噢,总不能写一条数据,或者修改一条数据还存在,只修改一半的情况吧?这一点先了解下,以后的学习中自由妙用。
额,这个问题看上去有点难,我们先看看原子性——要么执行,要么全部不执行。操作多条语句,思考起来太复杂了,来简单点而的。比如我们要修改用户 ID 为 1 的电话好嘛为 13888888888:
update travel_user set travel_user_phone=’13888888888’where travel_user_id=1;
我们先考虑执行失败怎么办?执行失败了,自然需要保证数据恢复原样。那么要干的第一件事情,自然是记录原始数据了,有了原始记录,在之后的修改过程中,发生了任何问题,都可以恢复原样。那么第二件事情,自然是修改记录了,如果修改失败了,根据原始记录恢复数据就好了。第三件事情,是将修改的记录写到磁盘了,如果失败,有原始记录,恢复就好,如果成功,操作就完成了,事务也成功完成了。
想想看,这个原始记录需要记录在哪里?白纸黑字的东西才会让人放心,自然是文件了。Mysql 提供了一个叫 redo log 的家伙来做这个事情。
MYSQL 当然不会这么傻啦,肯定先写到内存嘛,内存操作就快了,内存写差不多了,单独起一个线程来异步把内存里的数据往磁盘上写就好了。当然,数据在内存中放时间长了,万一机房断电,数据就没有了,这个风险嘛,交给用户去配置,让他自己决定多长时间刷新到磁盘持久就好了。当然,正向操作要记录,反向操作的也要记录啦,比如记录了一条 insert, 必然记录一条 delete 用于数据恢复,就叫 undo log 好了。
当然考虑到其他问题,比如集群下的数据同步,MYSQL 还提供了一个叫 binlog 的东西用于记录数据的逻辑操作——执行的 SQL 语句。这样你修改主节点的数据,只需要主节点把 binlog 的语句发到其他节点执行就好了噢。
那么新的问题又来了,redo log 的内容必须是和 binlog 是一致的,要不就乱套了噢。怎么来解决呢?
这个好办,先写 redo log,如果写失败了,那我们就认为这次事务有问题,回滚,不再写 binlog。如果 redo log 写成功了,写 binlog 写一半失败了, 事务回滚嘛,然后删除无效的 binlog 就好了(不删除,被同步就麻烦了)。如果写 redo log 和 binlog 都成功了,事务完成,加个标记表示成功嘛。这个过程还有个学名——“二阶段提交”。
prepare:redo log 刷新到磁盘,事务进入 prepare 状态。
commit::binlog 刷新到磁盘,事务进入 commit 状态,并打上 log_xid 标记。
在讨论这个话题之前,我们来看几个问题,假如有两个事务并发执行可能发生什么情况呢?一个事务把另一个事务的数据覆盖了,这个叫丢失更新问题。一个事务 A 读取了一条 d1,一个事务 B 写入一个数据,但是未提交,事务 A 再次查询,读到未提交的数据,这叫“脏读”。一个事务 A 读取了一条 d1,一个事务 B 修改了 d1,并提交事务,事务 A 再次读取 d1, 发现两条记录不一致,这就是虚读。一个事务 A 做了一次查询,事务 B 写入了数据 d2,并提交事务,事务 A 再做了一次查询(语句可以不同),发现查询结果包含了 d2, 这就叫幻读。虚读和幻读都叫“不可重复读”。
为了解决这些问题数据库定义了四种隔离级别:
读未提交:read uncommitted,事务 A 未提交的数据事务 B 也能读取到,允许脏读,但是不允许发生丢失更新的问题。
读已提交:read committed,事务 A 已提交的数据事务 B 才能读取到,不允许脏读,允许发生不可重复读。
可重复读:repeatable read,事务 A 提交之后的数据,事务 B 读取不到,但是,事务 B 可重复读,允许幻读,但不允许虚读,即同一条记录的情况。MYSQL 的默认级别就是 repeatable read。
串行化:serializable,事务排队执行,一个事务执行完才能执行下一个,事务排队执行,性能可想而知,几乎无用武之地。
那 MYSQL 是怎么来保证事务的隔离级别的呢?
其实 MYSQL 在设计上比较鸡贼啦,每次开启事务都会给事务编个号,然后每一行记录都包含了三个隐藏列,DATA_TRX_ID 记录最后一个事务的 ID,DB_ROLL_PTR 记录当前记录的 redo log 信息,DB_ROW_ID 用于标记新插入数据的行 ID。
这下好了增删改查操作按下面的来就行了:
select:
读取创建版本小于或等于当前事务版本号,并且删除版本为空或大于当前事务版本的记录。用于保证在读取之前记录都是存在的。
insert:
将当前事务的版本号保存至行的DATA_TRX_ID。
update:
新插入一行,并以当前事务版本号作为 DATA_TRX_ID,同时将原记录行的DATA_TRX_ID 设置为当前事务版本号
delete:
将当前事务版本号保存至行的DATA_TRX_ID。
MYSQL 会把已开始未提交的事务保存在一个叫做 trx_sys 的事务链表中。同时还搞了一个叫做 ReadView 的东西。ReadView 包含了几个很重要的信息。ReadView{low_trx_id,up_trx_id, trx_ids},low_trx_id 表示务链表中最大的事务 id 编号,up_trx_id 表事务链表中最小的事务 id 编号,trx_ids 表示所有事务链表中事务的 id 集合。ReadView 在什么时候创建呢?这个就和隔离级别有关了。如果隔离级别是READ COMMITTED,那么每次读取数据前都生成一个 ReadView, 如果隔离级别是REPEATABLE READ,那么在第一次读取数据时生成一个 ReadView。
那么判断事务是否可见这个问题就好办了:
所有数据行上 DATA_TRX_ID 小于 up_trx_id 的记录,说明修改该行的事务在当前事务开启之前都已经提交完成,所以对当前事务来说,都是可见的。而对于 DATA_TRX_ID 大于 low_trx_id 的记录,说明修改该行记录的事务在当前事务之后,所以对于当前事务来说是不可见的。
至于位于(up_trx_id, low_trx_id)中间的事务是否可见,这个需要根据不同的事务隔离级别来确定。对于 READ COMMITTED 的事务隔离级别来说,对于事务执行过程中,已经提交的事务的数据,对当前事务是可见的;而对于 REPEATABLE READ 隔离级别来说,事务启动时,已经开始的事务链表中的事务的所有修改都是不可见的,所以在 REPEATABLE READ 级别下,low_trx_id 基本保持与 up_trx_id 相同的值即可。