1. 程式人生 > >MyBatis中呼叫SqlSession.commit()和SqlSession.close()對二級快取的影響

MyBatis中呼叫SqlSession.commit()和SqlSession.close()對二級快取的影響

         在學習MyBatis時,我一直對進行什麼操作會影響資料放進二級快取的情況感到非常疑惑。由此,我特地對各個情況進行測試分析。特別是在分析SqlSession的commit()和close()方法對二級快取的影響時,花了我好多的時間。只追求最終結果的朋友,可以直接拉到最後看我的總結。 
Mapper:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ths.demo4.mapper.UserMapper">
    <cache></cache><!-- 開啟這個個Mapper的二級快取 -->
    <sql id="userCols">
        ${table}.username, ${table}.password, ${table}.sex
    </sql>
    <select id="getUser" resultType="com.ths.demo4.pojo.User" useCache="true">
        select id,
          <include refid="userCols">
              <property name="table" value="user"></property>
          </include>
        from jinbaizhe_user as user where user.id = #{id}
    </select>
    <insert id="insertUser" useGeneratedKeys="true" keyProperty="user.id">
        insert into jinbaizhe_user(username, password, sex)  values (#{user.username}, #{user.password}, #{user.sex})
    </insert>
    <update id="updateUser" parameterType="com.ths.demo4.pojo.User">
        update jinbaizhe_user set username=#{username}, password=#{password}, sex=#{sex} where id=#{id}
    </update>
    <delete id="deleteUser" parameterType="com.ths.demo4.pojo.User">
        delete from jinbaizhe_user where id=#{id}
    </delete>
    <select id="getUserByUsername" resultType="com.ths.demo4.pojo.User" useCache="true">
        select id, username, password, sex from jinbaizhe_user where username=#{username}
    </select>
    <select id="getAllUsers" resultType="com.ths.demo4.pojo.User" useCache="true">
        select * from jinbaizhe_user
    </select>
</mapper>

測試二級快取的作用範圍:

    @Test
    @Transactional
    @Rollback
    public void testCacheLevel2_1(){
        //測試二級快取的作用範圍
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
        System.out.println("測試二級快取的作用範圍-------begin");
        User user1 = userMapper1.getUser(1);//從資料庫中獲取,放進sqlSession1的一級快取中
        User user2 = userMapper2.getUser(1);//從資料庫中獲取,放進sqlSession2的一級快取中
        System.out.println("測試二級快取的作用範圍-------end");
    }

控制檯輸出:

    測試二級快取的作用範圍-------begin
    MyBatis:Cache Hit Ratio [com.ths.demo4.mapper.UserMapper]: 0.0
    MyBatis:==>  Preparing: select id, user.username, user.password, user.sex from jinbaizhe_user as user where user.id = ? 
    MyBatis:==> Parameters: 1(Integer)
    MyBatis:<==      Total: 1
    MyBatis:Cache Hit Ratio [com.ths.demo4.mapper.UserMapper]: 0.0
    MyBatis:==>  Preparing: select id, user.username, user.password, user.sex from jinbaizhe_user as user where user.id = ? 
    MyBatis:==> Parameters: 1(Integer)
    MyBatis:<==      Total: 1
    測試二級快取的作用範圍-------end

結論:。二級快取的作用範圍不是SqlSession(已驗證),而應該是Mapper(對映器)(這點未驗證)。

測試呼叫SqlSession.close()會將其一級快取的資料放到二級快取中:

    @Test
    @Transactional
    @Rollback
    public void testCacheLevel2_2(){
        //測試SqlSession.close()會將其一級快取的資料放到二級快取中
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
        System.out.println("測試SqlSession.close()會將其一級快取的資料放到二級快取中-------begin");
        User user1 = userMapper1.getUser(1);//查詢後放進sqlSession1的一級快取中
        sqlSession1.close();//關閉sqlSession1,會將其中的一級快取的資料放進二級快取中。
        User user2 = userMapper2.getUser(1);//從二級快取中獲取
        System.out.println("測試SqlSession.close()會將其一級快取的資料放到二級快取中-------end");
    }

控制檯輸出:

    測試SqlSession.close()會將其一級快取的資料放到二級快取中-------begin
    MyBatis:Cache Hit Ratio [com.ths.demo4.mapper.UserMapper]: 0.0
    MyBatis:==>  Preparing: select id, user.username, user.password, user.sex from jinbaizhe_user as user where user.id = ? 
    MyBatis:==> Parameters: 1(Integer)
    MyBatis:<==      Total: 1   
    MyBatis:Cache Hit Ratio [com.ths.demo4.mapper.UserMapper]: 0.5
    測試SqlSession.close()會將其一級快取的資料放到二級快取中-------end

結論:呼叫SqlSession.close()方法後,會將其一級快取的資料放進二級快取中。

測試呼叫SqlSession.commit()會將其一級快取的資料放到二級快取中:

    @Test
    @Transactional
    @Rollback
    public void testCacheLevel2_3(){
        //測試SqlSession.commit()會將其一級快取的資料放到二級快取中
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
        System.out.println("測試SqlSession.commit()會將其一級快取的資料放到二級快取中-------begin");
        User user1 = userMapper1.getUser(1);//查詢後放進sqlSession1的一級快取中
        sqlSession1.commit();//進行commit,會將其中的一級快取的資料放進二級快取中,並清空一級快取。
        User user2 = userMapper2.getUser(1);//從二級快取中獲取
        System.out.println("測試SqlSession.commit()會將其一級快取的資料放到二級快取中-------end");
    }

控制檯輸出:

    測試SqlSession.commit()會將其一級快取的資料放到二級快取中-------begin
    MyBatis:Cache Hit Ratio [com.ths.demo4.mapper.UserMapper]: 0.0
    MyBatis:==>  Preparing: select id, user.username, user.password, user.sex from jinbaizhe_user as user where user.id = ? 
    MyBatis:==> Parameters: 1(Integer)
    MyBatis:<==      Total: 1
    MyBatis:Cache Hit Ratio [com.ths.demo4.mapper.UserMapper]: 0.5
    測試SqlSession.commit()會將其一級快取的資料放到二級快取中-------end

結論:呼叫SqlSession.commit()方法後,會將其一級快取的資料放進二級快取中,並清空一級快取(這點在一級快取的文章中已證明)。

測試執行更新操作對二級快取的影響:

    @Test
    @Transactional
    @Rollback
    public void testCacheLevel2_4(){
        //測試執行更新操作對二級快取的影響
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        SqlSession sqlSession3 = sqlSessionFactory.openSession();
        UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
        UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);
        System.out.println("測試執行更新操作對二級快取的影響-------begin");
        User user1 = userMapper1.getUser(1);//查詢後放進sqlSession1的一級快取中
        System.out.println("user.sex="+user1.getSex());
        sqlSession1.close();//關閉sqlSession1,將一級快取中的資料放進二級快取中(使用sqlSession.commit()也能達到同樣的效果)
        //修改user1
        user1.setSex("test");
        userMapper2.updateUser(user1);//sqlSession2進行更新操作,會清空自身的一級快取。(這點在一級快取的文章中已證明)
        //沒有執行commit()操作,不會影響二級快取
        User user2 = userMapper3.getUser(1);//還是從二級快取中獲取
        System.out.println("user.sex="+user2.getSex());//輸出應是原來的"male",而不是"test"
        System.out.println("測試執行更新操作對二級快取的影響-------end");
    }

控制檯輸出:

    測試執行更新操作對二級快取的影響-------begin
    MyBatis:Cache Hit Ratio [com.ths.demo4.mapper.UserMapper]: 0.0
    MyBatis:==>  Preparing: select id, user.username, user.password, user.sex from jinbaizhe_user as user where user.id = ? 
    MyBatis:==> Parameters: 1(Integer)
    MyBatis:<==      Total: 1
    user.sex=male
    MyBatis:==>  Preparing: update jinbaizhe_user set username=?, password=?, sex=? where id=? 
    MyBatis:==> Parameters: parker(String), 1(String), test(String), 1(Integer)
    MyBatis:<==    Updates: 1
    MyBatis:Cache Hit Ratio [com.ths.demo4.mapper.UserMapper]: 0.5
    user.sex=male
    測試執行更新操作對二級快取的影響-------end

結論:當對SqlSession執行更新操作(update、delete、insert)時,只會清空其自身的一級快取,不影響二級快取。

測試執行更新操作並呼叫commit()對二級快取的影響:

    @Test
    @Transactional
    @Rollback
    public void testCacheLevel2_5(){
        //測試執行更新操作並commit()對二級快取的影響
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        SqlSession sqlSession3 = sqlSessionFactory.openSession();
        UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
        UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);
        System.out.println("測試執行更新操作並commit()對二級快取的影響-------begin");
        User user1 = userMapper1.getUser(1);//查詢後放進sqlSession1的一級快取中
        System.out.println("user.sex="+user1.getSex());
        sqlSession1.close();//關閉sqlSession1,將一級快取中的資料放進二級快取中(使用sqlSession.commit()也能達到同樣的效果)
        //修改user1
        user1.setSex("test");
        userMapper2.updateUser(user1);//sqlSession2進行更新操作,會清空自身的一級快取。
        //執行commit()操作,清空二級快取
        sqlSession2.commit();
        User user2 = userMapper3.getUser(1);//從資料庫中獲取
        System.out.println("user.sex="+user2.getSex());//輸出應是"test"
        System.out.println("測試執行更新操作並commit()對二級快取的影響-------end");
    }

控制檯輸出:

    測試執行更新操作並commit()對二級快取的影響-------begin
    MyBatis:Cache Hit Ratio [com.ths.demo4.mapper.UserMapper]: 0.0
    MyBatis:==>  Preparing: select id, user.username, user.password, user.sex from jinbaizhe_user as user where user.id = ? 
    MyBatis:==> Parameters: 1(Integer)
    MyBatis:<==      Total: 1
    user.sex=male
    MyBatis:==>  Preparing: update jinbaizhe_user set username=?, password=?, sex=? where id=? 
    MyBatis:==> Parameters: parker(String), 1(String), test(String), 1(Integer)
    MyBatis:<==    Updates: 1
    MyBatis:Cache Hit Ratio [com.ths.demo4.mapper.UserMapper]: 0.0
    MyBatis:==>  Preparing: select id, user.username, user.password, user.sex from jinbaizhe_user as user where user.id = ? 
    MyBatis:==> Parameters: 1(Integer)
    MyBatis:<==      Total: 1
    user.sex=test
    測試執行更新操作並commit()對二級快取的影響-------end

結論:當對SqlSession執行更新操作(update、delete、insert)後並執行SqlSession.commit()時,不僅清空其自身的一級快取(執行更新操作的效果),也清空二級快取(執行commit()的效果)。

測試執行更新操作並呼叫close()對二級快取的影響:

    @Test
    @Transactional
    @Rollback
    public void testCacheLevel2_6(){
        //測試執行更新操作並close()對二級快取的影響
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        SqlSession sqlSession3 = sqlSessionFactory.openSession();
        UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
        UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);
        System.out.println("測試執行更新操作並close()對二級快取的影響-------begin");
        User user1 = userMapper1.getUser(1);//查詢後放進sqlSession1的一級快取中
        System.out.println("user.sex="+user1.getSex());
        sqlSession1.close();//關閉sqlSession1,將一級快取中的資料放進二級快取中(在這裡換成SqlSession.commit()也能達到同樣的效果)
        user1.setSex("test");//修改user1
        userMapper2.updateUser(user1);//sqlSession2進行更新操作,會清空自身的一級快取。
        sqlSession2.close();//執行close()操作,此時並沒有清空二級快取。
        User user2 = userMapper3.getUser(1);//從二級快取中獲取
        System.out.println("user.sex="+user2.getSex());//輸出應是"male"
        System.out.println("測試執行更新操作並close()對二級快取的影響-------end");
    }

控制檯輸出:

    測試執行更新操作並close()對二級快取的影響-------begin
    MyBatis:Cache Hit Ratio [com.ths.demo4.mapper.UserMapper]: 0.0
    MyBatis:==>  Preparing: select id, user.username, user.password, user.sex from jinbaizhe_user as user where user.id = ? 
    MyBatis:==> Parameters: 1(Integer)
    MyBatis:<==      Total: 1
    user.sex=male
    MyBatis:==>  Preparing: update jinbaizhe_user set username=?, password=?, sex=? where id=? 
    MyBatis:==> Parameters: parker(String), 1(String), test(String), 1(Integer)
    MyBatis:<==    Updates: 1
    MyBatis:Cache Hit Ratio [com.ths.demo4.mapper.UserMapper]: 0.5
    user.sex=male
    測試執行更新操作並close()對二級快取的影響-------end

結論:當對SqlSession執行更新操作(update、delete、insert)後並執行SqlSession.close()時,只會清空其自身的一級快取(執行更新操作的效果),對二級快取沒影響了。

那麼問題來了,在執行select操作後,無論是呼叫SqlSession.commit()還是SqlSession.close(),都能將一級快取中的資料放到二級快取中;而在執行更新操作(update、delete、insert)後,呼叫SqlSession.commit()SqlSession.close()卻會有不同的效果,這是為什麼呢?

下面先從SqlSession的原始碼開始分析。

DefaultSqlSession的部分函式的原始碼

    private final Executor executor;

    //DefaultSqlSession的close()
    public void close() {
        try {
            this.executor.close(this.isCommitOrRollbackRequired(false));
            this.closeCursors();
            this.dirty = false;
        } finally {
            ErrorContext.instance().reset();
        }

    }

    //DefaultSqlSession的isCommitOrRollbackRequired()
    private boolean isCommitOrRollbackRequired(boolean force) {
        //由於dirty=true,autoCommit為false,導致函式返回true
        return !this.autoCommit && this.dirty || force;//這裡很重要,請關注!
    }

    //DefaultSqlSession的commit()
    public void commit() {
        this.commit(false);
    }

    //DefaultSqlSession的commit()
    public void commit(boolean force) {
        //在我們這個例子中可以將形參force的值當作false,因為實際呼叫的是上面的commit()函式。
        try {
            this.executor.commit(this.isCommitOrRollbackRequired(force));
            this.dirty = false;
        } catch (Exception var6) {
            throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + var6, var6);
        } finally {
            ErrorContext.instance().reset();
        }

    }

看完DefaultSqlSession的部分原始碼,比較一下close()函式和commit()的區別,可以發現最主要的區別還是close()呼叫了this.executor.close(this.isCommitOrRollbackRequired(false));,而commit()呼叫了this.executor.commit(this.isCommitOrRollbackRequired(force));,所以我們還得繼續看executor(CachingExecutor)下的commit()和close()的區別。

CachingExecutor的部分函式的原始碼

    /*
    CacheExecutor有一個重要的屬性delegate,它儲存的是某類普通的Executor,值在構造時傳入。執行資料庫update操作時,它直接呼叫delegate的update方法,執行query方法時先嚐試從cache中取值,取不到再呼叫delegate的查詢方法,並將查詢結果存入cache中。
    每一個SqlSession中都有一個屬於自己的Executor,當開啟二級快取後,會使用CachingExecutor來裝飾Executor。而裝飾者的功能不就是增加功能嗎?所以在CachingExecutor類中,與二級快取有關的操作是不會在它的delegate屬性下操作的,而是在它自己的方法裡操作,這點對接下來的分析很重要。
    */
    private final Executor delegate;
    private final TransactionalCacheManager tcm = new TransactionalCacheManager();

    //CachingExecutor的close()
    public void close(boolean forceRollback) {
        //由於DefaultSqlSession的isCommitOrRollbackRequired()返回的是true,forceRollback=true。
        try {
            if (forceRollback) {//條件為true
                this.tcm.rollback();//執行了這行程式碼
            } else {
                this.tcm.commit();//tcm是TransactionalCacheManager的例項物件,與將一級快取中的資料新增到二級快取有關
            }
        } finally {
            this.delegate.close(forceRollback);//由於對delegate的操作與二級快取並無太大關係,這裡就不再分析了。
        }
    }

    //CachingExecutor的commit()
    public void commit(boolean required) throws SQLException {
        this.delegate.commit(required);//由於delegate的操作與二級快取並無太大關係,這裡就不再分析了。
        this.tcm.commit();//tcm是TransactionalCacheManager的例項物件,與將一級快取中的資料新增到二級快取有關
    }

看到這裡,其實答案已經快出來了。只要變數forceRollbackfalseforceRollback=!this.autoCommit && this.dirty || force),那麼close()和commit()函式都會去呼叫this.tcm.commit(),那麼對於二級快取來說,兩者就會有相同的作用效果。想想之前的一個例子:使用mappper查詢後,不管是呼叫commit()還是close(),都會將一級快取裡的資料放進二級快取中。分析到這裡情況應該很清楚了,問題就出在變數forceRollback的身上。如果對此還有疑問,我們可以繼續往下面看對CachingExecutor的屬性tcm(TransactionalCacheManager的例項物件)的分析。

TransactionalCacheManager的部分函式的原始碼

    public void commit() {
        Iterator var1 = this.transactionalCaches.values().iterator();

        while(var1.hasNext()) {
            TransactionalCache txCache = (TransactionalCache)var1.next();
            txCache.commit();//commit()和rollback()僅有的區別
        }

    }

    public void rollback() {
        Iterator var1 = this.transactionalCaches.values().iterator();

        while(var1.hasNext()) {
            TransactionalCache txCache = (TransactionalCache)var1.next();
            txCache.rollback();//commit()和rollback()僅有的區別
        }

    }

問題又變成了關注TransactionalCache下的commit()和rollback()的區別

TransactionalCache的部分屬性和函式的原始碼

    private final Cache delegate;//存放二級快取資料的地方。它裡面也有一個屬性delegate,經過了好幾次的裝飾,最裡面一層有的一個屬性名為cache(HashMap型別),是真正存放存放二級快取資料的地方。
    private boolean clearOnCommit;
    private final Map<Object, Object> entriesToAddOnCommit;
    private final Set<Object> entriesMissedInCache;

    public void commit() {
        if (this.clearOnCommit) {
            this.delegate.clear();
        }

        this.flushPendingEntries();//將entriesToAddOnCommit(Map)裡的資料放進delegate(Cache)中。
        this.reset();
    }

    public void rollback() {
        this.unlockMissedEntries();//在delegate(Cache)中移除含有entriesMissedInCache中的資料。
        this.reset();
    }


    private void flushPendingEntries() {
        將entriesToAddOnCommit(Map)裡的資料放進delegate(Cache)中。
        Iterator var1 = this.entriesToAddOnCommit.entrySet().iterator();

        while(var1.hasNext()) {
            Entry<Object, Object> entry = (Entry)var1.next();
            this.delegate.putObject(entry.getKey(), entry.getValue());
        }

        var1 = this.entriesMissedInCache.iterator();

        while(var1.hasNext()) {
            Object entry = var1.next();
            if (!this.entriesToAddOnCommit.containsKey(entry)) {
                this.delegate.putObject(entry, (Object)null);//對應的value設為空值
            }
        }

    }

    private void unlockMissedEntries() {
        //在delegate(Cache)中移除含有entriesMissedInCache中的資料。
        Iterator var1 = this.entriesMissedInCache.iterator();

        while(var1.hasNext()) {
            Object entry = var1.next();

            try {
                this.delegate.removeObject(entry);
            } catch (Exception var4) {
                log.warn("Unexpected exception while notifiying a rollback to the cache adapter.Consider upgrading your cache adapter to the latest version.  Cause: " + var4);
            }
        }

    }

現在再回去重新看CachingExecutor的分析,是不是更加清楚了呢。 
回到前面的問題,SqlSession裡的屬性dirty為什麼變成了true呢?因為沒有執行SqlSession.commit(),又由於是更新操作(update、delete、insert),且autoCommit為false(沒有開啟自動提交),導致修改後的資料沒提交,又與資料庫裡的資料不一致,那它不就是髒資料(dirty)麼?那麼從邏輯上分析,既然是髒資料,那就完全沒有將髒資料放到二級快取裡的道理。由此看來,呼叫Sqlsession.close()並不一定會產生與呼叫Sqlsession.commit()一樣的效果。

總結: 
1. 進行select操作後,呼叫SqlSession.close()方法,會將其一級快取的資料放進二級快取中,此時一級快取隨著SqlSession的關閉也就不存在了。 
2. 進行select操作後,呼叫SqlSession.commit()方法,會將其一級快取的資料放進二級快取中,並清空一級快取(清空一級快取這點在一級快取的文章中已說明)。 
3. 對SqlSession執行更新操作(update、delete、insert)時,同時不呼叫SqlSession.commitSqlSession.close(),這時只會清空其自身的一級快取,對二級快取沒有影響(清空一級快取這點在一級快取的文章中已說明)。 
4. 對SqlSession執行更新操作(update、delete、insert)後並執行SqlSession.commit()時,不僅清空其自身的一級快取(執行更新操作的結果),也清空二級快取(執行commit()的效果)。 
5. 對SqlSession執行更新操作(update、delete、insert)後並執行SqlSession.close()時(沒有執行SqlSession.commit()),需分兩類情況。當autoCommit為false時,只會清空其自身的一級快取(執行更新操作的效果),對二級快取沒有影響。當autoCommit為true時,會清空二級快取。 
6. 在我們的這幾個例子中,close()會不會產生和commit()同樣的效果(將資料放進二級快取中或清空二級快取),要看SqlSession裡的dirty屬性,值為flase(即沒進行過更新操作),則有同樣的效果。若值為true還要看autoCommit的值。換言之,當SqlSession只執行了select操作時,即沒有進行過更新操作(update、delete、insert)時,不管是呼叫SqlSession.commit()還是SqlSession.close(),不管autoCommit是true還是false,都能使一級快取中的資料放進二級快取中。當SqlSession執行了update操作後,dirty的值變為true,此時還要看autoCommit的值來決定。更籠統的來說,當forceRollback=!this.autoCommit && this.dirty || force的值為false,close()會產生和commit()同樣的效果;當其值為false時,兩者會有不同的效果。