数据库事务和锁
事务的概念
一个数据库事务通常包含了一个序列的对数据库的读/写操作。它的存在包含有以下两个目的:
- 为数据库操作序列提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持一致性的方法。
- 当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。
当事务被提交给了DBMS(数据库管理系统),则DBMS需要确保该事务中的所有操作都成功完成且其结果被永久保存在数据库中,如果事务中有的操作没有成功完成,则事务中的所有操作都需要被回滚,回到事务执行前的状态;同时,该事务对数据库或者其他事务的执行无影响,所有的事务都好像在独立的运行。
事务的ACID性质
- 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
- 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。
- 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
- 持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。
事务的隔离级别
数据库中事务有4种隔离级别:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
Read Uncommitted | 可能 | 可能 | 可能 |
Read Committed | 不能 | 可能 | 可能 |
Repeatable Read | 不能 | 不能 | 可能 |
Serializable | 不能 | 不能 | 不能 |
数据库数据如下:
select * from isolation;
+----+------+
| id | name |
+----+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+----+------+
3 rows in set
脏读
可能读取到其他会话中未提交事务修改的数据。
----------------------------------------------------------------------------------
事务A | 事务B
----------------------------------------------------------------------------------
begin | begin
----------------------------------------------------------------------------------
| update isolation set name = '-1' where id = 1
----------------------------------------------------------------------------------
select name where id = 1 |
----------------------------------------------------------------------------------
commit | commit
----------------------------------------------------------------------------------
Console:1
不可重复读
在一个事务中,多次读取的数据不一样。主要指另一个事务update、delete操作后会影响该事务select
----------------------------------------------------------------------------------
事务A | 事务B
----------------------------------------------------------------------------------
begin | begin
----------------------------------------------------------------------------------
|
----------------------------------------------------------------------------------
select name where id = 2 |
----------------------------------------------------------------------------------
| update isolation set name = '-1' where id = 2
----------------------------------------------------------------------------------
| commit
----------------------------------------------------------------------------------
select name where id = 2 |
----------------------------------------------------------------------------------
commit |
----------------------------------------------------------------------------------
Console:2 Console:-1
幻读
在一个事务中,多次读取的数据不一样。主要指另一个事务insert操作后会影响该事务select
----------------------------------------------------------------------------------
事务A | 事务B
----------------------------------------------------------------------------------
begin | begin
----------------------------------------------------------------------------------
|
----------------------------------------------------------------------------------
select count(1) where name = '3' |
----------------------------------------------------------------------------------
| insert into isolation(name) values('3')
----------------------------------------------------------------------------------
| commit
----------------------------------------------------------------------------------
select count(1) where name = '3' |
----------------------------------------------------------------------------------
commit |
----------------------------------------------------------------------------------
Console:1 Console:2
不可重复读和幻读的区别
不可重复读和幻读的相似之处在于结果上都是在一个事务中多次查询结果不一致,其不同在于造成其结果的原因不同,不可重复读因为update、delete操作而幻读因为insert操作。
可以从锁机制实现隔离来看这两个的区别:可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。
锁
数据库通过加锁来实现上诉数据库事务。Mysql的InnoDB数据库引擎通过乐观锁、悲观锁实现事务隔离,实现数据库的并发控制。
悲观锁
百度百科中这样介绍悲观锁,悲观锁在数据库理论中根据锁的互斥性把锁分为共享锁和排它锁:
正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
上诉悲观锁,我们可以看出有点像Java中的Synchronized、ReentranLock的感觉,是同步阻塞的。在两个事务同时进行锁操作时,后一个事务会被阻塞直至获取锁或者超时。
下面使用Mysql的Update操作来看悲观锁机制,事务A进行更新操作,但是不提交:
// 设置Mysql不用自动提交
mysql> set autocommit=0;
Query OK, 0 rows affected
// Mysql Update操作会造成加锁
mysql> update isolation set name = '-1' where id=1;
Query OK, 1 row affected
Rows matched: 1 Changed: 1 Warnings: 0
// 这里不Commit
事务B同时也进行相同的更新操作,结果会操作该线程阻塞:
mysql> update isolation set name = '-1' where id=1
// 会一直阻塞在这里直至获取锁或者超时
乐观锁
百度百科中这样介绍乐观锁:
乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
上诉乐观锁,我们可以看出来有点像Java中的CAS操作,是同步非阻塞的。
Reference
https://tech.meituan.com/innodb-lock.html
http://chenzhou123520.iteye.com/blog/1860954
https://blog.csdn.net/sadfishsc/article/details/51027734