1、事务隔离级别
先说事务四个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),也就是我们常说的事务ACID。
隔离级别主要有四种:
未提交读(READ-UNCOMMITTED):最低的隔离级别。
提交读(READ-COMMITTED):允许读取并发业务现已提交的数据,能够阻止脏读,但幻读、不可重复读仍有可能发生。
可重复读(REPEATABLE-READ):对同一字段的多次读取结果都一致,除非数据是被本身业务自己所修正,能够阻止脏读和不可重复读,但幻读仍有可能发生。
可串行化(SERIALIZABLE):最高的阻隔等级,所有的业务是必须顺次逐个执行,能有效防止脏读、不可重复读、幻读。
MySQL默认可重复读。
隔离级别做了什么事情呢?主要是确保数据一致性。我们来看几个例子:
1.1、脏读(dirty read)
当事务A对数据进行了修改,而这种修改还没有提交到数据库中。
这时,另外一个事务B也访问这个数据,然后使用了这个数据。
主要表现为事务还没有提交就读取到的对应的数据结果。
小A和小B 是好朋友,某天,小B买车找小A借钱5W,小A开启了事务转了10W给小B但是没有提交事务,小B开启了事务查看余额,发现余额10W以为朋友关系铁借5W给10W,然后就高兴的提交事务去买车了。过了一会小A发现转多了,就赶紧回滚了事务,但是这个时候小B 已经看到有10W并且买完车了。是不是很诡异,平白无故多了好多钱。
1.2、不可重复读(unrepeatable read)
在事务1内,读取了一个数据,事务1还没有结束时,事务2也访问了这个数据,修改了这个数据,并提交。
紧接着,事务1又读这个数据。由于事务2的修改,那么事务1两次读到的的数据可能是不一样的,
因此称为是不可重复读。某天,小A想看自己的余额,就开启了事务查看余额发现余额50W,这个时候小B开启了事务还上次借的10W并且提交了事务,小B提交之后,小A正好忘了刚才的余额就又看了一次,卧槽?我小A事务还没提交呢,这次怎么60W了?我到底有50W还是60W?
1.3、幻读(phantom problem)
当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,
当之前的事务再次读取该范围的记录时,会产生幻行。
InnoDB存储引擎通过多版本并发控制(MVCC)解决了幻读的问题。某天,小A想看部门的所有采购订单,就开启了事务查看订单列表发现今天一共10个订单,这个时候小B开启了事务新增了一个订单并提交事务,小A手一抖又点了一次搜索。卧槽?我小A的事务还没提交呢,这次怎么又11个订单了了?那么今天一共有多少个订单呢?
不可重复读和幻读很像,区别主要是前者针对update、deleted,后者针对insert。
如果需要解决:不可重复读一般加行锁即可,但是幻读可能需要锁表防止新增数据来解决。
1.4、隔离级别对比
图片摘自高性能MySQL:

2、锁
2.1、共享锁(读锁)
对某一资源加共享锁,自身可以读该资源,其他人也可以读该资源(也可以再继续加共享锁,即 共享锁可多个共存),但无法修改。要想修改就必须等所有共享锁都释放完之后。
例:
2.2、排它锁(写锁)
对某一资源加排它锁,自身可以进行增删改查,其他人无法进行任何操作。
例:你有一张银行卡, 除了你自己能存钱、取钱外,任何人都无法对你账号的余额进行操作,及时查看都不可以。
2.3、间隙锁
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁。
例如表中ID为1、3、4、5,for update条件为id<5,那么会锁住1-5的数据,id=2的虽然缺失,但是如果锁期间有id=2的insert则需要等到事务结束才会执行。
2.4、悲观锁(抽象性)
类似mysql的排他锁、常见使用示例:for update。
例:现在有一批macbook特价抢购,只要999,库存只有10台,要是你设计的不好,卖超了就要扣自己的钱了。那怎么搞?
start transaction;
select stock from product where product_id = 10001 for update;
if(stock > 0) 继续执行
if(stock <= 0) 卖完了
commit; for update 确保了必须要等上个事务提交之后才会执行数据,等于是在这行数据上加了一个排它锁。
2.5、乐观锁(抽象性)
乐观锁主要是靠表设计和代码来实现,一般都是用version版本或者timestamp时间戳字段控制。
注:乐观锁和悲观锁都是抽象的,并不是只管表现出来的。且都是针对读(select)来讲的。
2.6、死锁
死锁这个问题,很多人可能都看过一个笑话:
面试官问:请简单讲一下死锁,讲明白了我就录用你。应聘者:请先录用我,录用我之后我就给你讲明白。哈哈,就是这样,然后大家陷入了死循环,必须要有一个人主动让步才能缓解这个尴尬的情况。MySQL中的死锁同理,容我们用SQL模拟出来这个场景:
假设存在某表:product,字段 stock 用来记录每个商品的库存。
现在有两个用户分别购买了product_id=5、product_id=6 的商品各一件。
如果我们不用for update这种场景,可能存在一个问题:
A用户,同一笔交易先把id=5的商品加入购物车,后把id=6的商品加入购物车:
start transaction;
update product set stock = stock - 1 where product_id = 5;
update product set stock = stock - 1 where product_id = 6;
commit;
B用户,同一笔交易先把id=6的商品加入购物车,后把id=5的商品加入购物车:
start transaction;
update product set stock = stock - 1 where product_id = 6;
update product set stock = stock - 1 where product_id = 5;
commit; 如果用户A、用户B的两笔交易同时进行,那么会发生什么情况呢?
1、A事务说:我先锁住 id = 5 的更新;B事务说,我先锁住 id = 6 的更新。
2、A事务说:我要更新 id = 6 的数据了,咦?谁把这行数据锁住了,我没法更新了,等等吧,等到对方处理完这条数据释放锁了我再更新。
3、B事务说:我要更新 id = 5 的数据了,咦?谁把这行数据锁住了,我没法更新了,等等吧,等到对方处理完这条数据释放锁了我再更新。
4、无限循环ing....
以上就是死锁的原因和表现,主要还是对统一资源进行竞争导致的。
那么出现了死锁如何释放呢?这个时候,需要要人为干预处理,因为程序已经进行无限循环了。
那怎么尽可能的避免这种情况呢?不同的业务场景处理方式不同,拿上面的例子来说,我们如果都先更新id=5的是不是就可以了。