详解 hibernate 悲观锁 乐观锁 深入分析 代码实例
首先,为什么要有锁这种概念和技术呢?
什么是锁( locking )
业务逻辑的实现过程中,往往需要保证数据访问的排他性。如在金融系统的日终结算处理中,我们希望针对某个 cut-off 时间点的数据进行处理,而不希望在结算进行过程中(可能是几秒种,也可能是几个小时),数据再发生变化。此时,我们就需要通过一些机制来保证这些数据在某个操作过程中不会被外界修改,这样的机制,在这里,也就是所谓的 “锁” ,即给我们选定的目标数据上锁,使其无法被其他程序修改。Hibernate
支持两种锁机制:即通常所说的 “悲观锁( Pessimistic Locking )”和 “乐观锁( Optimistic Locking )” 。
明白了吧,其实就为了在对数据进行操作的时候,保持一对一性,在你改数据的时候,就不允许其它别人来动。我总结为:一个数据一次同时只被一个人修改,这就是原则。
什么是悲观锁哟?
悲观锁( Pessimistic Locking )
悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库 性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。
select * from account where name="user" for update
什么是乐观锁呢?
乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
优点
缺点
添加属性
<hibernate-mapping> <class name="org.hibernate.sample.TUser" table="t_user" dynamic-update="true" dynamic-insert="true" optimistic-lock="version" > …… </class> </hibernate-mapping>
添加描述符
<class name="Version.Student" table="studentVersion" >
<id name="id" unsaved-value="null">
<generator class="uuid.hex"></generator>
</id>
<!--version标签必须跟在id标签后面,这个很特别,注意,和后面跟着的不一样-->
<version name="version" column="ver" type="int"></version> <property name="name" type="string" column="name"></property> </class>
其实不光可以对表进行操作,也可以细分对字段进行乐观锁:
<!-- 这一列用来记录版本号,和Student对象本身意义无关 -->
<version column="version" name="version" type="java.lang.Integer"/>
<!--
optimistic-lock="false"默认为true",fullname
和name使用了乐观锁,password没有使用乐观锁
-->
<property name="fullname" column="FULLNAME" optimistic-lock="true"/>
<property name="name" column="NAME"/>
<property name="password" column="PASSWORD" optimistic-lock="false"/>
Criteria criteria = session.createCriteria(TUser.class); criteria.add(Expression.eq("name","Erica")); List userList = criteria.list(); TUser user =(TUser)userList.get(0); Transaction tx = session.beginTransaction(); user.setUserType(1); // 更新 UserType 字段 tx.commit();
Session session= getSession(); Criteria criteria = session.createCriteria(TUser.class); criteria.add(Expression.eq("name","Erica")); Session session2 = getSession(); Criteria criteria2 = session2.createCriteria(TUser.class); criteria2.add(Expression.eq("name","Erica")); 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();
补充内容
Hibernate乐观锁实现方式有两种:Version和Timestamp
下面用代码来验证:
表和POJO就自己建吧,pojo好办,HIBERNATE自动映射了来的就可以了,没有什么要改动的地方。
重点讲最重要的地方
3、Student.hbm.xml
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <!-- Mapping file autogenerated by MyEclipse Persistence Tools --> <hibernate-mapping> <class name="com.hy.model.PBagReceive" table="P_BagReceive" dynamic-update="true" dynamic-insert="true" optimistic-lock="version" catalog="hy_management"> <id name="id" type="java.lang.Integer"> <column name="ID" /> <generator class="native" /> </id> <!-- --> <version column="version" name="version" type="java.lang.Integer"/> <property name="purchaseDate" type="java.util.Date"> <column name="PurchaseDate" length="10"> <comment>进货日期</comment> </column> </property> ... </class> </hibernate-mapping>
4、Hibernate.cfg.xml
<?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <property name="hibernate.connection.driver_class"> com.mysql.jdbc.Driver </property> <property name="hibernate.connection.url"> jdbc:mysql://192.168.1.103:3306/hy_management </property> <property name="hibernate.connection.username">root</property> <property name="hibernate.connection.password">root</property> <property name="hibernate.dialect"> org.hibernate.dialect.MySQLDialect </property> <property name="hibernate.show_sql">true</property> <property name="hibernate.hbm2ddl.auto">create</property> <property name="current_session_context_class">thread</property> <mapping resource="com/hy/model/PBagReceive.hbm.xml"/> </session-factory> </hibernate-configuration> <!-- <property name="hibernate.hbm2ddl.auto">create</property> 每次启动都会删除SQL表的所有数据 -->
5、测试代码:
package com.hy.test; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.Transaction; import org.hibernate.cfg.Configuration; import java.io.File; import java.util.Iterator; import java.util.Set; import com.hy.model.PBagReceive; public class TestLock { private static final Configuration configuration; private static final SessionFactory sessionFactory; static { configuration = new Configuration().configure(); sessionFactory = configuration.buildSessionFactory(); } public static void main(String[] args) { String filePath=System.getProperty("user.dir")+File.separator+"src"+File.separator+"hibernate.cfg.xml"; File file=new File(filePath); System.out.println(filePath); SessionFactory sessionFactory=new Configuration().configure(file).buildSessionFactory(); Session session=sessionFactory.openSession(); Transaction t=session.beginTransaction(); /* * 插入一条记录 */ PBagReceive pb = new PBagReceive(); pb.setManufacturer("sfsfsfsf"); pb.setPurchaseNumber(123123123); session.clear(); session.save(pb); t.commit(); /* * 启动两个线程,去更新同一条数据 */ new TestLock().new Lock("ThreadOne", pb.getId()); new TestLock().new Lock("ThreadTwo", pb.getId()); } class Lock extends Thread { private Integer id; Lock(String name, Integer id) { super(name); this.id = id; start(); } @Override public void run() { Session session = sessionFactory.getCurrentSession(); Transaction transaction = session.beginTransaction(); /* * 并没有执行select语句,在第一次调用get或set方法时才执行select语句 */ PBagReceive pb = (PBagReceive) session.load(PBagReceive.class, id); try { /* * 查看是否读到了相同数据 */ System.out.println("**" + pb.getVersion() + "**"); /* * 这句很关键,线程读取之后等待,使得多个线程 * 同时读取相同(版本号)数据,以便测试乐观锁, * 这个时间尽量长一些,以确保能同时读取后再更新。 */ sleep(10 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } /* * 查看是否读到了相同数据(放在这里可能会看不到想要的效果) */ // System.out.println("**" + student.getVersion() + "**"); /* * 对fullname和name加乐观锁,未对password加锁。 * 分别去掉切换下面三行注释,观察效果。 */ try { pb.setManufacturer("new" + pb.getManufacturer()); transaction.commit(); } catch (Exception e) { // TODO: handle exception System.out.println("提交失败!"); } } } }
Hibernate: insert into hy_management.P_BagReceive (version, PurchaseNumber, Manufacturer) values (?, ?, ?)成功以后,在插入时version会自动添加,不用手动加
Hibernate: select pbagreceiv0_.ID as ID0_0_, pbagreceiv0_.version as version0_0_, pbagreceiv0_.PurchaseDate as Purchase3_0_0_, pbagreceiv0_.PurchaseNumber as Purchase4_0_0_, pbagreceiv0_.Standard as Standard0_0_, pbagreceiv0_.TotalPrice as TotalPrice0_0_, pbagreceiv0_.SupplyUnits as SupplyUn7_0_0_, pbagreceiv0_.Manufacturer as Manufact8_0_0_, pbagreceiv0_.Remarks as Remarks0_0_, pbagreceiv0_.Remark1 as Remark10_0_0_, pbagreceiv0_.Remark2 as Remark11_0_0_, pbagreceiv0_.Remark3 as Remark12_0_0_, pbagreceiv0_.Remark4 as Remark13_0_0_ from hy_management.P_BagReceive pbagreceiv0_ where pbagreceiv0_.ID=?
**0**
Hibernate: select pbagreceiv0_.ID as ID0_0_, pbagreceiv0_.version as version0_0_, pbagreceiv0_.PurchaseDate as Purchase3_0_0_, pbagreceiv0_.PurchaseNumber as Purchase4_0_0_, pbagreceiv0_.Standard as Standard0_0_, pbagreceiv0_.TotalPrice as TotalPrice0_0_, pbagreceiv0_.SupplyUnits as SupplyUn7_0_0_, pbagreceiv0_.Manufacturer as Manufact8_0_0_, pbagreceiv0_.Remarks as Remarks0_0_, pbagreceiv0_.Remark1 as Remark10_0_0_, pbagreceiv0_.Remark2 as Remark11_0_0_, pbagreceiv0_.Remark3 as Remark12_0_0_, pbagreceiv0_.Remark4 as Remark13_0_0_ from hy_management.P_BagReceive pbagreceiv0_ where pbagreceiv0_.ID=?
**0**
Hibernate: update hy_management.P_BagReceive set version=?, Manufacturer=? where ID=? and version=?
Hibernate: update hy_management.P_BagReceive set version=?, Manufacturer=? where ID=? and version=?
可以看到,第二个“用户”session2修改数据时候,记录的版本号已经被session1更新过了,所以抛出了红色的异常,我们可以在实际应用中处理这个异常,例如在处理中重新读取数据库中的数据,同时将目前的数据与数据库中的数据展示出来,让使用者有机会比较一下,或者设计程序自动读取新的数据
注意:如果手工设置stu.setVersion()自行更新版本以跳过检查,则这种乐观锁就会失效,应对方法可以将Student.java的setVersion设置成private