数据库乐观锁和悲观锁的理解和实现(转载总结)
数据的锁定分为两种,第⼀种叫作悲观锁,第⼆种叫作乐观锁。
1、悲观锁,就是对数据的冲突采取⼀种悲观的态度,也就是说假设数据肯定会冲突,所以在数据开始读取的时候就把数据锁定住。【数据锁定:数据将暂时不会得到修改】
2、乐观锁,认为数据⼀般情况下不会造成冲突,所以在数据进⾏提交更新的时候,才会正式对数据的冲突与否进⾏检测,如果发现冲突了,则让⽤户返回错误的信息。让⽤户决定如何去做。
理解:
1. 乐观锁是⼀种思想,具体实现是,表中有⼀个版本字段,第⼀次读的时候,获取到这个字段。处理完业务逻辑开始更新的时候,需要再次查看该字段的值是否和第⼀次的⼀样。如果⼀样更新,反之拒绝。
之所以叫乐观,因为这个模式没有从数据库加锁。
2. 悲观锁是读取的时候为后⾯的更新加锁,之后再来的读操作都会等待。这种是数据库锁
乐观锁优点程序实现,不会存在死锁等问题。他的适⽤场景也相对乐观。阻⽌不了除了程序之外的数据库操作。
悲观锁是数据库实现,他阻⽌⼀切数据库操作。
再来说更新数据丢失,所有的读锁都是为了保持数据⼀致性。乐观锁如果有⼈在你之前更新了,你的更新应当是被拒绝的,可以让⽤户从新操作。悲观锁则会等待前⼀个更新完成。这也是区别。具体业务具体分析
实现:
⼀、悲观锁
1、排它锁,当事务在操作数据时把这部分数据进⾏锁定,直到操作完毕后再解锁,其他事务操作才可操作该部分数据。这将防⽌其他进程读取或修改表中的数据。
2、实现:⼤多数情况下依靠数据库的锁机制实现
⼀般使⽤ select ...for update 对所选择的数据进⾏加锁处理,例如select * from account where name=”Max” for update,这条sql 语句锁定了account 表中所有符合检索条件(name=”Max”)的记录。本次事务提交之前(事务提交时会释放事务过程中的锁),外界⽆法修改这些记录。
⼆、乐观锁
1、如果有⼈在你之前更新了,你的更新应当是被拒绝的,可以让⽤户重新操作。
2、实现:⼤多数基于数据版本(Version)记录机制实现
具体可通过给表加⼀个版本号或时间戳字段实现,当读取数据时,将version字段的值⼀同读出,数据每更新⼀次,对此version值加⼀。当我们提交更新的时候,判断当前版本信息与第⼀次取出来的版本值⼤⼩,如果数据库表当前版本号与第⼀次取出来的version值相等,则予以更新,否则认为是过期数据,拒绝更新,让⽤户重新操作。
三、ORM框架中悲观锁乐观锁的应⽤
⼀般悲观锁、乐观锁都需要都通过sql语句的设定、数据的设计结合代码来实现,例如乐观锁中的版本号字段,单纯⾯向数据库操作,是需要⾃⼰来实现乐观锁的,简⾔之,也就是版本号或时间戳字段的维护是程序⾃⼰维护的,⾃增、判断⼤⼩确定是否更新都通过代码判断实现。数据库进提供了乐观、悲观两个思路进⾏并发控制。
对于常⽤java 持久化框架,对于数据库的这⼀机制都有⾃⼰的实现,以Hibernate为例,总结⼀下ORM框架中悲观锁乐观锁的应⽤
1、Hibernate的悲观锁:
基于数据库的锁机制实现。如下查询语句:
[html]
1. String hqlStr ="from TUser as user where user.name=Max";
2. Query query = ateQuery(hqlStr);
3. query.setLockMode("user",LockMode.UPGRADE); //加锁
4. List userList = query.list();//执⾏查询,获取数据
观察运⾏期Hibernate⽣成的SQL语句:
[html]
1. 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属性:
[html]
1. <hibernate-mapping>
2. <class name="org.hibernate.sample.TUser"table="t_user"dynamic-update="true"dynamic-insert="true"optimistic-lock="version">
3. ……
4. </class>
5. </hibernate-mapping>
optimistic-lock属性有如下可选取值:
Ø none:⽆乐观锁
Ø version:通过版本机制实现乐观锁
Ø dirty:通过检查发⽣变动过的属性实现乐观锁
Ø all:通过检查所有属性实现乐观锁
通过version实现的乐观锁机制是Hibernate官⽅推荐的乐观锁实现,同时也是Hibernate中,⽬前唯⼀在数据对象脱离Session发⽣修改的情况下依然有效的锁机制。因此,⼀般情况下,我们都选择version⽅式作为Hibernate乐观锁实现机制。
实现⼆、添加⼀个Version属性描述符
[html]
1. <hibernate-mapping>
2. <class name="org.hibernate.sample.TUser"table="t_user"dynamic-update="true"dynamic-insert="tru
html实现用户注册登录代码e"optimistic-lock="version">
3. <id name="id"column="id"type="java.lang.Integer">
4. <generator class="native"/>
5. </id>
6. <version column="version"name="version"type="java.lang.Integer"/>
7. ……
8. </class>
9. </hibernate-mapping>
注意version 节点必须出现在ID 节点之后。这⾥声明了⼀个version属性,⽤于存放⽤户的版本信息,保存在TUser表的version字段中。
测试:
此时如果我们尝试编写⼀段代码,更新TUser表中记录数据,如:
[html]
1. Criteria criteria = ateCriteria(TUser.class);
2. criteria.add(Expression.eq("name","Max"));
3. List userList = criteria.list();
4. TUser user =((0);
5. Transaction tx = session.beginTransaction();
6. user.setUserType(1); //更新UserType字段
7. txmit();
每次对TUser进⾏更新的时候,我们可以发现,数据库中的version都在递增。⽽如果我们尝试在txmit 之前,启动另外⼀个Session,对名为Max的⽤户进⾏操作,下⾯模拟并发更新时的情况:
[html]
1. Session session= getSession();
2. Criteria criteria = ateCriteria(TUser.class);
3. criteria.add(Expression.eq("name","Max"));
4. Session session2 = getSession();
5. Criteria criteria2 = ateCriteria(TUser.class);
6. criteria2.add(Expression.eq("name","Max"));
7. List userList = criteria.list();
8. List userList2 = criteria2.list();TUser user =((0);
9. TUser user2 =((0);
10. Transaction tx = session.beginTransaction();
11. Transaction tx2 = session2.beginTransaction();
12. user2.setUserType(99);
13. tx2mit();
14. user.setUserType(1);
15. txmit();
执⾏并发更新的代码,在txmit()处抛出StaleObjectStateException异常,并指出版本检查失败,当前事务正在试图提交⼀个过期数据。通过捕捉这个异常,我们就可以在乐观锁校验失败时进⾏相应处理。
这就是hibernate实现悲观锁和乐观锁的主要⽅式。
四、总结
悲观锁相对⽐较谨慎,设想现实情况应该很容易就发⽣冲突,所以我还是独占数据资源吧。
乐观锁就想得开⽽且⾮常聪明,应该是不会有什么冲突的,我对表使⽤⼀个时间戳或者版本号,每次读、更新操作都对这个字段进⾏⽐对,如果在我之前已经有⼈对数据进⾏更新了,那就让它更新,⼤不了我再读⼀次或者再更新⼀次。
乐观锁的管理跟SVN管理代码版本的原理很像,如果在我提交代码之前⽤本地代码的版本号与服务器做对⽐,如果本地版本号⼩于服务器上最新版本号,则提交失败,产⽣冲突代码,让⽤户决定选择哪个版本继续使⽤。
在实际⽣产环境⾥边,如果并发量不⼤且不允许脏读,可以使⽤悲观锁;但如果系统的并发⾮常⼤的话,悲观锁定会带来⾮常⼤的性能问题,所以我们就要选择乐观锁定的⽅法另外,Mysql在处理并发访问数据上,还有添加读锁(共享锁)、写锁(排它锁),控制锁粒度【表锁(table lock)、⾏级锁(row lock)】等实现,有兴趣可以继续研究。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论