type
status
date
slug
summary
tags
category
icon
password

事务隔离级别

什么是脏读?

事务A读取事务B尚未提交的数据,此时如果事务B发生错误并执行回滚操作,那么事务A读取到的数据就是脏数据。就好像原本的数据是正确的,此时由于事务B更改了它,由于“确认键”还未按下,此时数据的定值还未确定。这个时候事务A立即读取了这个脏数据,但事务B想了想又决定不更改了,又用回滚把数据恢复成原来的样子,而事务A却什么都不知道,最终结果就是事务A读取了此次的脏数据,称为脏读。
这种情况常发生于转账与取款操作中,如下所示。
脏写通常就是在脏读的基础上添加了写操作。

不可重复读和脏读有什么区别?

脏读是某一事务读取了另一个事务未提交的脏数据.
不可重复读则是读取了前一事务已提交的数据。

什么是不可重复读?(前后多次读取,数据内容不一致)

事务A在执行读取操作,由于整个事务A比较大,前后读取同一条数据需要经历很长的时间。而在事务A第一次读取数据时,比如读取了小明的年龄为20岁,此时事务B执行更改操作,将小明的年龄更改为30岁,这时事务A第二次读取到小明的年龄,发现其年龄是30岁,和之前的数据不一样了,也就是数据不重复了。同一个事务下不可以(无法)读取到重复的数据,成为不可重复读(无法重复读)。如下所示。

什么是幻读?(前后多次读取,数据总量不一致)

事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,这个时候事务A读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,成为幻读。

不可重复读和幻读有什么区别?

  1. 不可重复读是读取了其他事务更改的数据,针对update操作
    1. 解决:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。
  1. 幻读是读取了其他事务新增的数据,针对insert和delete操作
    1. 解决:使用表级锁,锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。

事务的四大特性

  1. 原子性:事务包含的所有数据库操作要么全部成功,要不全部失败回滚
  1. 一致性:一个事务执行之前和执行之后都必须处于一致性状态。拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
  1. 隔离性:一个事务未提交的业务结果是否对于其它事务可见。级别一般有:read_uncommit,read_commit,read_repeatable,Serializable 串行化访问
  1. 持久性:一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。

数据库的隔离级别

数据库事务的隔离级别有4个,由低到高依次为Read uncommitted 、Read committed 、Repeatable read 、Serializable ,这四个级别可以逐个解决脏读 、不可重复读 、幻读 这几类问题。
MySQL隔离级别
脏读
不可重复读
幻读
加锁读
读未提交
不加锁
读已提交
x
不加锁
可重复读
x
x
不加锁
可串行化
x
x
x
加锁
注:√代表会出现的问题
  1. READ UNCOMMITTED(读未提交)
      • 这是最低的隔离级别,事务可以读取未提交的数据(脏读)。
      • 可能发生的现象:
        • 脏读:一个事务读取了另一个事务尚未提交的数据。如果后者回滚,该数据将无效,但前者已经使用了这个数据。
        • 不可重复读:同一查询在一个事务中读取两次时,可能会获取到不同的结果。
        • 幻读:同一个查询在不同时间内可能会看到不同的行集。
      • 示例:
        • 公司发工资了,领导把5000元打到我的账号上,但是该事务并未提交,而我正好去查看账户,发现工资已经到账,是5000元整,我以为老板良心发现给我加工资了非常高兴。可是不幸的是,领导发现发给我的工资金额不对,应当是2000元,于是迅速回滚了事务,修改金额后确认无误,再将事务提交,最后我实际的工资只有2000元,空欢喜一场。
          出现上述情况,即我们所说的脏读,两个并发的事务,“事务A:领导给我发工资”、“事务B:我查询工资账户”,事务B读取了事务A尚未提交的数据。
          当隔离级别设置为Read uncommitted 时,就可能出现脏读,如何避免脏读,请看下一个隔离级别。
  1. READ COMMITTED(读已提交)
      • 事务只能读取已提交的数据,避免了脏读
      • 可能发生的现象:
        • 不可重复读:在同一事务中,多次查询同一数据时,数据可能会发生变化。
        • 幻读:查询的结果集可能会在事务执行过程中被其他事务插入或删除行,导致结果集发生变化。
      • 示例:
        • 我拿着工资卡去消费,刷卡时系统读取到卡里确实有2000元,而此时花呗自动还款,把我工资卡的2000元全都还了花呗,并在我输完密码前之前提交了事务,当POS机扣款时,系统检查到我的工资卡已经没有钱,扣款失败,十分尴尬。
          出现上述情况,即我们所说的不可重复读 ,两个并发的事务,“事务A:消费”、“事务B:还款”,事务A事先读取了数据,事务B紧接了更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。虽然“再次读取数据”这件事可以避免脏读,但却无法避免不可重复读。
          当隔离级别设置为Read committed 时,避免了脏读,但是可能会造成不可重复读。如何解决不可重复读这一问题,请看下一个隔离级别。
  1. REPEATABLE READ(可重复读)
      • 事务中对数据的读操作,能够确保在事务执行期间,数据的一致性(即数据不会被其他事务修改)。这种隔离级别避免了脏读不可重复读
      • 实现方式是快照,将整个库的的数据创建一个快照,就像用一个相机全都拍下来,此时同一个事物下的任何查询都是从快照中读取。
      • 可能发生的现象:
        • 幻读:虽然事务中的数据不会被修改,但其他事务可能插入或删除数据,导致查询结果不一致。
      • 示例:
        • 当隔离级别设置为Repeatable read 时,可以避免不可重复读。当我拿着工资卡去消费时,一旦系统开始读取工资卡信息(即事务开始),花呗就不可能对该记录进行修改,也就是花呗不能还款。
          虽然Repeatable read避免了不可重复读,但还有可能出现幻读
          我闲来无事,去银行打印工资卡的消费记录,此时银行柜台的电脑显示当月工资卡的总消费金额 (select sum(amount) from transaction where month = 本月)为80元,而此时我的花呗自动还款2000元,即新增了一条2000元的消费记录(insert transaction ... ),并提交了事务,随后柜员将我当月信用卡消费的明细打印到A4纸上,我拿到手后发现消费总额为2080元,我很很诧异,才过去几秒钟怎么多出了两千元的扣款,以为出现了幻觉,幻读就这样产生了。
          注:Mysql的默认隔离级别就是Repeatable read。
          幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),不同的是不可重复读查询的都是同一个数据项,或者也可以说行数据,而幻读针对的是一批数据整体(比如数据的个数),也可以说表数据。
      • 但你可能还有一个问题:假如事务A读取数据的值为0,在此基础上加1等于1,但是并未提交;此时事务B读取数据的值为0,并加2等于2,之后事务A提交,紧接着事务B提交,在这种情况下,事务A的结果岂不是丢失了吗?
        • 可重复读隔离级别 下,虽然事务A在读取数据时确保了数据的稳定性(不会被其他事务修改),但是它并没有考虑到 事务并发冲突的情况。事务A看到的数据是事务A开始时的一致快照,并且不能看到事务B对数据的修改。这就是事务A的“结果”似乎被事务B的提交所“覆盖”了。为了避免这种情况,可以考虑使用悲观锁(如 SELECT ... FOR UPDATE)来显式锁定数据,直到事务A提交。这会防止事务B对数据进行修改,直到事务A完成;又或者下面的串行化隔离级别;又或者在后端应用层面限制并发问题。
  1. SERIALIZABLE(可串行化)
      • 这是最高的隔离级别,要求事务完全串行化执行。即使是查询也会被阻塞,确保在任何时刻,只有一个事务能够访问数据,完全避免了脏读、不可重复读和幻读
      • 这种级别的性能开销较大,但它保证了事务之间的完全隔离。

悲观锁和乐观锁(从策略上划分)

悲观锁:
顾名思义,就是很悲观,每次去拿数据的时候都认为别人会对数据进行修改,所以每次读数据的时候都会上锁。因此,在整个数据处理过程中,将数据处于锁定状态,直到取出数据。
悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据),以保证操作最大程度的独占性。
但随之而来的是各种开销。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁:
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。
而乐观锁机制在一定程度上解决了这个问题。乐观锁就如同他的名字一样,非常了乐观,每次去读数据都认为其它事务没有在写数据,总是认为别人不会修改数据,所以就不上锁,大多是基于数据版本( Version )记录机制实现,只有在线程提交数据时会通过检查版本号的形式检测数据有没有被修改过。。
何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。
此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
其实乐观锁相当于一种检测冲突的手段,可通过为记录添加版本或添加时间戳来实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
**注意:**首先明确一点乐观锁和悲观锁是一种编程策略,并不是数据库具有悲观锁和乐观锁。

共享锁和排它锁(从读写角度划分)

共享锁(S锁,Shared Lock)
也叫读锁(Read Lock),持有S锁的事务只读不可写。如 SELECT 语句。如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。
排它锁(X锁,Exclusive Lock)
也叫写锁(Write Lock),持有X锁的事务可读可写。用于数据修改操作,例如 INSERT、UPDATE 或 DELETE。确保不会同时同一资源进行多重更新。如果事务T对数据A加上排他锁后,则其他事务不能再对A加任何类型的封锁。直到T对A的锁解除。获准排他锁的事务既能读数据,又能修改数据。
**注意:**共享锁和排它锁是悲观锁的不同的实现,它俩都属于悲观锁的范畴。

表级锁和行级锁、页面锁(从粒度角度划分)
表级锁:表级锁将整个表加锁,性能开销最小。用户可以同时进行读操作。当一个用户对表进行写操作时,用户可以获得一个写锁,写锁禁止其他的用户读写操作。写锁比读锁的优先级更高,即使有读操作已排在队列中,一个被申请的写锁仍可以排在所队列的前列。开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
行级锁:行级锁仅对指定的记录进行加锁,这样其它进程可以对同一个表中的其它记录进行读写操作。开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
**页级锁:**开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
特点:
三种锁各有各的特点,若仅从锁的角度来说,表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如WEB应用;行级锁更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。

MVCC(多版本并发控制)

英文全称为Multi-Version Concurrency Control,乐观锁为理论基础的MVCC(多版本并发控制),MVCC的实现没有固定的规范。每个数据库都会有不同的实现方式。
notion image
MySQL中,默认的事务隔离级别是可重复读(repeatable-read),为了解决不可重复读,InnoDB采用了MVCC(多版本并发控制)来解决这一问题。
MVCC是利用在每条数据后面加了隐藏的两列(创建版本号和删除版本号),每个事务在开始的时候都会有一个递增的版本号,如下所示。
  • 新增:
    • id
      name
      age
      create_version
      delete_version
      1
      张三
      10
      1
  • 更新:
    • 更新操作采用delete+add的方式来实现,首先将当前数据标志为删除
      id
      name
      age
      create_version
      delete_version
      1
      张三
      10
      1
      2
      然后新增一条新的数据:
      id
      name
      age
      create_version
      delete_version
      1
      张三
      10
      1
      2
      1
      张三
      11
      2
      删除和添加在同一个事务中,所以版本号一致。
  • 删除:
    • 删除操作是直接将数据的删除版本号更新为当前事务的版本号
      id
      name
      age
      create_version
      delete_version
      1
      张三
      11
      2
      3
  • 查询操作:
    • 查询操作为了避免查询到旧数据或已经被其他事务更改过的数据,需要满足如下条件:
      1. 查询时当前事务的版本号需要大于或等于创建版本号,就是说记录创建是在事务中(等于的情况)或者事务启动之前。
      1. 查询时当前事务的版本号需要小于删除的版本号,就是说删除操作是在当前事务启动之后做的。
        1. 即:create_version <= current_version < delete_version
          这样就可以避免查询到其他事务修改的数据。
补充:
  1. MVCC手段只适用于MySQL隔离级别中的读已提交(Read committed)和可重复读(Repeatable Read)
  1. Read uncommitted 由于存在脏读,即能读到未提交事务的数据行,所以不适用MVCC。原因是MVCC的创建版本和删除版本要在事务提交后才会产生。
  1. Serializable 由于是会对所涉及到的表加锁,并非行锁,自然也就不存在行的版本控制问题。
  1. 通过以上总结,可知,MVCC主要作用于事务性的,有行锁控制的数据库模型。

关于删除操作

看到这里或许有人会有疑问:删除操作只是修改删除版本号,并没有真正删除数据,所以磁盘空间实际上没有被释放?
是的,数据虽然删了,但是空间并没有被释放,只是显示处于 free 状态,以后再写入新数据的时候就可以直接复用,而不需要在申请新的磁盘空间了。同时防止删除其中一行数据导致表的整个索引结构发生变化,带来巨大的磁盘IO。
释放磁盘空间需要重建表才可以,参考以下的操作:
  1. 执行 optimize table ${table_name}
  1. 如果是 InnoDB 的表,执行 alter table ${table_name} engine = innodb
需要注意以下问题:
  1. 这两个命令都会重建表,尽量不要在磁盘空间紧张(>90%)的时候进行操作,先扩容磁盘,操作完之后再缩容
  1. 这两个命令在开始和结束的时候都会尝试获取 metadata lock,所以尽量不要在业务高峰期执行
  1. MySQL官方建议不要经常(每小时或每天)进行碎片整理,一般根据实际情况,只需要每周或者每月整理一次即可(每月初凌晨4点清理MySQL所有实例下的表碎片)
notion image
JWT时间戳、timestamp和datetime、bigint
Loading...