对于不同的数据库来说,锁定都是核心的组件,直接影响到数据库的选择,跨数据库的迁移等。我们来了解一下 Oracle 的乐观锁定和悲观锁定。
Oracle的锁机制主要分为行锁和表锁,行锁即锁定表中的某行数据,表锁锁定表中所有数据。锁定的数据不能插入,更新,删除,只能查询,语法 for update。锁的周期为一次数据提交,一次数据提交中可能会有多条SQL语句。
在大并发中为了保证某些数据的唯一性,常用到锁的机制。
表锁:
线程1:select * from user for update;
线程2:update user set name=’123’ <- 堵塞
update user set name=’123’ where id = 1 <- 堵塞
user表被锁定,其他进程无法修改表中数据,只能查询。
行锁:
线程1:select * from user where id = 1 for update;
线程2:update user set name=’123’ <- 堵塞
update user set name=’123’ where id = 1 <- 堵塞
update user set name=’123’ where id = 2 <- 正常
user表中id=1数据被锁定,其他进程无法修改user该条数据。
悲观锁即为独占锁,A线程操作a表a1数据,其他线程需等待A的事务提交后才能操作a1数据。
乐观锁通过控制版本号实现数据是否提交,A线程操作a表a1数据(读取当前表版本号1),事务提交改为版本号1+1=2。B线程在A线程未提交亦执行相同操作,版本号1+1=2<2,阻止该操作update a set a1=new and version.a=version.a+1 where version = version.a,允许版本号不连续覆盖的话version >= version.a。
表锁行锁针对的是数据库,乐观锁,悲观锁是一种概念.我们接下来看一下所谓的 乐观锁和悲观锁的更详细说明。
一、问题引出
1. 假设当当网上用户下单买了本书,这时数据库中有条订单号为001的订单,其中有个status字段是’有效’,表示该订单是有效的;
2. 后台管理人员查询到这条001的订单,并且看到状态是有效的;
3. 用户发现下单的时候下错了,于是撤销订单,假设运行这样一条SQL: update order_table set status = ‘取消’ where order_id = 001;
4. 后台管理人员由于在②这步看到状态有效的,这时,虽然用户在③这步已经撤销了订单,可是管理人员并未刷新界面,看到的订单状态还是有效的,于是点击”发货”按钮,将该订单发到物流部门,同时运行类似如下SQL,将订单状态改成已发货:update order_table set status = ‘已发货’ where order_id = 001;
<<< 上面的问题应该如何解决呢? 请见下文分解 >>>
二、悲观锁
所谓的悲观锁:顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次拿数据的时候都会上锁。这样别人拿数据的时候就要等待直到锁的释放。
Oracle的悲观锁需要利用一条现有的Connection,它分成两种方式,从SQL语句的区别来看,就是一种是select for update,一种是select for update nowait的形式。
1. 执行select xxx for update操作时,数据会被锁定,只有执行comit或rollover才会释放
2. 执行select xxx for update nowait操作时,数据也会被锁定,其他人访问时或返回ORA-00054错误,内容是资源正忙,需要采取相应的业务措施进行处理。
虽然悲观锁应用起来很简单并且十分安全,与此同时却有两大问题:
1. 锁定:应用的使用者选择一个记录进行更新,然后去吃午饭,但是没有结束或者丢弃该事务。这样其他所有需要更新该记录的用户就必须等待正在进行实务操作的用户回来并且完成该事务或者直到DBA杀掉该不愉快的事务并且释放锁。
2. 死锁:用户A和B同时更新数据库。用户A锁定了一条记录并且试图请求用户B持有的锁,同时用户B也在等待获取用户A持有的锁。两个事务同时进入了无限等待状态即进入死锁状态。
三、乐观锁
所谓的乐观锁:就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。oracle默认使用乐观锁。
在乐观锁中,我们有3种常用的做法来实现:
-
第一种就是在数据取得的时候把整个数据都copy到应用中,在进行提交的时候比对当前数据库中的数据和开始的时候更新前取得的数据。当发现两个数据一模一样以后,就表示没有冲突可以提交,否则则是并发冲突,需要去用业务逻辑进行解决。
-
第二种乐观锁的做法就是采用版本戳,这个在Hibernate中得到了使用。采用版本戳的话,首先需要在你有乐观锁的数据库table上建立一个新的column,比如为number型,当你数据每更新一次的时候,版本数就会往上增加1。比如同样有2个session同样对某条数据进行操作。两者都取到当前的数据的版本号为1,当第一个session进行数据更新后,在提交的时候查看到当前数据的版本还为1,和自己一开始取到的版本相同。就正式提交,然后把版本号增加1,这个时候当前数据的版本为2。当第二个session也更新了数据提交的时候,发现数据库中版本为2,和一开始这个session取到的版本号不一致,就知道别人更新过此条数据,这个时候再进行业务处理,比如整个Transaction都Rollback等等操作。在用版本戳的时候,可以在应用程序侧使用版本戳的验证,也可以在数据库侧采用Trigger(触发器)来进行验证。不过数据库的Trigger的性能开销还是比较的大,所以能在应用侧进行验证的话还是推荐不用Trigger。
-
第三种做法和第二种做法有点类似,就是也新增一个Table的Column,不过这次这个column是采用timestamp型,存储数据最后更新的时间。在Oracle9i以后可以采用新的数据类型,也就是timestamp with time zone类型来做时间戳。这种Timestamp的数据精度在Oracle的时间类型中是最高的,精确到微秒(还没与到纳秒的级别),一般来说,加上数据库处理时间和人的思考动作时间,微秒级别是非常非常够了,其实只要精确到毫秒甚至秒都应该没有什么问题。和刚才的版本戳类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。如果不想把代码写在程序中或者由于别的原因无法把代码写在现有的程序中,也可以把这个时间戳乐观锁逻辑写在Trigger或者存储过程中。
四、常见问题
引用一下 wiki的定义:
There are mechanisms employed to manage the actions of multiple concurrent users on a database – the purpose is to prevent lost updates and dirty reads. The two types of locking are Pessimistic and Optimistic Locking.
Pessimistic locking: A user who reads a record, with the intention of updating it, places an exclusive lock on the record to prevent other users from manipulating it. This means no one else can manipulate that record until the user releases the lock. The downside is that users can be locked out for a very long time, thereby slowing the overall system response and causing frustration.
Where to use pessimistic locking: This is mainly used in environments where data-contention (the degree of users request to the database system at any one time) is heavy; where the cost of protecting data through locks is less than the cost of rolling back transactions if concurrency conflicts occur. Pessimistic concurrency is best implemented when lock times will be short, as in programmatic processing of records. Pessimistic concurrency requires a persistent connection to the database and is not a scalable option when users are interacting with data, because records might be locked for relatively large periods of time. It is not appropriate for use in web application development.
本质上,这里wiki的意思就是,悲观锁和乐观锁都是为了解决丢失更新问题或者是脏读。悲观锁和乐观锁的重点就是是否在读取记录的时候直接上锁。悲观锁的缺点很明显,需要一个持续的数据库连接,这在web应用中已经不适合了。
**观点:悲观锁是指一个用户在更新数据的时候,其他用户不能读取这条记录;也就是update阻塞读才叫悲观锁;
**
分析:这个观点是错的;
这在db2背景的开发中尤其常见;因为db2默认就是update会阻塞读;但是这是各个数据库对读写的时候上锁的并发处理实现不一样。但这根本不是悲观锁乐观锁的区别。Oracle可以做到写不阻塞读仅仅是因为做了多版本并发控制(Multiversion concurrency control)
但是在Oracle里面,一样可以做乐观锁和悲观锁的控制。这本质上是应用层面的选择。
观点:Oracle实际上用的就是乐观锁
分析:这个观点是错的;
前面说了,Oracle的确可以做到写不阻塞读,但是这不是悲观锁和乐观锁的问题。这是因为实现了多版本并发控制。按照wiki的定义,悲观锁和乐观锁是在应用层面选择的。Oracle的应用只要在第二步做了select for update,就是悲观锁的做法;
况且Oracle在任何隔离级别下,除了分布式事务两阶段提交的短暂时间,其他所有情况下都不存在写阻塞读的情况,如果按照这个观点的话那Oracle已经不能做悲观锁了
五、结论
-
如果系统并发量不大且不允许脏读,可以使用悲观锁解决并发问题。
-
如果系统并发非常大的话,悲观锁会带来很大性能问题,所以一般采用乐观锁。
-
如果系统读比较多,写比较少,也应该使用乐观锁,可以提高吞吐量。
-
现在大部分应用都应该是乐观锁的;