前言
提数据库就会讲到数据库的事务问题,常见的一个问题就是:你的数据库是如何保证事务的?这里的数据库可以是关系型数据库,如mysql,或者是非关系型的,如MongoDB或是其他数据库。令人头疼的是,这样的问题很常见却不知怎么回答,一个原因是没有涉及过事务相关的操作,还有项目中有事务,往往也是一个注解搞定,根本不深入。今天就来全面总结下有关事务的问题。
事务的若干概念
ACID特性
一般来说,事务具有4个特点,A(原子性),C(一致性),I(隔离性,事务之间具有一定隔离性),D(持久性),称为ACID特性。
原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。
隔离性(Isolation):多个事务并发执行时,一个事务的执行
不应影响
其他事务的执行。这里的不影响
具体要看具体的事务隔离级别。
持久性(Durability):一个事务一旦提交,他对数据库的修改应该永久保存在数据库中。
数据库的原子性,一致性和持久性一般通过事务日志实现,而隔离性通过数据库锁、MVCC(并发一致性控制)实现。
事务隔离型
事务定义了四种隔离级别。分别是未提交读
、已提交读
、可重复读
和串行化
。
-
读未提交(READ-UNCOMMITTED)
所有事务都可以看到其他未提交事务的执行结果,可能产生脏读
,一般(十分)不采用。 -
读已提交(READ-COMMITTED)
其他事务提交后才可以看到,避免了脏读现象。但仍避免不了这个问题:我一个事务中第一次某行数据是一个值,第二次读又是一个值了。(在两次读过程中,有事务提交了),这种现象叫不可重复读
。顾名思义,就是不能重复读取。
读已提交
是大多数数据库默认的隔离级别(不包括mysql,mysql隔离级别是可重复读),因为这已经满足隔离的基本定义,性能也很高。 -
可重复读(REPEATABLE-READ)
针对前一种隔离级别的问题,修复了不可重复读
的问题,也就是在一个事务中,读取同一行的数据是不变的。不过,有个问题是:当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。这是幻读
。
可重复读
是mysql默认隔离级别。 -
串行化(SERIALIZABLE)
所有事务操作串行执行,没有隔离级别引起的问题,但性能也最差,一般不用。
设置mysql隔离级别
set session transaction isolation level read committed;
mysql的锁
按锁的粒度来分,分为表锁和行锁。
行级锁是Mysql中锁定粒度最细的一种锁,只针对行加锁。行级锁能大大减少数据库操作的冲突。加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁 和 排他锁。
表级锁是MySQL中锁定粒度最大的一种锁,表示对整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。
MySQL不同的存储引擎支持不同的锁机制。如MyISAM支持表级锁,而InnoDB支持行级锁和表级锁(默认支持行级锁)。
MyISAM
MyIsam支持表级锁,表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)。也就是读锁和写锁。其中读读之间可以共存,读写之间互斥。
MyIsam的读、写锁都是阻塞锁,即如果拿不到锁,会一直阻塞住。
加锁方式是一次性拿到所有的锁,所以不会出现死锁现象。一般select
会自动加读锁,update
、insert
、delete
会自动加写锁。另外,即是同一个表,如果在sql中出现多次,也会加多个锁。
默认写锁的级别比读锁高,当有大量读、写任务时,读锁可能会一直阻塞获取锁。这也正是 MyISAM 表不太适合于有大量更新操作和查询操作应用的原因。
InnoDB
InnoDB默认为行锁,表锁前面已经说了,现在说说行锁。行锁种类和表锁类似,也分别读和写锁。默认分为共享锁
(S,也就是读锁)和排他锁
(X,写锁)。
共享锁,行锁加共享锁使用SELECT ... LOCK IN SHARE MODE
。共享锁之间可以并发。
排他锁,使用for update
进行加锁。排他锁与其他锁互斥,加排他锁后不能再加其他锁。
如 select .... for update
就是对读操作加了排他锁。
其他 update、delete、insert等写操作,会自动加排他锁。
行锁是一行一行进行的,分别取出来一一加锁。
MVCC(多版本并发控制)
MVCC是数据库提高读、写并发的常用技术,简单的说就是通过设置多版本,在读取数据时根据事务开始时机以及隔离级别读取对应版本的数据,可以让读、写互不阻塞。大大提高性能。
MVCC中读分为快照读和当前读。快照读就是读取历史版本,当前读就是读取当前数据。
普通的读都属于快照读,如select ...
属于当前读的有(加锁操作和写操作)
select ... lock in share mode
加行共享锁
select ... for update
加行排他锁
update ...
delete
insert into
加锁分析
在innodb下分析
- 普通的
select
不加锁,直接走快照读。 update
、delete
操作,在RC(读已提交)情况下,有主键走主键(加X锁),有索引走索引(唯一索引或普通索引,加锁两次,一次加在二级索引上,一次加在主键索引上,唯一索引锁一行,普通的锁符合条件的行数,加X锁)- 对于
update
、delete
,没有索引的情况,在RC(读已提交)下,会扫全表加锁,然后对不符合条件的释放锁。(全部加锁这一步是不能少的,也即最终是对符合条件的行加的行锁) - 对于
update
、delete
,在RR(可重复读)情况下,查询条件是索引,但不是唯一索引的情况下,会在索引中加X锁和间隙锁(GAP), 在主键索引中加X锁。GAP锁是为防止记录之间插入记录。 - 对于
update
、delete
,在RR
下,查询条件不是索引,扫描主键索引,在主键索引所有记录上加X锁,并在记录之间加GAP(间隙锁)。 - 对于where条件,多余1个情况下,可分为
Index key
和Index Filter
、table filter
。有索引确定查询条件(会添加间隙锁),index filter 对索引条件进行过滤,过滤后加上X锁,table filter 会始终加上X锁。有mysql引擎层过滤。 - GAP(间隙锁)存在于RR(可重读读)级别及以上,读未提交(RU)和读已提交(RC)是没有间隙锁的。
再次总结下,在RC(读已提交)级别下,当需要加锁时(写操作或使用for update、share in lock mode),无论有无索引,最终加的都是行锁,不过,有索引会避免全表扫描,直接在索引行加锁;而无索引的话则会先全表扫描(这一步不可少),然后由mysql引擎过滤掉不符合条件的行,加上行锁。
在RR(可重复读)级别下,区别在于有了间隙锁,不过原则是如果能查到唯一一行,则加行锁,多行的话,则是行锁+间隙锁。如果是聚簇索引,则一颗索引树,非聚簇索引则非聚簇和聚簇树都要分析下。
发生死锁的条件不在于sql的多少,而在于加锁的顺序,比如一个sql先对A行加锁,后对B行加锁,而另一个sql则相反,则可能会产生死锁。具体可看MySQL 加锁处理分析 写的很详细。
隔离性实践
在读已提交
情况下
事务A | 事务B |
---|---|
begin; | begin; |
select score from grade where id=1; // id为1的分数为50 | |
update grade set score =100 where id=1; | |
select * from grade where id=1; // 这里成了100 | |
commit; | commit; |
如果改为可重复读
,则结果一直是50。