数据库事务隔离级别

1. 三种读取问题

三种数据读取问题出现的根本原因:并发访问

1.1 脏读

读取未提交数据

A事务读取B事务尚未提交的数据,此时如果B事务发生错误并执行回滚操作,那么A事务读取到的数据就是脏数据。
就好像原本的数据是艾希 比较干净、纯粹。B事务更改艾希为盖伦,A事务在之后读取了这个数据(盖伦)。然后B又把数据回滚为艾希,而事务A却什么都不知道,傻傻的以为盖伦就是正确的值。最终结果就是事务A读取了此次的脏数据,称为脏读。

1.2 不可重复读

前后多次读取,数据内容不一致

A事务在执行读取操作,且耗时较长。事务A第一次读取数据时,读到库中值为 艾希,此时事务B执行更改操作,将值改为 盖伦 后提交。之后事务A第二次读取到库中值时,发现和之前的数据不一样了(第一次读到艾希,第二次却成了盖伦)。即系统不可以读取到重复的数据了,称为不可重复读。

1.3 幻读

前后多次读取,数据总量不一致

A事务在执行读取操作,需要两次统计库中艾希的总数量。第一次查询库中艾希数据总量为1,此时事务B执行了新增100个艾希的操作并提交。事务A第二次再去读取时发现艾希的数据总量变成了101。这和之前统计的不一样,就像产生了幻觉一样,按相同的查询条件检索却平白无故的多了几条数据,称为幻读。

2. 事务隔离级别

隔离级别脏读不可重复读幻读默认数据库
Read uncommitted 读未提交×××
Read committed 读已提交××Oracle、SQL Server…
Repeatable read 可重复读×MySQL
Serializable 可串行化

3. 数据库锁机制

3.1 MyISAM与InnoDB(表锁与行锁)

Mysql 在5.5之前默认使用 MyISAM 存储引擎,之后使用 InnoDB 。
MyISAM : 使用的是表锁。更新一条记录就要锁整个表,导致性能较低,并发不高。当然同时也不会存在死锁问题
InnoDB: 采用了行锁且支持事务(MyISAM不支持事务)

在 Mysql 中,行级锁并不是直接锁记录,而是锁索引
索引分为 主键索引 和 非主键索引 两种,如果一条sql 语句操作了主键索引,Mysql 就会锁定这条主键索引;如果一条语句操作了非主键索引,MySQL会先锁定该非主键索引,再锁定相关的主键索引。
InnoDB 行锁是通过给索引项加锁实现的,如果没有索引,InnoDB 会通过隐藏的聚簇索引来对记录加锁。也就是说:如果不通过索引条件检索数据,那么InnoDB将对表中所有数据加锁,实际效果跟表锁一样。因为没有了索引,找到某一条记录就得扫描全表,要扫描全表,就得锁定表。

3.2 共享锁与排他锁

共享锁:又称 读锁 或 S锁
对某一资源加共享锁,自身可以读该资源,其他人也可以读该资源(可以共存多个共享锁),但无法修改。要想修改就必须等所有共享锁都释放完之后。
排他锁:又称 写锁 或 X锁
对某一资源加排他锁,自身可以进行增删改查,其他人无法进行任何操作。

数据库默认:增删改操作默认都会加排他锁,而查询不会加任何锁。

注意:数据库规定同一资源上不能同时共存共享锁和排他锁

T1:select * from table lock in share mode

T2:select * from table lock in share mode

T3:update table set column1='hello'

T2 不用等 T1 运行完就能运行,T3 却要等 T1 和 T2 都运行完才能运行。因为 T3 必须等 T1 和 T2 的共享锁全部释放才能进行加排他锁然后执行 update 操作。

3.3 死锁

3.3.1 死锁的产生

T1:  select * from table lock in share mode
     update table set column1='hello'

T2:	 select * from table lock in share mode
     update table set column1='world'

假设 T1 和 T2 同时达到 select,T1 对 table 加共享锁,T2 也对 table 加共享锁。
当 T1 的 select 执行完,准备执行 update 时,根据锁机制,T1 的共享锁需要升级到排他锁才能执行接下来的 update。在升级排他锁前,必须等 table 上的其它共享锁(T2)释放。
同理,T2 也在等 T1 的共享锁释放。于是死锁产生了。

另一种死锁:

T1:begin tran
     update table set column1='hello' where id=10;

T2:begin tran
     update table set column1='world' where id=20;

这种语句虽然最为常见,很多人觉得它有机会产生死锁,但实际上要看情况

1.	如果id是主键(默认有主键索引),那么T1会瞬间找到该条记录(id=10的记录),然后对该条记录加排他锁。T2同理,这样T1和T2各更新各的,互不影响。此时T2不需要等待。
2.	如果id是普通的一列,没有索引。那么当T1对id=10这一行加排他锁后,T2为了找到id=20,需要对全表扫描。
	但因为T1已经为一条记录加了排他锁,导致T2的全表扫描进行不下去(其实是因为T1加了排他锁,数据库默认会为该表加意向锁,T2要扫描全表,就得等该意向锁释放,也就是T1执行完成),就导致T2等待。

3.3.2 死锁的解决

3.3.2.1 直接加排他锁
T1:	select * from table(xlock) (xlock意思是直接对表加排他锁)
	update table set column1='hello'

T2:	select * from table(xlock)
	update table set column1='world'

这样,当T1的select 执行时,直接对表加上了排他锁,T2在执行select时,就需要等T1事物完全执行完才能执行。排除了死锁发生。
弊端:当第三个user过来想执行一个查询语句时,也因为排他锁的存在而不得不等待,第四个、第五个user也会因此而等待,会产生性能问题。
3.3.2.2 加更新锁

更新锁其实就可以看成排他锁的一种变形,只是它也允许其他人读(还允许加共享锁)。但不允许其他操作,除非我释放了更新锁
T1 执行 select,加更新锁。
T2 运行,准备加更新锁,但发现已经有一个更新锁在那儿了,只好等。
当后来有 user3、user4…需要查询 table 表中的数据时,并不会因为 T1 的 select 在执行就被阻塞,照样能查询,相比起上例,这提高了效率。

4. 悲观锁与乐观锁

首先说明,悲观锁和乐观锁都是针对读(select)来说的。只要加锁了就属于悲观策略。

4.1 悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,用完了才放。
悲观锁适用于多写的场景下。

4.2 乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁。但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。
乐观锁适用于写比较少的情况下(多读场景),这样可以提高吞吐量

4.2.1 版本号机制

在数据表中加上一个数据版本号version字段,表示数据被修改的次数。当数据被修改时,version值会加1
当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。(阿里开发手册要求重试操作至少为3次)

账户余额:100元
version :1

A操作员:读取version == 1update 账户余额为 50;
B操作员:读取version == 1update 账户余额为 150;
A操作员:完成任务,开始确认version号与开始时读取的(1)是否一致,发现一致,提交事务。
		 此时由于提交的数据被更新,数据库记录 version 更新为 2;
B操作员:完成任务,却发现当前库中版本号(2)与自己开始读取版本号(1)不一致,操作员 B 的提交被驳回, 重试更新操作。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值