表锁和行锁
问:select * from person for update
或update person p set p.name ='张三'
或 delete from person
加的是行锁还是表锁?
答:表锁。
问:select * from person where id=1 for update
或者select * from person where id in(1001) for update
或update person p set p.name ='张三' where p.id=1
或delete from person p where p.id=610
加的是行锁还是表锁?
答:行锁
问:上面的锁是悲观锁还是乐观锁?
答:都是悲观锁。Oracle数据库锁机制是悲观锁,乐观锁一般是指用户自己实现的一种锁机制。
悲观锁
所谓的悲观锁:顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次拿数据的时候都会上锁,这样别人拿数据的时候就要等待直到锁的释放。
数据库行级锁就是一种悲观锁,就是在执行DML(insert,update,delete)语句时,Oracle都会将对应的行锁住,其他的数据库连接无法再对这些行进行DML操作。
Java 中 synchronized 和 Lock 等锁也是悲观锁思想的实现,即当前只会有一个线程对资源进行操作,其他线程则等待锁的释放后,成功获取到锁才能访问被锁的资源。
此外,分布式锁也是一种悲观锁。
乐观锁
乐观锁的应用场景举例:
- 假设当当网上用户下单买了本书,这时数据库中有条订单号为001的订单,其中有个status字段是’有效’,表示该订单是有效的;
- 后台管理人员查询到这条001的订单,并且看到状态是有效的;
- 用户发现下单的时候下错了,于是撤销订单,假设运行这样一条SQL:
update order_table set status = ‘取消’ where order_id = 001;
- 后台管理人员由于在②这步看到状态有效的,这时,虽然用户在③这步已经撤销了订单,可是管理人员并未刷新界面,看到的订单状态还是有效的,于是点击”发货”按钮,将该订单发到物流部门,同时运行类似如下SQL,将订单状态改成已发货:
update order_table set status = ‘已发货’ where order_id = 001;
所谓的乐观锁:就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。
应用是使用了乐观锁,但并不意味着Oracle的数据库锁就不存在了,表锁和行锁是仍然有效的。乐观锁只是在应用程序层面加个一个所机制
在乐观锁中,我们有3种常用的做法来实现:
比对法
第一种就是在数据取得的时候把整个数据都copy到应用中,在进行提交的时候比对当前数据库中的数据和开始的时候更新前取得的数据。当发现两个数据一模一样以后,就表示没有冲突可以提交,否则则是并发冲突,需要去用业务逻辑进行解决。
此种方法一般不使用,首先实现起来繁琐,此外也有CAS这种乐观锁的ABA问题。
版本戳
第二种乐观锁的做法就是采用版本戳,这个在Hibernate中得到了使用。采用版本戳的话,首先需要在你有乐观锁的数据库table上建立一个新的column,比如为number型,当你数据每更新一次的时候,版本数就会往上增加1。比如同样有2个session同样对某条数据进行操作。两者都取到当前的数据的版本号为1,当第一个session进行数据更新后,在提交的时候查看到当前数据的版本还为1,和自己一开始取到的版本相同,则提交成功,然后把版本号增加1,这个时候当前数据的版本为2。当第二个session也更新的时候,发现数据库中版本为2,和一开始这个session取到的版本号不一致,就知道别人更新过此条数据,这个时候再进行业务处理,比如整个Transaction都Rollback等操作。在用版本戳的时候,可以在应用程序侧使用版本戳的验证,也可以在数据库侧采用Trigger(触发器)来进行验证。不过数据库的Trigger的性能开销还是比较的大,所以能在应用侧进行验证的话还是尽量不用Trigger。
timestamp型
第三种做法和第二种做法有点类似,就是也新增一个Table的Column,不过这次这个column是采用timestamp型,存储数据最后更新的时间。在Oracle9i以后可以采用新的数据类型,也就是timestamp with time zone类型来做时间戳。这种Timestamp的数据精度在Oracle的时间类型中是最高的,精确到微秒(还没与到纳秒的级别),一般来说,加上数据库处理时间和人的思考动作时间,微秒级别是非常非常够了,其实只要精确到毫秒甚至秒都应该没有什么问题。和刚才的版本戳类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。如果不想把代码写在程序中或者由于别的原因无法把代码写在现有的程序中,也可以把这个时间戳乐观锁逻辑写在Trigger或者存储过程中。
悲观锁和乐观锁的对比
局限性
乐观锁与悲观锁相比,适用的场景受到了更多的限制,无论是CAS机制还是版本号机制。
比如,CAS机制只能保证单个变量操作的原子性,当涉及到多个变量的时候,CAS机制是无能为力的,而synchronized却可以通过对整个代码块进行加锁处理;再比如,版本号机制如果在查询数据的时候是针对表1,而更新数据的时候是针对表2,也很难通过简单的版本号来实现乐观锁。
竞争激烈程度
在竞争不激烈(出现并发冲突的概率比较小)的场景中,乐观锁更有优势。因为悲观锁会锁住代码块或数据,其他的线程无法同时访问,必须等待上一个线程释放锁才能进入操作,会影响并发的响应速度。另外,加锁和释放锁都需要消耗额外的系统资源,也会影响并发的处理速度。
在竞争激烈(出现并发冲突的概率较大)的场景中,悲观锁则更有优势。因为乐观锁在执行更新的时候,可能会因为数据被反复修改而更新失败,进而不断重试,造成CPU资源的浪费。
乐观锁是否会加锁
乐观锁本身是不加锁的,只有在更新的时候才会去判断数据是否被其他线程更新了,比如AtomicInteger便是一个例子。但是有时候乐观锁可能会与加锁操作合作,比如MySQL在执行更新数据操作的时候会加上排他锁。因此可以理解为乐观锁本身是不加锁的,只有在更新数据的时候才有可能会加锁。