原文地址:https://my.oschina.net/oosc/blog/1620279
前言
锁是防止在两个事务操作同一个数据源(表或行)时交互破坏数据的一种机制。
数据库采用*技术保证并发操作的可串行性。
以Oracle为例:
Oracle的锁分为两大类:数据锁(也称DML锁)和字典锁。
字典锁是Oracle DBMS内部用于对字典表的*。
字典锁包括语法分析锁和DDL锁,由DBMS在必要的时候自动加锁和释放锁,用户无机控制。
Oracle主要提供了5种数据锁:
共享锁(Share Table Lock,简称S锁)、
排它锁(Exclusive Table Lock,简称X锁)、
行级锁(Row Share Table Lock,简称RS锁)、
行级排它锁(Row Exclusive Table Lock,简称RX锁)和
共享行级排它锁(Share Row Exclusive Table Lock,简称SRX锁)。
其*粒度包括行级和表级。
以Mysql为例:
行级锁/字段锁/表级锁
针对锁粒度划分:行锁、字段锁、表锁、库锁
(1)行锁:访问数据库的时候,锁定整个行数据,防止并发错误。
(2)字段锁:访问数据库的时候,锁定表的某几个字段数据,防止并发错误。
(3)表锁:访问数据库的时候,锁定整个表数据,防止并发错误。
行锁 和 表锁 的区别:
- 表锁: 开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低
- 行锁: 开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高
由浅入深举例说明:
1) 创建测试表
SYS@ORA11GR2>create table t_lock as select rownum as id,0 as type from dual connect by rownum <=3;
Table created.
SYS@ORA11GR2>select * from t_lock;
ID TYPE
---------- ----------
1 0
2 0
3 0
2) 会话1:查询type为0的最小id
SYS@ORA11GR2>set time on;
18:58:22 SYS@ORA11GR2>
18:58:23 SYS@ORA11GR2>select min(id) from t_lock where type=0;
MIN(ID)
----------
1
3) 会话2:查询type为0的最小id
SYS@ORA11GR2>set time on
18:59:31 SYS@ORA11GR2>select min(id) from t_lock where type=0;
MIN(ID)
----------
1
4) 会话1:将ID为1的这条记录的type置为1
19:00:53 SYS@ORA11GR2>update t_lock set type=1 where id=1;
1 row updated.
19:01:21 SYS@ORA11GR2>commit;
Commit complete.
19:01:37 SYS@ORA11GR2>select * from t_lock;
ID TYPE
---------- ----------
1 1
2 0
3 0
5) 会话2:将ID为1的这条记录的type置为2
19:02:47 SYS@ORA11GR2>update t_lock set type=2 where id=1;
1 row updated.
19:03:11 SYS@ORA11GR2>commit;
Commit complete.
19:03:17 SYS@ORA11GR2>select * from t_lock;
ID TYPE
---------- ----------
1 2
2 0
3 0
6) 小结:
我们看到id为1的type现在的值为2,会话1将type更新为1的记录已经“丢失”
1.2. 悲观锁
1) 会话1:查询id为2的记录并进行锁定
19:05:43 SYS@ORA11GR2>select * from t_lock where id=2 and type =0for update nowait;
ID TYPE
---------- ----------
2 0
2) 会话2:查询id为2的记录,此时查询报错
19:07:43 SYS@ORA11GR2>select * from t_lock where id=2 and type=0for update nowait;
select * from t_lock where id=2 and type=0 for update nowait
*
ERROR at line 1:
ORA-00054: resource busy and acquire with NOWAIT specified or timeout expired
3) 会话1:对id为2的记录进行更新。
19:19:08 SYS@ORA11GR2>update t_lock set type=1 where id=2 and type=0;
1 row updated.
19:19:30 SYS@ORA11GR2>commit;
Commit complete.
19:19:39 SYS@ORA11GR2>select * from t_lock where id=2;
ID TYPE
---------- ----------
2 1
4) 会话2:查询id为2的记录,由于已经将id为2的type已经变为1,所以查不到数据了。
19:19:15 SYS@ORA11GR2>select * from t_lock where id=2 and type=0for update nowait;
no rows selected
1.3乐观锁
1) 会话1:查询id为3的伪列ora_rowscn的值
19:22:00 SYS@ORA11GR2>select id,type,ora_rowscn from t_lock where id = 3;
ID TYPE ORA_ROWSCN
---------- ---------- ----------
3 0 1246809
2) 会话2:查询id为3的伪列ora_rowscn的值
19:23:01 SYS@ORA11GR2>select id,type,ora_rowscn from t_lock where id = 3;
ID TYPE ORA_ROWSCN
---------- ---------- ----------
3 0 1246809
3) 会话1:更新id为3的type为1
19:24:22 SYS@ORA11GR2>update t_lock set type=1 where ora_rowscn=1246809 and id = 3;
1 row updated.
19:25:29 SYS@ORA11GR2>commit;
Commit complete.
验证:
19:28:22 SYS@ORA11GR2>select id,type,ora_rowscn from t_lock where id = 3;
ID TYPE ORA_ROWSCN
---------- ---------- ----------
3 1 1247164
4) 会话2:更新id为3的type为1
19:26:05 SYS@ORA11GR2>update t_lock set type=1 whereora_rowscn=1246809 and id =3;
0 rows updated.
验证:
19:29:37 SYS@ORA11GR2>select id,type,ora_rowscn from t_lock where id = 3;
ID TYPE ORA_ROWSCN
---------- ---------- ----------
3 1 1247164
(因为会话1的事务更改了id=3的值,而且事务已经提交,事务的ora_rowscn已经变为1247164,原来的ora_rowscn=1246809已经不存在,所以没有可更改的行了)
1.4死锁
1) 创建测试表
19:35:46 SYS@ORA11GR2>create table t_lock_1 (id number(2),name varchar2(15));
Table created.
19:35:57 SYS@ORA11GR2>create table t_lock_2 as select * from t_lock_1;
Table created.
19:36:24 SYS@ORA11GR2>insert into t_lock_1 values(1,'liubei');
1 row created.
19:37:11 SYS@ORA11GR2>insert into t_lock_2 values (1,'guanyu');
1 row created.
19:37:38 SYS@ORA11GR2>commit;
Commit complete.
19:37:43 SYS@ORA11GR2>select * from t_lock_1;
ID NAME
---------- ---------------
1 liubei
19:38:01 SYS@ORA11GR2>select * from t_lock_2;
ID NAME
---------- ---------------
1 guanyu
2) 会话1:更新表t_lock_1的id字段为1的name为“liuxuande”,不提交
19:39:55 SYS@ORA11GR2>update t_lock_1 set name='liuxuande' where id =1;
1 row updated.
3) 会话2:更新表t_lock_2的id字段为1的name为“关云长”,不提交
19:39:47 SYS@ORA11GR2>update t_lock_2 set name='guanyunchang' where id = 1;
1 row updated.
4) 会话1:更新表t_lock_2的id字段为1的name为“guanyunchang”,此时挂起状态
19:40:30 SYS@ORA11GR2>update t_lock_2 set name='guanyunchang' where id =1;
5) 会话2:更新表t_lock_1的id字段为1的name为“liuxuande”,此时挂起状态
19:44:14 SYS@ORA11GR2>update t_lock_1 set name='liuxuande' where id =1;
6) 会话1:此时回到会话1,出现死锁错误
19:40:30 SYS@ORA11GR2>update t_lock_2 set name='guanyunchang' where id =1;
update t_lock_2 set name='guanyunchang' where id =1
*
ERROR at line 1:
ORA-00060: deadlock detected while waiting for resource
会话1处于死锁状态,而会话2处于挂起状态。
乐观锁/悲观锁
悲观锁:顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁: 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
悲观锁 和 乐观锁的区别:
两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
并发控制: 事务和锁的存在都是为了更好的解决并发访问造成的数据不一致性的的问题。
乐观锁和悲观锁都是为了解决并发控制问题, 乐观锁可以认为是一种在最后提交的时候检测冲突的手段,而悲观锁则是一种避免冲突的手段。
乐观锁: 是应用系统层面和数据的业务逻辑层次上的(实际上并没有加锁,只不过大家一直这样叫而已),利用程序处理并发, 它假定当某一个用户去读取某一个数据的时候,其他的用户不会来访问修改这个数据,但是在最后进行事务的提交的时候会进行版本的检查,以判断在该用户的操作过程中,没有其他用户修改了这个数据。开销比较小
乐观锁的实现大部分都是基于版本version控制实现的, 当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加1。当我们提交更新的时候,判断当前版本信息与第一次取出来的版本值大小,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据,拒绝更新,让用户重新操作。
除此之外,还可以通过时间戳的方式,通过提前读取,事后对比的方式实现。
写到这里我突然想起了,java的cuurent并发包里的Automic 类的实现原理CAS原理(Compare and Swap), 其实也可以看做是一种乐观锁的实现,通过将字段定义为volalate,(不允许在线程中保存副本,每一次读取或者修改都要从内存区读取,或者写入到内存中), 通过对比应该产生的结果和实际的结果来进行保证原子操作,进行并发控制(对比和交换的正确性保证 是处理器的原子操作)。
乐观锁的优势和劣势
优势:如果数据库记录始终处于悲观锁加锁状态,可以想见,如果面对几百上千个并发,那么要不断的加锁减锁,而且用户等待的时间会非常的长, 乐观锁机制避免了长事务中的数据库加锁解锁开销,大大提升了大并发量下的系统整体性能表现 所以如果系统的并发非常大的话,悲观锁定会带来非常大的性能问题,所以建议就要选择乐观锁定的方法, 而如果并发量不大,完全可以使用悲观锁定的方法。乐观锁也适合于读比较多的场景。
劣势: 但是乐观锁也存在着问题,只能在提交数据时才发现业务事务将要失败,如果系统的冲突非常的多,而且一旦冲突就要因为重新计算提交而造成较大的代价的话,乐观锁也会带来很大的问题,在某些情况下,发现失败太迟的代价会非常的大。而且乐观锁也无法解决脏读的问题
同时我在思考一个问题,乐观锁是如何保证检查版本,提交和修改版本是一个原子操作呢? 也就是如何保证在检查版本的期间,没有其他事务对其进行操作?
解决方案: 将比较,更新操作写入到同一条SQL语句中可以解决该问题,比如 update table1 set a=1, b=2, version = version +1 where version = 1; mysql 自己能够保障单条SQL语句的原子操作性。
如果是多条SQL语句,就需要mySQL的事务通过锁机制来保障了。
悲观锁: 完全依赖于数据库锁的机制实现的,在数据库中可以使用Repeatable Read的隔离级别(可重复读)来实现悲观锁,它完全满足悲观锁的要求(加锁)。
它认为当某一用户读取某一数据的时候,其他用户也会对该数据进行访问,所以在读取的时候就对数据进行加锁, 在该用户读取数据的期间,其他任何用户都不能来修改该数据,但是其他用户是可以读取该数据的, 只有当自己读取完毕才释放锁。
悲观锁的优势和劣势
劣势:开销较大,而且加锁时间较长,对于并发的访问性支持不好。
优势 : 能避免冲突的发生,
我们经常会在访问数据库的时候用到锁,怎么实现乐观锁和悲观锁呢?以hibernate为例,可以通过为记录添加版本或时间戳字段来实现乐观锁,一旦发现出现冲突了,修改失败就要通过事务进行回滚操作。可以用session.Lock()锁定对象来实现悲观锁(本质上就是执行了SELECT * FROM t FOR UPDATE语句)
乐观锁和悲观所各有优缺点,在乐观锁和悲观锁之间进行选择的标准是:发生冲突的频率与严重性。
如果冲突很少,或者冲突的后果不会很严重,那么通常情况下应该选择乐观锁,因为它能得到更好的并发性,而且更容易实现。但是,如果冲突太多或者冲突的结果对于用户来说痛苦的,那么就需要使用悲观策略,它能避免冲突的发生。 如果要求能够支持高并发,那么乐观锁。
其实使用乐观锁 高并发==高冲突, 看看你怎么衡量了。
但是现在大多数源代码开发者更倾向于使用乐观锁策略
共享锁和排它锁是具体的锁,是数据库机制上的锁。
共享锁(读锁) 在同一个时间段内,多个用户可以读取同一个资源,读取的过程中数据不会发生任何变化。读锁之间是相互不阻塞的, 多个用户可以同时读,但是不能允许有人修改, 任何事务都不允许获得数据上的排它锁,直到数据上释放掉所有的共享锁
排它锁(写锁) 在任何时候只能有一个用户写入资源,当进行写锁时会阻塞其他的读锁或者写锁操作,只能由这一个用户来写,其他用户既不能读也不能写。
加锁会有粒度问题,从粒度上从大到小可以划分为
表锁 开销较小,一旦有用户访问这个表就会加锁,其他用户就不能对这个表操作了,应用程序的访问请求遇到锁等待的可能性比较高。
页锁:是MySQL中比较独特的一种锁定级别,锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也同样是介于上面二者之间。另外,页级锁定和行级锁定一样,会发生死锁。
行锁 开销较大,能具体的锁定到表中的某一行数据,但是能更好的支持并发处理, 会发生死锁
事物: 用于保证数据库的一致性
所谓数据一致性,就是当多个用户试图同时访问一个数据库,它们的事务同时使用相同的数据时,可能会发生以下四种情况:丢失更新、脏读、不可重复读 和 幻读
所谓数据完整性, 数据库中的数据是从外界输入的,而数据的输入由于种种原因,会发生输入无效或错误信息。保证输入的数据符合规定,
数据完整性分为四类:实体完整性(Entity Integrity)、域完整性(Domain Integrity)、参照完整性(Referential Integrity)、用户定义的完整性(User-definedIntegrity)。
数据库采用多种方法来保证数据完整性,包括外键、约束、规则和触发器。
事务的ACID特性
原子性Automicity,一个事务内的所有操作,要么全做,要么全不做
一致性Consistency,数据库从一个一致性状态转到另一个一致性状态
独立性(隔离性)isolation, 一个事务在执行期间,对于其他事务来说是不可见的
持久性(Durability): 事务一旦成功提交,则就会永久性的对数据库进行了修改
隔离级别: mySQL默认的隔离级别是可重复读
在SQL 中定义了四种隔离级别;
READ UNCOMMITED(未提交度) 事务之间的数据是相互可见的
READ COMMITED(提交读) 大多数数据库的默认隔离级别, 保证了不可能脏读,但是不能保证可重复读, 在这个级别里,数据的加锁实现是读取都是不加锁的,但是数据的写入、修改和删除是需要加锁的。
REPEATABLE READ (可重复读) 解决了不可重复读的问题,保证了在同一个事务之中,多次读取相同的记录的值的结果是一致的。 但是无法解决幻读。这个阶段的事务隔离性,在mysql中是通过基于乐观锁原理的多版本控制实现的。
SERIALIZABLE (可串行化读) 最高的隔离级别,解决了幻读 ,它会在读取的每一行数据上都进行加锁, 有可能导致超时和锁争用的问题。
它的加锁实现是读取的时候加共享锁,修改删除更新的时候加排他锁,读写互斥,但是并发能力差。
隔离级别 | 脏读(Dirty Read) | 不可重复读(NonRepeatable Read) | 幻读(Phantom Read) |
---|---|---|---|
未提交读(Read uncommitted) | 可能 | 可能 | 可能 |
已提交读(Read committed) | 不可能 | 可能 | 可能 |
可重复读(Repeatable read) | 不可能 | 不可能 | 可能 |
可串行化(Serializable ) | 不可能 | 不可能 | 不可能 |
丢失更新: 当两个或者多个事务同时对某一数据进行更新的时候,事务B的更新可能覆盖掉事务A的更新,导致更新丢失
解决方案:
悲观锁的方式: 加锁,建议最后一步更新数据的时候加上排它锁,不要在一开始就加锁
执行到了最后一步更新,首先做一下加锁的查询确认数据有没有没改变,如果没有被改变,则进行数据的更新,否则失败。 一定要是做加锁的查询确认,因为如果你不加锁的话,有可能你在做确认的时候数据又发生了改变。
乐观锁的方式:使用版本控制实现
级别高低是:脏读 < 不可重复读 < 幻读。所以,设置了*别的SERIALIZABLE_READ就不用在设置REPEATABLE_READ和READ_COMMITTED了
脏读: 事务可以读取未提交的数据,比如:
事务A对某一个数据data=1000 进行了修改: data = 2000, 但是还没有提交;
事务B读取data 得到了结果data = 2000,
由于某种原因事务A撤销了刚才的操作,数据data = 1000 然后提交
这时事务B读取到的2000就是脏数据。正确的数据应该还是 1000
解决方法 : 把数据库的事务隔离级别调整到READ_COMMITTED , 但是存在事务A与B都读取了data,A还未完成事务,B此时修改了数据data,并提交, A又读取了data,发现data不一致了,出现了不可重复读。
不可重复读 在同一个事务之中,多次读取相同的记录的值的结果是不一样的,针对的是数据的修改和删除。
事务A 读取data = 1000, 事务还未完成;
事务B 修改了data = 2000, 修改完毕事务提交;
事务A 再次读取data, 发现data = 2000 了,与之前的读取不一致的
解决办法; 把数据库的事务隔离级别调整到 REPEATABLE READ , 读取时候不允许其他事务修改该数据,不管数据在事务过程中读取多少次,数据都是一致的,避免了不可重复读问题
幻读: 当某个事务在读取某个范围内的记录的时候,另外一个事务在这个范围内增加了一行,当前一个事务再次读取该范围的数据的时候就会发生幻行,. 针对的是数据的插入insert
解决方案 : 采用的是范围锁 RangeS RangeS_S模式,锁定检索范围为只读 或者 把数据库的事务隔离级别调整到SERIALIZABLE_READ, MySQL中InnoDB 和 XtraDB 利用(多版本并发控制)解决了幻读问题,
加锁协议
一次*协议:因为有大量的并发访问,为了预防死锁,一般应用中推荐使用一次*法,就是在方法的开始阶段,已经预先知道会用到哪些数据,然后全部锁住,在方法运行之后,再全部解锁。这种方式可以有效的避免循环死锁,但在数据库中却不适用,因为在事务开始阶段,数据库并不知道会用到哪些数据。
两段锁协议 将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁)
1. 加锁阶段:在该阶段可以进行加锁操作。在对任何数据进行读操作之前要申请并获得S锁(共享锁,其它事务可以继续加共享锁,但不能加排它锁),在进行写操作之前要申请并获得X锁(排它锁(只有当前数据无共享锁,无排它锁之后才能获得),其它事务不能再获得任何锁)。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。
2. 解锁阶段:当事务释放了一个*以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。
事务提交时(commit) 和事务回滚时(rollback)会自动的同时释放该事务所加的insert、update、delete对应的锁。
这种方式虽然无法避免死锁,但是两段锁协议可以保证事务的并发调度是串行化(串行化很重要,尤其是在数据恢复和备份的时候)的。
死锁 指两个事务或者多个事务在同一资源上相互占用,并请求对方所占用的资源,从而造成恶性循环的现象。
出现死锁的原因:
1. 系统资源不足
2. 进程运行推进的顺序不当
3. 资源分配不当
产生死锁的四个必要条件
1. 互斥条件: 一个资源只能被一个进程使用
2. 请求和保持条件:进行获得一定资源,又对其他资源发起了请求,但是其他资源被其他线程占用,请求阻塞,但是也不会释放自己占用的资源。
3. 不可剥夺条件: 指进程所获得的资源,不可能被其他进程剥夺,只能自己释放
4. 环路等待条件: 进程发生死锁,必然存在着进程-资源之间的环形链
处理死锁的方法: 预防,避免,检查,解除死锁
数据库也会发生死锁的现象,数据库系统实现了各种死锁检测和死锁超时机制来解除死锁,锁监视器进行死锁检测,
MySQL的InnoDB处理死锁的方式是 将持有最少行级排它锁的事务进行回滚,相对比较简单的死锁回滚办法
如何避免死锁?
避免死锁的核心思想是:系统对进程发出每一个资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,否则予以分配.这是一种保证系统不进入不安全或者死锁状态的动态策略。 什么是不安全的状态?系统能按某种进程推进顺序( P1, P2, …, Pn),为每个进程Pi分配其所需资源,直至满足每个进程对资源的最大需求,使每个进程都可顺序地完成。此时称 P1, P2, …, Pn 为安全序列。如果系统无法找到一个安全序列,则称系统处于不安全状态。
其实第一和第二是预防死锁的方式,分别对应着的是破坏循环等待条件,和破坏不可剥夺条件。
第一: 加锁顺序: 对所有的资源加上序号,确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生,比如有资源 A, B,规定所有的线程只能按照A–B的方式获取资源, 这样就不会发生 线程1持有A,请求B,线程2持有B请求A的死锁情况发生了
第二: 获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求,同时放弃掉自己已经成功获得的所有资源的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行。
第三:死锁的提前检测, 很出名的就是银行家算法。 每当一个线程获得了锁,会存储在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中,当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。
银行家算法: 思想:
当进程首次申请资源时,要测试该进程对资源的最大需求量,如果系统现存的资源可以满足它的最大需求量则按当前的申请量分配资源,否则就推迟分配。当进程在执行中继续申请资源时,先测试该进程已占用的资源数与本次申请的资源数之和是否超过了该进程对资源的最大需求量。若超过则拒绝分配资源,若没有超过则再测试系统现存的资源能否满足该进程尚需的最大资源量,若能满足则按当前的申请量分配资源,否则也要推迟分配
如何预防死锁?
主要是通过设置某些外部条件去破坏死锁产生的四个必要条件中的一个或者几个。
破坏互斥条件,一般不采用,因为资源的互斥性这个特性有时候是我们所需要的;
破坏请求和保持条件:可以一次性为一个进程或者线程分配它所需要的全部资源,这样在后面就不会发起请求资源的情况,但是这样资源的效率利用率很低;
破坏不可剥夺条件: 当一个已保持了某些不可剥夺资源的进程,请求新的资源而得不到满足时,它必须释放已经保持的所有资源,待以后需要时再重新申请,但是释放已获得的资源可能造成前一阶段工作的失效,反复地申请和释放资源会增加系统开销,降低系统吞吐量;
破坏循环等待条件: ,可釆用顺序资源分配法。首先给系统中的资源编号,规定每个进程,必须按编号递增的顺序请求资源,同类资源一次申请完。也就是说,只要进程提出申请分配资源Ri,则该进程在以后的资源申请中,只能申请编号大于Ri的资源。
但是这样的话,编号必须相对稳定,这就限制了新类型设备的增加;尽管在为资源编号时已考虑到大多数作业实际使用这些资源的顺序,但也经常会发生作业使甩资源的顺序与系统规定顺序不同的情况,造成资源的浪费;此外,这种按规定次序申请资源的方法,也必然会给用户的编程带来麻烦
InnoDB 中事务隔离性的实现:
READ COMMITED 和 REPEATABLE READ 的隔离性实现:MVCC
MVCC(多版本控制系统)的实现(目的: 实现更好的并发,可以使得大部分的读操作不用加锁, 但是insert,delete,update是需要加锁的):
MVCC 只在 READ COMMITED 和 REPEATABLE READ 这两个事务隔离性级别中使用。这是因为MVCC 和其他两个不兼容,READ UNCOMMITED 总是读取最新的行,不关事务, 而Seriablizable则会对每一个读都加共享锁。
在InnoDB中,会在每行数据后添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(即何时被删除)。 在实际操作中,存储的并不是时间,而是系统的版本号,每开启一个新事务,系统的版本号就会递增。
通过MVCC,虽然每行记录都需要额外的存储空间,更多的行检查工作以及一些额外的维护工作,但可以减少锁的使用,大多数读操作都不用加锁,读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行,也只锁住必要行。
select (不加锁): 满足两个条件的结果才会被返回:
1. 创建版本号<= 当前事务版本号,小于意味着在该事务之前没有其他事务对其进行修改,等于意味着事务自身对其进行了修改;
2. 删除版本号 > 当前事务版本号 意味着删除操作是在当前事务之后进行的,或者删除版本未定义,意味着这一行只是进行了插入,还没有删除过。
INSERT ; 为新插入的每一行保存当前事务的版本号作为创建版本号
DELETE ; 为删除的行保存当前事务的版本号为删除版本号
UPDATE; 为修改的每一行保存当前事务的版本号作为创建版本号
“读”与“读”的区别
MySQL中的读,和事务隔离级别中的读,是不一样的, 在REPEATABLE READ 级别中,通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,是不及时的数据(存储在缓存等地方的数据),不是数据库当前的数据!这在一些对于数据的时效特别敏感的业务中,就很可能出问题。
对于这种读取历史数据(缓存数据)的方式,我们叫它快照读 (snapshot read),而读取数据库当前版本数据的方式,叫当前读 (current read)。很显然,在MVCC中:
快照读:就是select ,是不加锁的, 在REPEATABLE READ 和READ COMMITED 级别中 select语句不加锁。
select * from table ….;
当前读:插入/更新/删除操作,属于当前读,处理的都是当前的数据,需要加锁。
select * from table where ? lock in share mode;
select * from table where ? for update;
insert;
update ;
delete;
事务的隔离级别实际上都是定义了当前读的级别,MySQL为了减少锁处理(包括等待其它锁)的时间,提升并发能力,引入了快照读的概念,使得select不用加锁。而update、insert这些“当前读”,就需要另外的模块来解决了。(这是因为update、insert的时候肯定要读取数据库中的值来与当前事务要写入的值进行对比,看看在该事务所处理的数据在这一段时间内有没有被其他事务所操作(就是先读取数据库中数据的版本号与当前的版本号做检查))
为了解决当前读中的幻读问题,MySQL事务使用了Next-Key锁。Next-Key锁是行锁和GAP(间隙锁)的合并
GAP(间隙锁)就是在两个数据行之间进行加锁,防止插入操作
行锁防止别的事务修改或删除,解决了数据不可重复读的问题
行锁防止别的事务修改或删除,GAP锁防止别的事务新增,行锁和GAP锁结合形成的的Next-Key锁共同解决了RR级别在读数据时的幻读问题
InnoDB 中 Serializable 的隔离性实现
Serializable级别使用的是悲观锁的理论, 读加共享锁,写加排他锁,读写互斥, 在Serializable这个级别,select语句还是会加锁的。
应用场景
ORM框架中悲观锁乐观锁的应用
一般悲观锁、乐观锁都需要都通过sql语句的设定、数据的设计结合代码来实现,例如乐观锁中的版本号字段,单纯面向数据库操作,是需要自己来实现乐观锁的,简言之,也就是版本号或时间戳字段的维护是程序自己维护的,自增、判断大小确定是否更新都通过代码判断实现。数据库进提供了乐观、悲观两个思路进行并发控制。
对于常用java 持久化框架,对于数据库的这一机制都有自己的实现,以Hibernate为例,总结一下ORM框架中悲观锁乐观锁的应用
1、Hibernate的悲观锁:
基于数据库的锁机制实现。如下查询语句:
String hqlStr ="from TUser as user where user.name=Max";
Query query = session.createQuery(hqlStr);
query.setLockMode("user",LockMode.UPGRADE); //加锁
List userList = query.list();//执行查询,获取数据
观察运行期Hibernate生成的SQL语句:
select
tuser0_.id as id, tuser0_.name as name, tuser0_.group_id as group_id,
tuser0_.user_type as user_type, tuser0_.sex as sex
from t_user tuser0_
where (tuser0_.name='Erica' )
for update
这里Hibernate通过使用数据库的for update子句实现了悲观锁机制。对返回的所有user记录进行加锁。
2、Hibernate的加锁模式有:
LockMode.NONE : 无锁机制。
LockMode.WRITE :Hibernate在写操作(Insert和Update)时会自动获取写锁。
LockMode.READ : Hibernate在读取记录的时候会自动获取。
这三种锁机制一般由Hibernate内部使用,如Hibernate为了保证Update过程中对象不会被外界修改,会在save方法实现中自动为目标对象加上WRITE锁。
LockMode.UPGRADE :利用数据库的for update子句加锁。
LockMode. UPGRADE_NOWAIT :Oracle的特定实现,利用Oracle的for update nowait子句实现加锁。
注意,只有在查询开始之前(也就是Hiberate 生成SQL 之前)设定加锁,才会真正通过数据库的锁机制进行加锁处理,否则,数据已经通过不包含for update子句的Select SQL加载进来,所谓数据库加锁也就无从谈起。
3、Hibernate的乐观锁
Hibernate 在其数据访问引擎中内置了乐观锁实现。如果不用考虑外部系统对数据库的更新操作,利用Hibernate提供的透明化乐观锁实现,将大大提升我们的生产力。Hibernate中可以通过class描述符的optimistic-lock属性结合version描述符指定。具体实现方式如下:
现在,我们为之前示例中的TUser加上乐观锁机制。
实现一、 配置optimistic-lock属性:
<hibernate-mapping>
<class name="org.hibernate.sample.TUser" table="t_user"
dynamic-update="true" dynamic-insert="true" optimistic-lock="version">
……
</class>
</hibernate-mapping>
optimistic-lock属性有如下可选取值:
none:无乐观锁
version:通过版本机制实现乐观锁
dirty:通过检查发生变动过的属性实现乐观锁
all:通过检查所有属性实现乐观锁
通过version实现的乐观锁机制是Hibernate官方推荐的乐观锁实现,同时也是Hibernate中,目前唯一在数据对象脱离Session发生修改的情况下依然有效的锁机制。因此,一般情况下,我们都选择version方式作为Hibernate乐观锁实现机制。
实现二、添加一个Version属性描述符
<hibernate-mapping>
<class name="org.hibernate.sample.TUser" table="t_user"
dynamic-update="true" dynamic-insert="true" optimistic-lock="version">
<id name="id" column="id" type="java.lang.Integer">
<generator class="native"/>
</id>
<version column="version" name="version" type="java.lang.Integer"/>
……
</class>
</hibernate-mapping>
注意version 节点必须出现在ID 节点之后。这里声明了一个version属性,用于存放用户的版本信息,保存在TUser表的version字段中。
测试:
此时如果我们尝试编写一段代码,更新TUser表中记录数据,如:
Criteria criteria = session.createCriteria(TUser.class);
criteria.add(Expression.eq("name","Max"));
List userList = criteria.list();
TUser user =(TUser)userList.get(0);
Transaction tx = session.beginTransaction();
user.setUserType(1); //更新UserType字段
tx.commit();
每次对TUser进行更新的时候,我们可以发现,数据库中的version都在递增。而如果我们尝试在tx.commit 之前,启动另外一个Session,对名为Max的用户进行操作,下面模拟并发更新时的情况:
Session session= getSession();
Criteria criteria = session.createCriteria(TUser.class);
criteria.add(Expression.eq("name","Max"));
Session session2 = getSession();
Criteria criteria2 = session2.createCriteria(TUser.class);
criteria2.add(Expression.eq("name","Max"));
List userList = criteria.list();
List userList2 = criteria2.list();TUser user =(TUser)userList.get(0);
TUser user2 =(TUser)userList2.get(0);
Transaction tx = session.beginTransaction();
Transaction tx2 = session2.beginTransaction();
user2.setUserType(99);
tx2.commit();
user.setUserType(1);
tx.commit();
执行并发更新的代码,在tx.commit()处抛出StaleObjectStateException异常,并指出版本检查失败,当前事务正在试图提交一个过期数据。通过捕捉这个异常,我们就可以在乐观锁校验失败时进行相应处理。
这就是hibernate实现悲观锁和乐观锁的主要方式。