1. 程式人生 > >整合 Spring Redis 快取

整合 Spring Redis 快取

這裡的快取主要是用於 Service 層的,所以下面的配置,都是針對 service 模組的。

本文來自內部分享,對特殊資訊進行了簡單處理。


本文都是在以快取來講 Redis 的使用,實際上 Redis 不僅僅用於快取,本身還是 NoSQL 資料庫,大家可以自己查詢學習 Redis 的常用場景。

一、新增依賴

<!--快取-->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context-support</
artifactId
>
<version>4.3.14.RELEASE</version> </dependency> <!--redis--> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> <version>1.8.10.RELEASE</version> </dependency> <
dependency
>
<groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.4.3</version> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>
2.9.0</version> </dependency>

二、配置

增加 spring-redis.xml 配置檔案,內容如下:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:cache="http://www.springframework.org/schema/cache"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
            http://www.springframework.org/schema/cache
            http://www.springframework.org/schema/cache/spring-cache.xsd
            http://www.springframework.org/schema/util
            http://www.springframework.org/schema/util/spring-util.xsd">
            
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <property name="maxIdle" value="${redis.pool.maxIdle}"/>
        <property name="maxTotal" value="${redis.pool.maxTotal}"/>
        <property name="maxWaitMillis" value="${redis.pool.maxWaitMillis}"/>
        <property name="testOnBorrow" value="${redis.pool.testOnBorrow}"/>
        <property name="testOnReturn" value="${redis.pool.testOnReturn}"/>
    </bean>

    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <property name="hostName" value="${redis.master.ip}"/>
        <property name="port" value="${redis.master.port}"/>
        <property name="poolConfig" ref="jedisPoolConfig"/>
    </bean>

    <bean id="redisKeySerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
    <bean id="redisValueSerializer" class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>


    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory"/>
        <property name="keySerializer" ref="redisKeySerializer"/>
        <property name="hashKeySerializer" ref="redisKeySerializer"/>
        <property name="valueSerializer" ref="redisValueSerializer"/>
        <property name="hashValueSerializer" ref="redisValueSerializer"/>
    </bean>

    <!--在 redis.properties 配置快取詳細資訊-->
    <util:properties id="redisExpires" location="classpath*:META-INF/spring/redis.properties"/>

    <bean id="cacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
        <constructor-arg index="0" ref="redisTemplate"/>
        <!--預設快取 10 分鐘-->
        <property name="defaultExpiration" value="600"/>
        <property name="usePrefix" value="true"/>
        <property name="expires" ref="redisExpires"/>
    </bean>

    <!--啟用 cache 註解-->
    <cache:annotation-driven cache-manager="cacheManager" proxy-target-class="true"/>

</beans>

src/main/resources/ 下面如果沒有 META-INF/spring/ 目錄就建立一個,然後增加 redis.properties 配置,示例如下:

# 快取名=有效時間
halfHour=1800
oneHour=3600
oneDay=86400
webSession=1800
user=1800

除了上面配置外,在系統的 application.properties 中還需要提供下面幾個配置:

# redis 連線配置
redis.master.ip=10.10.10.100
redis.master.port=6379

# redis 連線池配置
redis.pool.maxIdle=200
redis.pool.maxTotal=1024
redis.pool.maxWaitMillis=1000
redis.pool.testOnBorrow=true
redis.pool.testOnReturn=true

三、通過註解方式使用快取

示例中,redis.propreties 配置如下:

# 資料庫定義,快取 30 天
databaseDef=2592000
# 資料庫元資料,快取 1 小時
databaseMeta=3600

這個示例在資料庫服務上配置的,資料庫服務中,查詢次數遠遠大於新增、修改、刪除的次數,非常適合使用快取。

1. 快取資料 @Cacheable

@Override
@Cacheable(value = "databaseDef", key = "'all'")
public List<DatabaseDefinitionVo> selectAll() {
  return databaseDefinitionDao.selectAllVo();
}

特別注意:所有這些註解中,key 的值是 Spel 表示式,必須按照 Spel 要求來寫。上面這個例子中,直接定義返回值的 key 是 all 字串,需要加上單引號' 括起來,下面還有其他用法。

在例子中,下面的方法也使用了這個註解:

@Override
@Cacheable(value = "databaseDef", key = "#id.toString()")
public DatabaseDefinition selectByPrimaryKey(Long id) {
    Assert.notNull(id, "資料庫 ID 不能為空!");
    DatabaseDefinition definition = databaseDefinitionDao.selectByPrimaryKey(id);
    Assert.notNull(definition, "資料庫定義不存在!");
    return definition;
}

在上面註解中,key 中的 #id 指的是引數中的 id,在 IDEA 中會有自動提示。.toString() 是呼叫 id 的方法,在系統中規定了 key 必須是字串型別,所以當型別是 Long 的時候,需要轉換。

使用快取的目的就是為了減少上面兩個方法呼叫時減少和資料庫的互動,減小資料庫的壓力,這是兩個主要的快取資料的方法,下面的幾個操作都和上面這兩個方法有一定的關係

重點:這裡以及下面幾個註解中,都指定了 value = "databaseDef",這裡的意思是說,要使用前面配置中的 databaseDef 對應的配置,也就是會快取 30天

2. 更新快取 @CachePut

@Override
@CachePut(value = "databaseDef", key = "#result.id.toString()")
public DatabaseDefinition save(DatabaseDefinition definition, CurrentUser userModel) 
      throws ServiceException {
  //程式碼
  return definition;
}

更新快取的方法需要注意的是返回值,在上面 save 方法中,有可能是新增,有可能是更新,不管是那個操作,當操作完成後,上面註解會根據 key 的值生成 key,然後將方法的返回值作為 value 儲存到快取中。

這裡 key 的寫法中 #result 指代返回值,.id 是返回值的屬性,.toString() 是呼叫 id 屬性的方法,在系統中規定了 key 必須是字串型別,所以當型別是 Long 的時候,需要轉換。

這個方法上加的快取還有問題,當新增或者更新後,通過 selectAll() 返回的值已經發生了變化,但是這裡沒有清除 all 的快取值,會導致 selectAll() 出現髒資料,下面會通過 @Caching 註解改造這裡。

3. 清除快取 @CacheEvict

@Override
@CacheEvict(value = "databaseDef", key = "#id.toString()")
public void deleteByPrimaryKey(Long id) throws ServiceException {
  DatabaseDefinition definition = selectByPrimaryKey(id);
  if (definition.getLoadState().equals(DatabaseDefinition.LoadState.UP)) {
    throw new ServiceException("請先解除安裝資料庫!");
  }
  databaseDefinitionDao.deleteByPrimaryKey(id);
}

在上面新增或者修改的時候根據 id 快取或者更新了快取資料,這裡當刪除資料的時候,還需要清空對應的快取資料。

在上面註解中,key 中的 #id 指的是引數中的 id,在 IDEA 中會有自動提示。

這個方法上加的快取還有問題,當刪除後,通過 selectAll() 返回的值已經發生了變化,但是這裡沒有清除 all 的快取值,會導致 selectAll() 出現髒資料,下面會通過 @Caching 註解改造這裡。

4. 組合使用 @Caching

上面兩個註解中,都提到了髒資料,通過 @Caching 註解可以解決這個問題。

先修改第二個註解,來解決 save 時的髒資料:

@Override
@Caching(put = @CachePut(value = "databaseDef", key = "#result.id.toString()"),
         evict = @CacheEvict(value = "databaseDef", key = "'all'"))
public DatabaseDefinition save(DatabaseDefinition definition, CurrentUser userModel) 
      throws ServiceException {
  //其他程式碼
  return definition;
}

前面說明,新增或者修改的時候,all 快取中的資料已經不對了,因此這裡在 put 的同時,使用 evict'all' 中的資料清除,這就保證了 selelctAll 下次呼叫時,會重新從庫中讀取資料。

對上面的刪除方法,也進行類似的修改:

@Override
@Caching(evict = {
    @CacheEvict(value = "databaseDef", key = "#id.toString()"),
    @CacheEvict(value = "databaseDef", key = "'all'")
})
public void deleteByPrimaryKey(Long id) throws ServiceException {
  DatabaseDefinition definition = selectByPrimaryKey(id);
  if (definition.getLoadState().equals(DatabaseDefinition.LoadState.UP)) {
    throw new ServiceException("請先解除安裝資料庫!");
  }
  databaseDefinitionDao.deleteByPrimaryKey(id);
}

注意這裡的 evict 是個陣列,裡面配置了兩個清除快取的配置。

5. 全域性配置 @CacheConfig

在上面所有例子中,都指定了 value = "databaseDef",實際上可以通過在類上使用 @CacheConfig 註解配置當前類中的 cacheNames 值,配置後,如果和類上的 value 一樣就不需要在每個註解單獨配置。只有不同時再去指定,方法上的 value 值優先順序更高。

@Service
@CacheConfig(cacheNames = "databaseDef")
public class DatabaseDefinitionServiceImpl implements 
          DatabaseDefinitionService, DatabaseSqlExecuteService, ApplicationListener {

有了上面配置後,其他快取名字相同的地方可以簡化,例如刪除方法修改後如下:

@Override
@Caching(evict = {
    @CacheEvict(key = "#id.toString()"),
    @CacheEvict(key = "'all'")
})
public void deleteByPrimaryKey(Long id) throws ServiceException {
  DatabaseDefinition definition = selectByPrimaryKey(id);
  if (definition.getLoadState().equals(DatabaseDefinition.LoadState.UP)) {
    throw new ServiceException("請先解除安裝資料庫!");
  }
  databaseDefinitionDao.deleteByPrimaryKey(id);
}

其他例子

除了上面針對 databaseDef 的快取外,還有 databaseMeta 的配置:

@Override
@Cacheable(value = "databaseMeta", key = "#databaseId.toString()")
public List<TableVo> selectTablesByDatabaseId(Long databaseId) 
  		throws Exception {
  //程式碼
}

@Override
@Cacheable(value = "databaseMeta", key = "#databaseId + '_' + #tableName")
public TableVo selectTableByDatabaseIdAndTableName(Long databaseId, String tableName) 
  		throws Exception {
  //程式碼
}

這兩個方法是獲取資料庫元資料的,只有修改資料庫表的時候才會變化,因此不存在清除快取的情況,但是萬一修改表後,想要新的元資料,該怎麼辦?

因此增加了一個空的方法來清空資料,方法如下:

@Override
@CacheEvict(value = "databaseMeta", allEntries = true)
public void cleanTablesCache() {

}

這裡指定了 databaseMeta,通過 allEntries = true 清空所有 key 的快取。通過這個方法可以保證在有需要的時候清空所有元資料的快取。

實際上如果想要更精確的清除,可以傳入要清除的 databaseIdtableName 來更精確的清除。

增加快取後的效果

呼叫 selectTablesByDatabaseId 多次時,輸出的日誌如下:

INFO  c.n.d.u.DynamicDataSource - 當前資料來源:預設資料來源
DEBUG c.n.d.DataScopeContextProviderFilter - 服務呼叫返回前清除資料許可權資訊
INFO  c.n.d.u.DynamicDataSource - 當前資料來源:預設資料來源
DEBUG c.n.d.d.D.selectByPrimaryKey - ==>  Preparing: SELECT xxx (隱藏完整 SQL)
DEBUG c.n.d.d.D.selectByPrimaryKey - ==> Parameters: 1(Long)
DEBUG c.n.d.d.D.selectByPrimaryKey - <==      Total: 1
INFO  c.n.d.u.DynamicDataSource - 當前資料來源:10.10.10.130/datareporting
DEBUG c.n.d.DataScopeContextProviderFilter - 服務呼叫返回前清除資料許可權資訊
INFO  c.n.d.u.DynamicDataSource - 當前資料來源:預設資料來源
DEBUG c.n.d.DataScopeContextProviderFilter - 服務呼叫返回前清除資料許可權資訊
INFO  c.n.d.u.DynamicDataSource - 當前資料來源:預設資料來源
DEBUG c.n.d.DataScopeContextProviderFilter - 服務呼叫返回前清除資料許可權資訊

從日誌可以看出來,只有第一次進行了資料庫查詢,後續通過日誌看不到資料庫操作。

呼叫清空快取後,會再次查詢資料庫。

初次呼叫時,WEB請求花了 700多ms,後面再次呼叫時,平均不到 30 ms,這就是快取最明顯的作用。

連線到 redis 服務後,檢視所有 key,結果如下:

[email protected]:~$ redis-cli 
127.0.0.1:6379> keys *
1) "databaseMeta:1"
2) "databaseDef:all"
127.0.0.1:6379> 

快取中的資料都有 value 字首,上面快取了 all 和 id 為 1 的資料。

快取註解是一種最簡單的快取方式,但是需要配合 value 屬性的配置來使用,許多時候我們可能需要更精確的控制快取,此時可以使用 RedisTemplate 來控制。

四、通過 RedisTemplate 使用快取

有關這部分的詳細用法可以從網上搜索相關內容進行學習,這裡列舉一個簡單的例子。

針對前面的 selectAll 我們換一種方式進行快取。

首先注入下面的介面:

@Resource(name = "redisTemplate")
private ValueOperations<String, List> valueOper;

修改 selectAll 方法如下:

@Override
//@Cacheable(key = "'all'")
public List<DatabaseDefinitionVo> selectAll() {
  List<DatabaseDefinitionVo> vos = valueOper.get("databaseDef:all");
  if(vos != null){
    return vos;
  }
  vos = databaseDefinitionDao.selectAllVo();
  //快取 1 小時
  valueOper.set("databaseDef:all", vos, 1, TimeUnit.HOURS);
  return vos;
}

首先通過 valueOper.get("databaseDef:all") 嘗試獲取快取資訊,如果存在就直接返回。

如果不存在,就查詢資料庫,然後將查詢結果通過 set 進行快取。

特別注意: 上面的 key,寫的是 "databaseDef:all",也就是字首需要自己加上,如果直接寫成 all,在 Redis 中的 key 就是 all,不會自動增加字首。

如果沒有字首,那麼當不同系統都使用 all 時,資料就會混亂!

五、Redis 伺服器配置注意事項

內網使用的伺服器,特殊配置如下:

# By default protected mode is enabled. You should disable it only if
# you are sure you want clients from other hosts to connect to Redis
# even if no authentication is configured, nor a specific set of interfaces
# are explicitly listed using the "bind" directive.
# protected-mode yes
protected-mode no

關閉了保護模式。

# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES
# JUST COMMENT THE FOLLOWING LINE.
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# bind 127.0.0.1

註釋了繫結的 IP,這樣可以讓所有電腦訪問 Redis。