Hibernate 乐观锁与悲观锁 -- ISS(Ideas Should Spread)

本文是笔者 Java 学习笔记之一,旨在总结我的学习 Java 过程当中的心得体会,现将该笔记发表,但愿可以帮助在这方面有疑惑的同行解决一点疑惑,个人目的也就达到了。欢迎分享和转载,转载请注明出处,谢谢合做。因为笔者水平有限,文中不免有所错误,但愿读者朋友不吝赐教,发现错漏我也会及时更新修改,欢迎斧正。(可在文末评论区说明或索要联系方式进一步沟通。)java

乐观锁和悲观锁

在 Java 的多线程环境中,若是有多个线程都要对某些资源进行访问和修改,那么为了防止线程不肯定的执行顺序给资源带来不一致的状态,须要对线程进行加锁,也就是说在同一时刻只能有一个线程对资源进行操做,加锁使多个线程对同一个资源的并发操做被串行化。数据库中的数据也是一种资源,而且数据库中的数据对数据一致性也必须获得保障,这能够经过加锁来达到目的,可是串行化访问的方法虽然可以保证资源的安全,可是在并发量很是高的数据库中会致使极高的用户响应时间,对用户来讲是不可接受的。mysql

对数据库的加锁方式能够分为两种,悲观锁(Pessimistic Lock)和乐观锁(Optimistic Lock)。sql

悲观锁

顾名思义,悲观锁对它在访问数据库的时候老是持有一种悲观的想法,认为在它访问或修改数据库的同时老是会有其它程序也会来对数据库进行访问修改。所以为了保证数据的一致性,悲观锁会在它对数据库进行操做的时候一直锁住它要访问的数据库表(或者锁住其要访问的一条记录),此时其它要对数据库一样位置进行操做的程序将会排队等待得到锁,直到前者操做完毕才释放锁。数据库

悲观锁的使用会致使如上所说的串行化访问的问题,即在一个链接拥有对一个表(或记录)的悲观锁时,其它链接都不能够对该表(或记录)进行操做,所以串行化致使的长响应时间对悲观锁来讲一样存在。apache

乐观锁

一样,乐观锁对它在访问数据库的时候老是持有一种乐观的想法,认为在它访问或修改数据库的时候不会有其它链接会对数据库的同一个位置进行修改,所以不会致使数据不一致的问题。所以乐观锁实际上并无对数据进行加锁处理。安全

可是乐观锁也不能盲目乐观,毕竟 “认为在它访问或修改数据库的时候不会有其它链接会对数据库的同一个位置进行修改” 仅仅是一厢情愿的想法,所以乐观锁也必需要对 “在它访问或修改数据库的时候有其它链接会对数据库的同一个位置进行修改” 这一不乐观的的状况作出弥补。一种常见的方法就是使用 版本号(Version) 来进行标记记录。markdown

乐观锁要求数据库在保存记录的时候也要有一个保留该记录的版本的字段,在对记录进行修改的时候,先把数据记录从数据库中读出来,包括版本号(假设此时版本号为 n),而后对数据进行修改后存回数据库前对版本号加 1 (即 n+1),而后再存回数据库,所以整个修改过程可能的 sql 语句以下:session

select * from user where userId=2;  -- 取出要修改的记录(含版本号)
// 程序取出记录中的版本号,假设为 4
update user set username='newUsername', -- ... 还有其它修改的字段
             set version=5              -- 把版本号加 1 (即4+1)
             where userId=2 and 
             version=4;         -- version 必须是 4,也就是说在程序中读
                -- 出该记录时到此时写回去期间没有其它链接对该记录进行修改

会有如下两种状况:多线程

  • 读出记录的时候版本号是 4 ,直到写回去的时候版本号仍然为 4 ,所以能够保证在此期间没有其它链接对该记录进行修改(可能会有读取),即乐观的状态,此时修改为功;
  • 读出记录的时候版本号是 4 ,写回去的时候版本号不为 4 (比 4 大),所以能够判定在此期间有其它链接对该记录进行了修改,修改完它们将该版本号加 1 ,所以咱们的链接因为慢提交,对该记录修改失败,能够选择重试或取消。

Hibernate 中对乐观锁和悲观锁的验证例子

悲观锁的验证

Hibernate 中悲观锁的实现是依靠底层数据库(至少在我测试 MySql 时是这样的)来实现的。为了验证这个想法,咱们使用如下的例子:并发

使用的表

CREATE TABLE `hm_tst_user` (
  `userid` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(128) DEFAULT NULL,
  PRIMARY KEY (`userid`)
);

使用的 JavaBean User.java

public class User implements Serializable {
    private Long    userid;
    private String  username;
    public User (final String username) {
        this.username = username;
    }
    public User () {}
    protected void setUserid (final Long userid) {
        this.userid = userid;
    }
    // ... 其它 getter/setter
}

使用的映射文件 User.hbm.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping auto-import="true">
    <class name="User" table="hm_tst_user">
        <id name="userid">
            <generator class="identity"/>
        </id>
        <property name="username"/>
    </class>
</hibernate-mapping>

使用的测试代码 junit

public class TestVersion extends TestCase {
    private SessionFactory sessionFactory;

    @Override
    protected void setUp () throws Exception {
        final StandardServiceRegistry registry = new StandardServiceRegistryBuilder ()
                .configure ()
                .build ();
        try {
            sessionFactory = new MetadataSources (registry).buildMetadata ().buildSessionFactory ();
        } catch (Exception e) {
            StandardServiceRegistryBuilder.destroy (registry);
        }
    }

    @Override
    protected void tearDown () throws Exception {
        if (sessionFactory != null) {
            sessionFactory.close ();
        }
    }

    @SuppressWarnings ("unchecked")
    public void test () throws InterruptedException {
        Session session = sessionFactory.openSession ();
        session.beginTransaction ();
        // 如下语句使用了了悲观锁 LockMode.PESSIMISTIC_WRITE
        final User user = session.get (User.class, 1L, LockMode.PESSIMISTIC_WRITE);
        // 在读出该记录并对其进行悲观锁锁定后,
        // 咱们跑去 mysql workbench 执行 sql 语句将该记录的 username 修改为其它(如 world)
        System.out.println ("Go to update the version field");
        // 为了有时间在程序间切换和输入 sql 语句,暂停 10 秒钟
        Thread.sleep (10000);
        // 修改 username 为 hello 加当前时间戳
        user.setUsername ("hello"+ System.currentTimeMillis ());
        session.getTransaction ().commit ();
        session.close ();
    }
}

测试程序中,在将记录以悲观锁的模式读出后,暂停了 10 秒钟(模拟这是一个长事务),在此期间咱们从命令行登陆 mysql ,将 userId 为 1 的记录的 username 修改为 world;然而在命令行中输入语句 update hm_tst_user set username='world' where userId=1; 回车后,会发现有很长时间的停顿,当程序和命令行两边都执行完毕后,咱们发现数据库中最终的结果是命令行中的结果 username=world。命令行中长时间的停顿是因为咱们的测试程序以悲观锁的模式读出,而且在休眠的 10 秒内都锁着这条记录,致使咱们从命令行上的语句须要等待测试程序的锁释放后才能进行,也就是说命令行上的语句执行排队在测试程序后面,这能够经过对测试程序中 Thread.sleep() 增长时间相应的命令行等待时间也会增加这一点来判断获得。

乐观锁的验证

Hibernate 中对乐观锁的实现也是基于版本号来实现的,所以咱们相应的在数据库表,JavaBean 和配置文件中增长一个字段便可,以下。

修改的数据库表

CREATE TABLE `hm_tst_user` (
  `userid` bigint(20) NOT NULL AUTO_INCREMENT,
  `version` int(11) DEFAULT NULL,       -- 增长一个版本号
  `username` varchar(128) DEFAULT NULL,
  PRIMARY KEY (`userid`)
);

修改的 JavaBean

public class User implements Serializable {
        private Long    userid;
        private Integer version;
        private String  username;
        // ... 和以上同样
        // ... 其它 getter/setter
    }

须要注意的是配置文件中对 version 的配置不能看成普通的 property 配置,以下

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping auto-import="true">
    <class name="User" table="hm_tst_user">
        <id name="userid">
            <generator class="identity"/>
        </id>
        <version name="version"/>       <!-- 使用 version 标签 -->
        <property name="username"/>
    </class>
</hibernate-mapping>

修改的测试代码

public class TestVersion extends TestCase {
    // 和以上相同的 tearDown 和 setUp 和字段 sessionFactory
    @SuppressWarnings ("unchecked")
    public void test () throws InterruptedException {
        Session session = sessionFactory.openSession ();
        session.beginTransaction ();
        // 如下语句使用了了乐观锁 LockMode.OPTIMISTIC
        final User user = session.get (User.class, 1L, LockMode.OPTIMISTIC);
        // 在读出该记录并对其进行悲观锁锁定后,
        // 咱们跑去 mysql workbench 执行 sql 语句将该记录的 username 修改为其它(如 world)
        // 注意在命令行操做要同时把版本号加1
        System.out.println ("Go to update the version field");
        // 为了有时间在程序间切换和输入 sql 语句,暂停 10 秒钟
        Thread.sleep (10000);
        // 修改 username 为 hello 加当前时间戳
        user.setUsername ("hello"+ System.currentTimeMillis ());
        session.getTransaction ().commit ();
        session.close ();
    }
}

测试程序中,咱们以乐观锁的方法读出记录,并对记录进行修改,而后写回。当程序执行到 System.out.println ("Go to update the version field"); 时,此时会有如下几种状况:

  • 咱们在命令行下将 userId=1 的记录的 username 修改为 world,而且将版本号 version 加 1,因为是乐观锁,咱们的命令行会立刻执行而且将结果反映到数据库中,当测试程序从休眠中唤醒时,再去执行更新操做,会发现数据库中版本号比本身大,说明被其它链接修改了,测试程序没法修改为功,在 hibernate 中会抛出一个异常 org.hibernate.StaleStateException,仍是很贴切的异常,不新鲜的状态异常
  • 咱们在命令行下将 userId=1 的记录的 username 修改为 world,可是没有将版本号 version 加 ,当测试程序从休眠中被唤醒时,检查到版本号没有变,它会错误地假定在此期间没有其它链接对该记录进行修改,所以对数据库的修改将会进行下去。

乐观锁的局限

从以上第二点能够看出,乐观锁的实现是局限于应用程序内的,也就是说若是其它应用程序不遵循 版本号加 1 的约定,那么乐观锁就可能失效。

其它,附上 Hibernate 的配置文件 hibernate.cfg.xml 和 maven 的 pom.xml 文件,整个测试的代码都全了

hibernate.cfg.xml

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
    <session-factory>
        <property name="connection.driver_class">com.mysql.jdbc.Driver</property>
        <property name="connection.url">jdbc:mysql://localhost:3306/test?useSSL=false</property>
        <property name="connection.username">k</property>
        <property name="connection.password">k</property>

        <property name="connection.pool_size">1</property>
        <property name="dialect">org.hibernate.dialect.MySQLDialect</property>
        <property name="cache.provider_class">org.hibernate.cache.internal.NoCacheProvider</property>
        <property name="show_sql">true</property>

        <mapping resource="User.hbm.xml"/>
    </session-factory>
</hibernate-configuration>

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>ml.kezhenxu.train</groupId>
    <artifactId>hibernate</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>

    <dependencies>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>5.1.0.Final</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.5</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.38</version>
        </dependency>
    </dependencies>

    <build>
        <testResources>
            <testResource>
                <filtering>false</filtering>
                <directory>src/test/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </testResource>
            <testResource>
                <directory>src/test/resources</directory>
            </testResource>
        </testResources>
    </build>
</project>