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>
相關文章
相關標籤/搜索