关于事务与锁的解释在很多博客中都很详细了,这里大致记下一些简要内容(方便以后复习时对照思考),重点是操作案例,以表现事务与锁的操作的具体样例!
很多都是我自己的理解,有错望指出!
参考:mysql的锁(行锁、表锁)
1. 锁
1.1 锁的分类
按锁的粒度分类
- 行锁:即在数据行加锁
优点:尽可能的减低锁的粒度,降低了锁冲突发生的概率,提高并发度。
缺点:开销大,加锁慢,可能会出现死锁。
注意: 行锁是针对索引的,加上行锁是在针对索引加锁,
- 表锁:对表加锁
优点:开销小,加锁和释放锁快,对表加锁可以很好避免了死锁问题。
缺点:锁的粒度太大,读取少数数据行也要锁定整张表,降低了并发度
可是我疑惑为什么加表锁能避免死锁,当a事务访问A表时,后续还要访问B表;事务b访问B表时要访问a表,这不就死锁了吗???
经查询,有知乎伙伴说,获取和释放锁的开销很小,可能事务一开始就将所有涉及的表都加上表锁,这样就不会出现死锁了!这好像才说得通!事实就是如此,如果一个事务真的涉及多张表,很容易产生死锁,所以最好将多张表一次性加上表锁。
- 页锁
略TODO
按封锁类型分类
- S锁(共享锁):也可称为‘读锁’,一事务在某资源获取了S锁,则该事物只能读取该资源,不可修改,其他事务可以在此资源上获取S锁,实现同样操作。但不能获取该资源的X锁。
- X锁(互斥锁):也可称为‘写锁’,一事务在某资源获取了X锁,则该事务可以对其进行读写操作,其他事务不能获取该资源的S锁和X锁。
此外:mysql还是有意向锁,有数据库自动生成,不需要用户干预,这里了解下。顾名思义,意向锁即具有加某种锁的意向,所以也分为S和X,名为IS(intention S)和IX。意向锁都是表锁。
- IX:事务要给某行记录加互斥锁,要先获得该表的意向锁IX。
- IS:事务要给某行记录加共享锁,要先获取该表的意向锁IS。
其兼容性如下表:
锁类型 | S | X | IS | IX |
---|---|---|---|---|
S | 兼容 | 冲突 | 兼容 | 冲突 |
X | 冲突 | 冲突 | 冲突 | 冲突 |
IS | 兼容 | 冲突 | 兼容 | 兼容 |
IX | 冲突 | 冲突 | 兼容 | 兼容 |
- 如果某事务要对条记录加锁,若与当前锁兼容则获取该锁,否则就要等待锁的释放。
- 对于sql,只有在 update、 delete、 insert会给所涉及的数据项添加X锁,对于select,数据库不会加锁。
- 当然我们可以显式地加上S锁和X锁:
select * from test where ... lock in share mode;
上述形式的使用场景可以用在某事务读取某数据后防止其他事务update和delete该数据,其他事务需要修改的话就要先加X锁,但因为该数据已加上S锁,所以需要等待。(即防止可重复读问题,但注意,如果本事务也对数据进行修改操作,就会发生死锁!)
select * from test where ... for update;
间隙锁
当我们使用的是范围查找而不是等值条件,并且还要加上共享锁和互斥锁时,mysql会将所在范围内的记录都加上锁,在这个范围内不存在的记录,称为间隙,也会加上锁。比如:
select * from where id > 100 for update;
这时候除了表中大于100的记录被加上锁,不存在的间隙也会被加上锁, 比如同时另外一个事务想插入一个id=101的记录,会插入失败。
这就让我想到了之前项目中为了保证注册时用户名的唯一性,必须先查找一遍以确定是否已经存在即将注册的用户名,然后再插入。可是极有可能再查找完确定不存在并返回结果的同时,另外一个事务突然插入同一个用户名,注册就会失败。这种现象即是幻读,这个就可以用间隙锁解决。
虽然间隙锁避免了我们为避免幻读而使事务隔离级别为序列化造成数据库的并发性严重降低的后果,但也要注意不要锁太大范围,即太大间隙。尽量优化业务逻辑,使用等值连接代替范围查找。
1.2 锁操作示例
示例主要是想通过实际操作对前面理论进行验证,顺序按照前面所述顺序来!
1.2.1 列锁
①列锁是针对索引的。 ②select不会加锁,所以不用获得锁
创建没有索引的表balance,然后插入两条数据。
create table balance (
id int,
balance int);
insert into balance values(1, 500),(2,300);
打开两个事务:
可以看到,第一个事务显式在id=1记录上加上行写锁,id=2也被锁住了,所以id没有索引时,会直接在表上加锁。
下面试试有索引的情况:
create index id_index on balance(id);
题外话,如果同时查询了一个索引一个非索引,比如:
select * from balance where id = 1 and balance = 500 for update;
第二行记录 1,300还会锁吗?答案是会的,因为上面的sql命中了id=1的索引,只在索引1上加锁,第二行索引也为一,所以会加上。
③update,insert,delete自动加写锁,索引规则与前面相同。
1.2.2 表锁
TODO
1.2.3 间隙锁
间隙锁也符合索引那套规则,加锁不在索引,即锁表再次创建索引:
create index id_index on balance(id);
对id>1的间隙加锁,1能访问,2不能
现在插入一条id=3的记录
insert into balance values(3, 1000)
直接在加不是间隙锁的事务中添加,被阻塞,因为3>1,在间隙中
虽然添加数据成功,但第二个事务普通的select都访问不到,这不是锁的缘故,而是因为前面提到过的MVCC的原因(下面会提到),后面的事务因为访问的是快照中的数据,此时快照中的数据并没有将新添加的数据3加入,所以访问不到!添加一方提交后,另一方才能访问到,这避免了即将提到的脏读。
没有间隙锁,普通的select(不与任何锁冲突)也无法访问到
加上间隙锁后,别的事务也无法添加指定范围内新的数据,这即避免了幻读。
2. 事务
2.1 事务的属性ACID
- A(Atomicity),原子性,即一个事务的所有操作要么全部执行,要么都不执行,就像一个原子操作一样。
- C(Consistency),一致性,即事务执行前与执行后数据内在的逻辑始终是成立的。比如转账前与转账后两人存款的总和始终不变。
- I(Isolation),隔离性,即数据库提供一些隔离机制,确定能多事务能一定程度地独立运行,互不干扰,意味着事务运行的中间状态是不会被别的事务看到的。
- D(Durable),持久性,即事务对数据造成的影响是永久性的,即是出现故障都能保证。
2.2 事务并发带来的一些问题
- 更新丢失:即两个事务对一个数据更新,一个事务覆盖另一个事务的问题。
- 脏读:即一个事务读取了另一个事务为提交的更改。
- 不可重复读:当一个事务访问某条数据后,另一事务更改了这条数据,再次访问,两次是不一样的数据。不可重复读。这个更改只涉及另一事务的Update和Delete操作。
- 幻读:当一个事务以某种条件读取数据,另一事物插入了满足条件的数据,再次访问多出了之前没有访问的数据。重点是另一事物是insert的操作!
追加:不可重复读和幻读的区别还是有些模糊,有很多博客都是像上面那样说,但有人说不可重复读比较偏向于两次读,而幻读则偏向于一读一写,比如某事务先select判断某数据字段并不存在,但当添加这一字段的数据时又说有冲突,可是再次读取还是没有这一字段(MVCC)。这时候可能是其他事务在这一事务读取之后添加了这一字段,此为幻读。理解自: mysql 幻读的详解、实例及解决办法
2.3 事务的隔离级别
- READ_UNCOMMITED:读未提交
- READ_COMMITTED:读提交
- REPEATABLE_READ:可重复读
可重复读的级别下也是可以防止幻读的,比如如果数据存在,则在此数据上加上X锁,如果数据不存在就是用next-key锁,放置其他事务添加或操作指定数据。 - SERIALIZABLE:串行化
串行化的原理就是将所有读取的数据都加上X锁,一旦读取其他事务就无法读取和操作。这虽保证了事务并发的安全性,但也使得并发效率大大降低。
脏读 | 不可重复读 | 幻读 | |
---|---|---|---|
读提交 | 解决 | ||
可重复读 | 解决 | 解决 | |
串行化 | 解决 | 解决 | 解决 |
参考的文章中说:
在并发事务处理带来的问题中,“更新丢失”通常应该是完全避免的。但防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决,因此,防止更新丢失应该是应用的责任。
“脏读”、“不可重复读”和“幻读”,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。数据库实现事务隔离的方式,基本可以分为以下两种。
一种是在读取数据前,对其加锁,阻止其他事务对数据进行修改。这势必会造成性能上的不好的影响。----悲观锁
另一种是不用加任何锁,通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一致性读取。从用户的角度,好像是数据库可以提供同一数据的多个版本,因此,这种技术叫做数据多版本并发控制(MultiVersion Concurrency Control,简称MVCC或MCC),也经常称为多版本数据库。----乐观锁
2.4 事务操作示例
2.4.1 并发访问的问题实现
- 丢失更新
略 -
脏读
因为mysql默认隔离级别是可重复读:
所以我们要更改其隔离级别。
select @@transaction_isolation;
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL
{READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
修改为读未提交,后打开自动提交。
我们这里已经打开了
可以看到第二个事务读取到了第一个事务未提交的脏数据可以使用
set auto_commit=0(1关闭)
将其打开。
第一个事务如果撤回,另一个事务就读取了完全错误的数据,这就是脏读。
- 不可重复读
略 - 幻读
略
感觉挺简单的,懒得实现了。
2.4.2事务的隔离级别
懂的,操作简单就不浪费时间了。
网友评论