MySQL事物的特性&隔离级别

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

不忘初心,方得始终。


提示:以下是本篇文章正文内容,下面案例可供参考

一、事物四大特性

事物的四大特性分别是:原子性、一致性、隔离性、持久性

1.原子性

原子性是指事物包含的所有操作要么全部成功,要么全部失败。因此事物操作如果成功,就必须要完全应用到DB,如果操作失败不能对DB有任何影响。

2.一致性

一致性是指事物必须使DB从一个一致性状态转换到另一个一致性状态,也就时说一个事物执行之前和执行之后必须处于一致性状态。举个栗子,假设用户Dregon和用户Pig两者的钱加起来一共是1000,name不管Dregon和Pig之间如何转账,转几次账,事物结束后,用户的钱加起来应该还是1000,这就是事物的一致性。

3.隔离性

隔离性是指当多个用户并发访问DB时,假如同时操作一张表。DB会为每一个用户开启的事物,不能被其他事物的操作干扰,多个并发事务之间要相互隔离。

4.持久性

持久性是指一个事物一旦提交了,那么对DB的改变就是永久性的,即便是在DB系统遇到故障的情况下也不会丢失提交事物的操作,举个栗子,我们在使用JDBC操作DB时,在提交事物方法后,提示用户事物操作完成,当我们程序执行完成,直到看到提示后,就可以认定事物已经正确提交,即使这个时候DB出现了问题,也必须将我们的事物完全之后完成,否则的话,就会造成我们虽然看到提示事物处理完毕,但是DB因为故障而没有执行事物的重大错误,这是不允许的。

二、事物的隔离级别

1.为什么要设置隔离级别

在DB的操作中,在并发的情况下可能出现如下问题:

更新丢失(Lost update)

如果多个线程操作基于同一个查询结构,对表中的记录进行修改,那么修改后的记录将会覆盖前面修改的记录,这样前面修改的记录就丢失了,这就叫做更新丢失。这是因为系统没有执行任何的所操作,因此开发事物并没有被隔离开来。

第①类丢失更新:事物A撤销时,把已经提交的事物B的更新数据覆盖了。

时间取款事物A转账事物B
T1开始事物
T2开始事物
T3查询用户余额为1000元
T4查询账户余额为1000元
T5汇入转账100元修改余额为1100元
T6提交事物
T7取出1000元余额修改为900元
T8撤销事物
T9余额恢复为1000元(丢失更新)

第②类丢失更新:事物A覆盖事物B所提交的数据,造成事物B所做的操作丢失。

时间转账事物A取款事物B
T1开始事物
T2开始事物
T3查询用户余额为1000元
T4查询账户余额为1000元
T5取出100元余额修改为900元
T6提交事物
T7汇入转账100元修改余额为1100元
T8提交事物
T9余额恢复为1100元(丢失更新)

解决方案:对行加锁,只允许并发一个更新事物。

脏读(Dirty Reads)

脏读(Dirty Read):A事务读取B事务尚未提交的数据并在此基础上操作,而B事务执行回滚,那么A读取到的数据就是脏数据。

时间转账事物A取款事物B
T1开始事物
T2开始事物
T3查询用户余额为1000元
T4取出500元余额修改为500元
T5查询账户余额为500元(脏读)
T6撤销事物余额恢复为1000元
T7汇入1100元,把余额修改为600
T8提交事物

解决办法:如果在第一个事务提交前,任何其他事务不可读取其修改过的值,则可以避免该问题。

幻象读

指两次执行同一个SELECT语句出现的查询结果不同,第二次读会增加一数据行,并没有说这两次执行是在一个事物中。一般情况下,幻象读应该正是我们所需要的,但有时候却不是。如果打开的游标,在对游标进行操作时,并不希望增加的记录加到游标命中的数据集中来,隔离级别为游标稳定性的,可以阻止幻读。举个栗子,目前工资为1000的员工有10人,那么事物1中读取所有工资为1000的员工,得到了10条记录;这时事物2向员工表插入了一条员工记录,工资也为1000;那么事物1再次读取所有工资为1000的员工共读取到了11条记录。

时间统计金额事物A转账事物B
T1开始事物
T2开始事物
T3统计总存款为10000元
T4新增一个存款账户存入100元
T5事物提交
T6再次统计总存款为10100元(幻读)

解决办法:如果在操作事务完成数据处理之前,任何其他事务都不可以添加新数据,则可避免该问题。

↓↓↓↓↓正是为了解决以上情况,数据库提供了几种隔离级别。↓↓↓↓↓

2.事物的隔离级别

数据库的隔离级别有4个,由低到高依次为Read uncommitted(未授权读取、读未提交)、Read committed(授权读取、读提交)、Repeatable read(可重复读取)、Serializable(序列化),这四个级别可以逐个解决脏读、不可重复读、幻象读这几类问题。

●Read uncommitted(未授权读取,读未提交)
如果一个事物已经开始写数据,则另外一个事物则不允许同时进行写操作,但允许其他事物读取此行数据。该隔离级别可以通过排他写锁实现。这样就避免了更新丢失,却可能出现脏读,也就时说事物B读取到了事物A未提交的数据。

●Read committed(授权读取,读提交)
读取数据的事物允许其他事物继续访问该行数据,但是未提交的写事物将会禁止其他事物访问该行。该隔离级别避免了脏读,但是却可能出现不可重复读。事物A事先读取了数据,事物B紧接着更新了数据,并提交了事物,而事物A再次读取该数据时,数据已经发生了改变。

●Repeatable read(可重读读)
可重复读是指在一个事物内,多次访问同一数据,在这个事物还没有结束时,另外一个事物也访问该同一数据,那么,在第一个事物中的两次读数据之间,即使第二个事物对数据进行修改,第一个事物两次读到的数据是一样的,这样就发生了在一个事物内两次读到的数据是一样的,因此成为可重读读。读取数据的事物将会禁止写事物(但允许读事物),写事物则禁止任何其他事物,这样避免了不可重复度和脏读,但是有时可能出现幻象读,(读数据的事物)这可以通过共享读锁排他写锁实现。

●Serializable(序列化)
提供严格的事物隔离,它需要事物序列化执行,事物只能一个接着一个的执行,但不能并发执行,如果仅仅通过行级锁是无法实现事物序列化的,必须通过其他机制保证新插入的数据不会被执行行查询操作的事物访问到。序列化是最高的事物隔离级别,同时代价也花费最高,性能很低,一般很少使用,在该级别下,事物顺序执行,不仅可以避免脏读、不可重复读、还避免了幻象读。

隔离级别脏读不可重读读幻读第①类丢失更新第②类丢失更新
Read uncommittedYESYESYESNOYES
Read committedNOYESYESNOYES
Repeatable readNONOYESNONO
SerializableNONONONONO

隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也就越大。对于多数的应用程序,可以优先考虑把DB系统的隔离级别设为Read committed,它能够避免脏读,而且具有较好的并发性能。尽管它会导致不可重读读、幻读和第②类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。大多数DB默认级别就是Read committed,比如SqlServer、Oracle、MySQL。

三、悲观锁和乐观锁

虽然DB的隔离级别可以解决大多数问题,但是灵活度较差,为此又提出了悲观锁和乐观锁的概念。

1.悲观锁

悲观锁,指的是数据被外界(包括系统当前的其他事物,以及来自外部系统的事物处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠DB提供的锁机制,也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,及时在本系统的数据访问层中实现了加锁机制,也无法保证外部系统不会修改数据。

●使用场景栗子

以MySQL的InnoDB为栗,场景模拟:

商品t_items表中有个字段status,status=1表示商品未被下单,status=2表示商品已被下单(此时商品无法再次下单),那么对某个商品下单时必须确保该商品的status=1。假设该商品的id=1

如果不采用锁,那么操作方法如下:
# step 1. 查询出商品信息
SELECT status FROM t_items WHERE id = 1;
# step 2. 根据商品信息生成订单,并插入订单表 t_orders
INSERT INTO t_orders(id, goods_id) value(null, 1);
# step 3. 修改商品status=2
UPDATE t_items SET status = 2;

那么问题来了:
	这种场景模拟,在高并发的情况下,很可能会出现问题。
	例如当第一步操作中,查询出来的商品status=1,
	但是 当执行第三步UPDATE操作的时候,有可能出现其他人先一步对商品下单,把status值修改成了2,
	其他查询商品status=1的人并不知道数据已经被修改了,
	这就造成一个商品被下单两次,使得数据不一致,所以说这种方式是不安全的。
	
解决方式:悲观锁
	在模拟场景中,商品信息从查询出来到修改,中间有一个处理订单的过程。
	使用悲观锁的原理就是,当我们在查询出i_items信息后,把当前的数据锁定,直到我们修改完毕后再解锁。
	那么在这个过程中,因为t_items被锁定了,就不会出现有第三者来对数据进行修改了。
	需要注意的是,要使用悲观锁,必须关闭MySQL的自动提交属性,因为MySQL默认使用autocommit模式,
	也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。可以使用命令设置MySQL为非autocommit模式:set autocommit = 0
	设置完autocommit后,就可以执行正常的业务了。如下
# step 1. 开始事物 begin;begin work;start transaction;(三者选择一个就可以)
begin work;
# step 2. 查询出商品信息
SELECT status FROM t_items WHERE id = 1 FOR UPDATE;
# step 3. 根据商品信息生成订单
INSERT INTO t_orders(id, goods_id) value(null, 1);
# step 4. 修改商品status=2
UPDATE t_items SET status = 2;
# step 5. 提交事务commit;commit work;(二者选一就可以)
commit work;

PS:
	上面的begin work 和 commit work为事物的开始 和 结束。因为在前一步我们关闭了MySQL的autocommit,所以需要手动控制事物的提交。
	上面 step 2. 查询出商品信息 执行了一次查询操作: SELECT status FROM t_items WHERE id = 1 FOR UPDATE;与普通查询不一样
	的是使用了 FOR UPDATE 的方式,这样就通过数据库实现了悲观锁。此时在t_items表中,id=1的商品数据就被锁定了,其他的事物必须等本
	次事物提交后才能执行。这样我们可以保证当前的数据不会被其他事物修改。
	需要注意,在事物中 FOR UPDATE 或 LOCK IN SHARE MODE 操作同一个数据时才会等待其他事物结束后才执行,普通SELECT则不受此影响。
	拿上面的栗子来说:
	当执行 SELECT status FROM t_items WHERE id = 1 FOR UPDATE; 后。在另外的事物中
	如果再次执行 SELECT status FROM t_items WHERE id = 1 FOR UPDATE;则第二个事物会一直等待第一个事物提交,
	此时第二个查询处于阻塞的状态。但是如果是在第二个事物中执行 SELECT status FROM t_items WHERE id = 1;则能正常查询出数据,
	不会受到第一个事物的影响。
	
补充:
	Row Lock 和 Table Lock

	使用SELECT...FOR UPDATE 会把数据锁住,这里需要注意一些锁的级别,MySQL InnoDB默认Row-Level Lock,
	所以只有【明确】指定主键 或 索引,MySQL才会执行 Row Lock(只锁住被选取的数据),否则MySQL将会执行Table Lock(整个数据表锁定)
	举个栗子: 
	1. SELECT * FROM t_items WHERE id = 1 FOR UPDATE;
	这条语句明确指定主键(id = 1),并且有此数据(id = 1的数据存在),则采用Row Lock,只锁定当前这条数据。
	2. SELECT * FROM t_items WHERE id = 3 FOR UPDATE;
	这条语句明确指定主键(id = 3),但是却查无此数据,此时不会产生Lock(没有元数据,又去Lock谁呢)。
	3. SELECT * FROM t_items WHERE name = ‘手机’ FOR UPDATE;
	这条语句没有指定数据的主键,那么此时产生Table Lock,即在当前事物提交前整张数据表的所有字段将无法被查询。
	4. SELECT * FROM t_items WHERE id > 0 FOR UPDATE;或SELECT * FROM t_items WHERE id <> 1 FOR UPDATE;
	这两条语句的主键都不明确,也会产生Table Lock。
	5. SELECT * FROM t_items WHERE status = 1 FOR UPDATE;(假设为status字段添加了索引)
	这条语句明确了索引,并且有此数据,则产生Row Lock
	6. SELECT * FROM t_items WHERE status = 3 FOR UPDATE;(假设为status字段添加了索引)
	这条语句明确指定索引,但是根据索引查无此数据,也就不会产生Lock	

悲观锁总结

悲观锁并不适用于任何场景,它也有存在的一些不足,因为悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。如果加锁的时间过长,其他用户长时间无法访问,影响了程序的并发访问性,同时这样对数据库性能开销也很大,特别是长事物,这样的开销往往无法承受。所以与悲观锁相对的,我们有了乐观锁。

2.乐观锁

乐观锁(Optimistic Locking)相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以只会在数据进行提交更新时,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回用户错误信息,让用户决定如何去做。实现乐观锁一般来说有一下两种方式:

●使用版本号

使用数据版本(Verasion)记录机制来实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库增加一个字段类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出来,数据每更新一次,对此version加一。当提交更新时,判断数据库表对应记录的当前版本信息与第一次读取出来的version值进行比较,如果数据库当前版本号与当前读取出来的版本号值相等,则予以更新,否则任务是过期数据。

●使用时间戳

乐观锁的第二种实现方式和第一种差不多,同样是需要乐观锁控制的表中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp),和上面的version类似,也是在更新提交时,检查当前数据库中的时间戳和自己更新前取到的时间戳进行比较,如果一致,则予以更新,否则就是版本冲突。

不负韶华,只争朝夕

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值