1. 程式人生 > >Mybatis學習系列(七)緩存機制

Mybatis學習系列(七)緩存機制

emca value 不存在 memcach except input jedis 寫入 on()

Mybatis緩存介紹

MyBatis提供一級緩存和二級緩存機制。

一級緩存是Sqlsession級別的緩存,Sqlsession類的實例對象中有一個hashmap用於緩存數據。不同的Sqlsession實例緩存的hashmap數據區域互不影響。Mybatis默認啟用一級緩存,在同一個sqlsession中多次執行相同的sql語句,第一次執行後會將數據緩存起來,後面的查詢將會從緩存中讀取。當一個sqlsession結束後(close),該sqlsession中緩存的數據也將不存在。

二級緩存是Mapper級別的緩存,多個sqlsession實例操作同一個Mapper配置可共享二級緩存。Mybatis默認沒有啟用二級緩存,需要手動配置開啟二級緩存。

一張圖看看一集緩存和二級緩存的區別:

技術分享圖片

一級緩存

一級緩存區域按sqlsession劃分,當執行查詢時會先從緩存區域查找,如果存在則直接返回數據,否則從數據庫查詢,並將結果集寫入緩存區。 Mybatis一級緩存是在sqlsession內部維護一個hashmap用於存儲,緩存key為hashcode+sqlid+sql,value則為查詢的結果集。一級緩存在執行sqlsession.commit()後將會被清空。

一級緩存示例:

編寫cacheMapper.xml配置文件

<mapper namespace="com.sl.mapper.CacheMapper">
<cache/> <select id="selectProductById" parameterType="int" resultType="com.sl.po.Product" > select * from products where id = #{id} </select> </mapper>

Mapper接口:

技術分享圖片
public interface CacheMapper {
    
    Product selectProductById(int id);
    
    @Options(flushCache
=FlushCachePolicy.TRUE) int updateProductById(Product product); }
View Code

測試方法:

public class TestCacheMapperClient {

SqlSessionFactory factory
= null; @Before public void init() throws IOException { String resource = "SqlMapConfig.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); factory = builder.build(inputStream); } // 一級緩存 @Test public void testSelectProductById() { SqlSession session = factory.openSession(); CacheMapper mapper = session.getMapper(CacheMapper.class); Product product = mapper.selectProductById(1); System.out.println(product.getName()); //執行commit 將清空一級緩存 //session.commit(); //再次執行查詢 從一級緩存讀取 Product product2 = mapper.selectProductById(1); System.out.println(product.getName()); // 關閉會話 session.close(); } }

執行第一次執行selectProductById,查詢數據庫,第二次執行,從緩存中讀取

技術分享圖片

如果在兩次查詢中間執行commit,即上面的註釋掉的session.commit(),則運行結果如下,顯然清空了一級緩存,再次執行數據庫查詢

技術分享圖片

二級緩存

二級緩存按照mapper劃分,一個mapper有一個自己的二級緩存(按照namespace區分不同緩存區域,如果多個mapper的namespace相同,則公用一個緩存區域),當多個sqlsession類實例加載相同的Mapper文件,執行mapper配置文件中的sql查詢時,這些sqlsession可共享一個二級緩存。Mybatis默認沒有啟用二級緩存,需要自行配置。

二級緩存示例:

1. 啟用二級緩存:

在mybatis置文件SqlMapConfig.xml中加入一下配置

<setting name="cacheEnabled" value="true"/>

在Mapper.xml配置文件中添加cache標簽

<cache />  <!-- 表示此mapper開啟二級緩存。-->

還可以配置其他參數,如:

<cache  flushInterval="60000"  size="512"  readOnly="true" eviction="FIFO"  type=”xxxxx” />

flushInterval:刷新時間間隔,單位毫秒,不設置則沒有刷新時間間隔,在執行配置了flushCache標簽的sql時刷新(清空)

size:緩存原數個數,默認1024

readOnly:是否只讀,默認false:mybatis將克隆一份數據返回,true:直接返回緩存數據的引用(不安全,程序如果修改,直接改了緩存項)

eviction:緩存的回收策略(LRU 、FIFO、 SOFT 、WEAK ),默認LRU

type:指定自定義緩存的全類名(實現Cache接口即可)

2. 結果集映射對象實現序列化接口

使用Mybatis二級緩存需要將sql結果集映射的pojo對象實現java.io.Serializable接口,否則將出現序列化錯誤。

public class Product implements Serializable{ …}

3.編寫cacheMapper.xml配置文件

<mapper namespace="com.sl.mapper.CacheMapper">
    <cache/>
   
<select id="selectProductById" parameterType="int" resultType="com.sl.po.Product" ><!-- useCache="false" 禁用二級緩存或者在Mapper接口上通過註解禁用--> select * from products where id = #{id} </select> <!-- update – 映射更新語句 --> <update id="updateProductById" parameterType="com.sl.po.Product" flushCache="true"> <!-- flushCache="true" 禁用二級緩存或者在Mapper接口上通過註解禁用--> update products set Name = #{Name},IsNew=#{IsNew} where id=#{id} </update> </mapper>

4.Mapper.java接口

public interface CacheMapper {
    Product selectProductById(int id);
    
    //@Options(flushCache=FlushCachePolicy.TRUE)  //清空 二級緩存
    int updateProductById(Product product);}

5.測試方法:

public class TestCacheMapperClient {
SqlSessionFactory factory
= null; @Before public void init() throws IOException { String resource = "SqlMapConfig.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); factory = builder.build(inputStream); }
//二級緩存 @Test public void testSelectProductById2() throws IOException { SqlSession session1 = factory.openSession(); CacheMapper mapper1 = session1.getMapper(CacheMapper.class); Product product = mapper1.selectProductById(1); System.out.println(product.getName()); /************同一session 共享一級緩存***************/ //CacheMapper mapper2 = session1.getMapper(CacheMapper.class); //Product product2 = mapper2.selectProductById(1); //System.out.println(product2.getName()); //執行commit 將清空一級緩存,無法情況二級緩存 session1.commit(); session1.close(); //清空二級緩存 //Mapper接口註解@Options(flushCache=FlushCachePolicy.TRUE) 或者Mapper.xml配置屬性 flushCache="true" SqlSession session4 = factory.openSession(); CacheMapper mapper4 = session4.getMapper(CacheMapper.class); Product up = new Product(); up.setId(1); up.setIsNew(true); up.setName("緩存測試2"); int count = mapper4.updateProductById(up); session4.commit(); session4.close(); /**********不同session實例 共享二級緩存************/ SqlSession session3 = factory.openSession(); CacheMapper mapper3 = session3.getMapper(CacheMapper.class); Product product3 = mapper3.selectProductById(1); System.out.println(product3.getName()); // 關閉會話 session3.close(); } }

測試結果,上面updateProductById方法在配置sql中清空了二級緩存,所以後面mapper3.selectProductById(1)仍然執行數據庫查詢。

技術分享圖片

6. 禁用二級緩存

Mybatis還提供屬性用於對指定的查詢禁用二級緩存,在Mapper.xml配置文件中可是使用useCache=false禁止當前select使用二級緩存,即:

 <select id="selectProductById" parameterType="int" resultType="com.sl.po.Product" useCache="false" ><!-- useCache="false" 禁用二級緩存或者在Mapper接口上通過註解禁用-->
            select * from products where id = #{id}
 </select>

在Mapper.Java接口中可是通過註解來禁用二級緩存,即:

    @Options(useCache=false)
    Product selectProductById(int id);

7.緩存刷新

當mybatis執行數據更新sql語句後,DB數據與緩存數據可能已經不一致,如果不執行刷新緩存則可能出現臟讀的情況,Mybatis同樣提供xml配置和註解兩種方式來實現緩存刷新

Xml配置形式:

<update id="updateProductById" parameterType="com.sl.po.Product" flushCache="true"> <!-- flushCache="true" 禁用二級緩存或者在Mapper接口上通過註解禁用-->
            update products set
            Name = #{Name},IsNew=#{IsNew} 
            where id=#{id}
</update>

註解形式:

@Options(flushCache=FlushCachePolicy.TRUE)  //清空 二級緩存
int updateProductById(Product product);

使用Redis做Mybatis二級緩存

Mybatis默認啟用二級緩存是服務器本地緩存,在程序部署到多臺服務器時可能出現數據不一致的情況,這種情況下最好能有個集中式緩存來解決此問題。MyBatis的二級緩存允許自定義實現,Mybatis提供二級緩存接口,我們可以通過實現org.apache.ibatis.cache.Cache接口來整合第三方緩存,比如redis、memcache等。

Demo實現步驟:

1. 添加jar包依賴

        <!-- redis client -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>${jedis.version}</version>
        </dependency>

2. 實現 Mybatis二級緩存org.apache.ibatis.cache.Cache接口

技術分享圖片
package com.sl.redis;

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.ibatis.cache.Cache;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class RedisCache implements Cache
{
    
    private Jedis redisClient = createClient();
    
    
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
 
    private String id;
 
    public RedisCache(final String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        }
        
        this.id = id;
    }
 
    public String getId() {
        return this.id;
    }
 
    public int getSize() {
        return Integer.valueOf(redisClient.dbSize().toString());
    }
 
    public void putObject(Object key, Object value) {
        
        redisClient.set(SerializeHelper.serialize(key.toString()), SerializeHelper.serialize(value));
    }
 
    public Object getObject(Object key) {
        Object value = SerializeHelper.unserialize(redisClient.get(SerializeHelper.serialize(key.toString())));
        return value;
    }
 
    public Object removeObject(Object key) {
        return redisClient.expire(SerializeHelper.serialize(key.toString()), 0);
    }
 
    public void clear() {
        
        redisClient.flushDB();
    }
 
    public ReadWriteLock getReadWriteLock() {
        return readWriteLock;
    }
 
    protected static Jedis createClient() {
        try {
            JedisPool pool = new JedisPool(new JedisPoolConfig(),"localhost");
            return pool.getResource();
        } catch (Exception e) {
            e.printStackTrace();
        }
        throw new RuntimeException("初始化連接池錯誤");
    }
    

}

package com.sl.redis;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SerializeHelper {
    public static byte[] serialize(Object object) {
        ObjectOutputStream oos = null;
        ByteArrayOutputStream baos = null;
        try {
            // 序列化
            baos = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(baos);
            oos.writeObject(object);
            byte[] bytes = baos.toByteArray();
            return bytes;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
 
    public static Object unserialize(byte[] bytes) {
        if (bytes == null)
            return null;
        ByteArrayInputStream bais = null;
        try {
            // 反序列化
            bais = new ByteArrayInputStream(bytes);
            ObjectInputStream ois = new ObjectInputStream(bais);
            return ois.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}
View Code

3. 修改Mapper.xml配置文件,通過type屬型指定自定義二級緩存實現 type="com.sl.redis.RedisCache"

<mapper namespace="com.sl.mapper.CacheMapper">
    
        <cache type="com.sl.redis.RedisCache"/>
        
        <select id="selectProductById" parameterType="int" resultType="com.sl.po.Product" ><!-- useCache="false" 禁用二級緩存或者在Mapper接口上通過註解禁用-->
            select * from products where id = #{id}
        </select>
        
        <!-- update – 映射更新語句 -->
        <update id="updateProductById" parameterType="com.sl.po.Product" flushCache="true"> <!-- flushCache="true" 禁用二級緩存或者在Mapper接口上通過註解禁用-->
            update products set
            Name = #{Name},IsNew=#{IsNew} 
            where id=#{id}
        </update>
    </mapper>

測試方法同上。

以上通過重寫Mybatis二級緩存接口Cache類中的方法,將mybatis中默認的二級緩存空間替換成Redis。mybatis的二級緩存默認存儲1024個對象(通過size可配置),緩存容易造成臟讀數據,影響數據的準確性,實際開發中往往放棄直接使用默認二級緩存。

Mybatis學習系列(七)緩存機制