目录
1 悲观锁
所谓的悲观锁:顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次拿数据的时候都会上锁。这样别人拿数据的时候就要等待直到锁的释放。
数据库行级锁,目的是让数据被查出来的时候就加上锁,然后再执行下面的程序逻辑,这样后面为了操作相同数据而进来的请求,就会在一开始就被拦住(这种效果千万不要以为可以做防重复提交)
在操作DML(insert,update,delete)语句时,oracle会自动加上行级锁,在select * from table for update 【of column】【nowait|wait 3】时,oracle也会自动加锁
1.1 单表 for update
一般在for update 时加nowait,这样就不用等待其他事务执行了,一判断有事务,立马抛出错误。
下面简单说一下 for update的四种情况:
- select * from table where id = '1001' for update 锁住了这条数据,那么另外一个人对该笔数据进行DML操作或者也执行同样的for update操作时,会检测到这笔数据上有行级锁,那么就会等待着锁释放;这样就会出现一个问题:其他的程序如果需要对这笔数据操作,就需要等,至于等多久要看锁什么时候释放!
- select * from table where id = '1001' for update nowait,意思就是如果这笔数据上本身加了锁,另外一个人去执行这句SQL的时候,发现加了锁,就会直接抛出异常(ORA-00054:资源正忙),不会等待这笔数据的锁释放。
- select * from table where id = '1001' for update wait 5;意思就是如果这笔数据被锁住,另外一个人如果执行这句SQL后,会等待5秒,如果5秒后这句SQL还没有得到这笔数据的锁,就会抛出异常(ORA-00054:资源正忙)
- 先执行 A语句:select * from table where id = '1001' for update 把1001加上锁,然后再执行 B语句:select * from table where id = '1001' and id ='1002' for update;这时候肯定查不出来,因为A已经把B要加锁的数据锁了,这样B连1002的数据都查不出来。解决方案:skip locked。如果把B语句改为:select * from table where id = '1001' and id ='1002' for update skip locked;意思就是执行的时候如果发现要查询的数据有锁,就把加了锁的数据排除,把剩下的数据加锁,然后查出来!
上面讲到了 for update 的四种方式,实际情况如何选择呢?
关于NOWAIT,当有LOCK冲突时会提示错误并结束STATEMENT而不是在那里等待(比如:要查的行已经被其它事务锁了,当前的锁事务与之冲突,加上nowait,当前的事务会结束会提示错误并立即结束 STATEMENT而不再等待).
WAIT 子句指定等待其他用户释放锁的秒数,防止无限期的等待。
“使用FOR UPDATE WAIT”子句的优点如下:
- 防止无限期地等待被锁定的行;
- 允许应用程序中对锁的等待时间进行更多的控制。
- 对于交互式应用程序非常有用,因为这些用户不能等待不确定
- 若使用了skip locked,则可以越过锁定的行,不会报告由wait n 引发的‘资源忙’异常报告
1.2 关联表for update
现在大部分业务都是联表查询,如果用for update 的话,就会把所有关联表查询出来的列所在的行全部加锁,那这个锁可就重了,比如:
select * from t1,t2 where t1.id = t2.id and t1.age = '20' for update
就会把T1和T2两个表中符合条件的行锁定;如果上述SQL我只想对T1表的结果集加锁,怎么办?答案:of column_name
例子:
select * from t1,t2 where t1.id = t2.id and t1.age = '20' for update of t1.id;
这样就会只把T1表中的符合条件的行加锁,T2表中符合条件的行不会加锁。
PS:如果单表for update of column_name查询,其实和 for update操作是一样的!
1.3 解除for update 锁的占用
如果在为了方便修改表时,使用了for update 就容易把表锁住,如果这时候及时的commit或者rollback还能释放锁
如果,在提交或者释放之前这个plsql会话连接断了,或者别人占有锁而自己也想操作锁,可以如下操作:
查看锁表进程
select
t2.username,t2.sid,t2.serial#,t2.logon_time
from
v$locked_object t1,v$session t2
where t1.session_id=t2.sid ;
解锁杀死锁表进程
// kill的数字即是,锁表进程中的SID和SERIAL字段的值,把所有的值全部杀掉即可
alter system kill session '1155,39095';
1.4 悲观锁缺点
虽然悲观锁应用起来很简单并且十分安全,与此同时却有两大问题:
- 锁定:应用的使用者选择一个记录进行更新,然后去吃午饭,但是没有结束或者丢弃该事务。这样其他所有需要更新该记录的用户就必须等待正在进行实务操作的用户回来并且完成该事务或者直到DBA杀掉该不愉快的事务并且释放锁。
- 死锁:用户A和B同时更新数据库。用户A锁定了一条记录并且试图请求用户B持有的锁,同时用户B也在等待获取用户A持有的锁。两个事务同时进入了无限等待状态即进入死锁状态。
2 乐观锁
所谓的乐观锁:就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。oracle默认使用乐观锁
在乐观锁中,我们有3种常用的做法来实现:
2.1 比对法
第一种就是在数据取得的时候把整个数据都copy到应用中,在进行提交的时候比对当前数据库中的数据和开始的时候更新前取得的数据。当发现两个数据一模一样以后,就表示没有冲突可以提交,否则则是并发冲突,需要去用业务逻辑进行解决。
2.2 版本戳
第二种乐观锁的做法就是采用版本戳,这个在Hibernate中得到了使用。采用版本戳的话,首先需要在你有乐观锁的数据库table上建立一个新的column,比如为number型,当你数据每更新一次的时候,版本数就会往上增加1。比如同样有2个session同样对某条数据进行操作。两者都取到当前的数据的版本号为1,当第一个session进行数据更新后,在提交的时候查看到当前数据的版本还为1,和自己一开始取到的版本相同。就正式提交,然后把版本号增加1,这个时候当前数据的版本为2。当第二个session也更新了数据提交的时候,发现数据库中版本为2,和一开始这个session取到的版本号不一致,就知道别人更新过此条数据,这个时候再进行业务处理,比如整个Transaction都Rollback等等操作。在用版本戳的时候,可以在应用程序侧使用版本戳的验证,也可以在数据库侧采用Trigger(触发器)来进行验证。不过数据库的Trigger的性能开销还是比较的大,所以能在应用侧进行验证的话还是推荐不用Trigger。
2.3 timestamp型
第三种做法和第二种做法有点类似,就是也新增一个Table的Column,不过这次这个column是采用timestamp型,存储数据最后更新的时间。在Oracle9i以后可以采用新的数据类型,也就是timestamp with time zone类型来做时间戳。这种Timestamp的数据精度在Oracle的时间类型中是最高的,精确到微秒(还没与到纳秒的级别),一般来说,加上数据库处理时间和人的思考动作时间,微秒级别是非常非常够了,其实只要精确到毫秒甚至秒都应该没有什么问题。和刚才的版本戳类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。如果不想把代码写在程序中或者由于别的原因无法把代码写在现有的程序中,也可以把这个时间戳乐观锁逻辑写在Trigger或者存储过程中。
2.4 例子Demo
说明:小明成绩错了,要改成绩。班主任能改,年级主任也能改!
//先查出来小明的成绩
select t.id,t.result from T t where t.id='10001';---10001,59
//更新成绩,改为60
update T t set t.result =‘60’ where t.id='10001' and t.result = '59'
//加上and t.result = '59' 这个条件的目的就是为了验证,数据库里10001的成绩在此期间有没有被其他人改过,如果改过,那就更新条数为0(因为找不到符合条件的数据);
PS:没有找到数据,所以没更新10001这笔数据,最好是程序返回一个没有更新到这笔数据的提示,如果不加任何提示,前端就会认为更新成功了!
1.利用数据库中的数据和已经取出的数据的一致性做为“锁”,与for update相比,乐观锁机制是等到更改数据的时候才去校验,悲观锁是读取数据就开始做了校验,从这个角度来看,乐观锁是对数据库没有额外开销,那么效率相对是高的。
2. 需要更改的字段可以作为乐观锁的验证字段;或者表里建立version版本号,每更新一次数据版本号+1;或者加lastupdatedate(最后更新时间),同理:数据更改的同时lastupdatedate也跟着变更!
3.其实乐观锁存在一个很致命的问题:
场景: 已上述小明改成绩为例,假设班主任改的同时,年级主任也改,两个请求几乎同时执行了查询:
select t.id,t.result from T t where t.id='10001';---10001,59
都查出来是59分!!
然后几乎同时执行了改成绩,班主任改成60分,年级主任改成了80分,关键是还都update到10001了班主任:
update T t set t.result =‘60’ where t.id='10001' --and t.result = '59' ;
年级主任:
update T t set t.result =‘80’ where t.id='10001' --and t.result = '59' ;
这时候班主任事务先提交,数据库小明成绩改成了60,年级主任事务紧接着提交,小明的成绩又从60改成了80,那么对于班主任来说,他的数据就是更新丢失!!!所以大家使用起来要注意并发的情况!!
附录:排他锁与共享锁
通过DML语句对一张表的某一行数据进行修改,一个事务开始,背后的步骤是:
1.对这张表加一个共享锁。这么做是为了防止别的会话通过DDL语句修改这张表的表结构。DDL语句要修改了这张表,就必须给表加上排他锁。但是现在给表加了共享锁了,也就排斥了DDL去加排他锁;
2.对修改的那一行加一个排他锁,别的会话不能修改这一行。但是我对整张表加的是共享锁而不是排他锁,所以别的会话还是可以修改其他行(也经历1、2两个步骤)。