使用Spring緩存註解操作Redis
爲了進一步簡化Redis的使用,Spring還提供了緩存註解,使用這些註解可以有效簡化編程過程。
一、緩存管理器和緩存的啓用
Spring在使用緩存註解前,需要配置緩存管理器,如緩存類型、超時時間等。
Spring可以支持多種緩存的使用,提供了緩存處理器的接口CacheManager和與之相關的類,使用Redis,主要就是以使用類RedisCacheManager爲主。
在Spring Boot的starter機制中,允許我們通過配置文件生成緩存管理器,它提供的配置如下。
緩存管理器配置:
# SPRING CACHE (CacheProperties)
spring.cache.cache-names= # 如果由底層的緩存管理器支持創建,以逗號分隔的列表來緩存名稱
spring.cache.caffeine.spec= # caffeine緩存配置細節
spring.cache.couchbase.expiration=0ms # couchbase緩存超時時間,默認是永不超時
spring.cache.ehcache.config= # 配置ehcache緩存初始化文件路徑
spring.cache.infinispan.config= #infinispan緩存配置文件
spring.cache.jcache.config= #jcache緩存配置文件
spring.cache.jcache.provider= #jcache緩存提供者配置
spring.cache.redis.cache-null-values=true # 是否允許Redis緩存空值
spring.cache.redis.key-prefix= # Redis的鍵前綴
spring.cache.redis.time-to-live=0ms # 緩存超時時間戳,配置爲0則不設置超時時間
spring.cache.redis.use-key-prefix=true # 是否啓用Redis的鍵前綴
spring.cache.type= # 緩存類型,在默認的情況下,Spring會自動根據上下文探測
因爲使用的是Redis,所以其他的緩存並不需要我們去關注,這裏只是關注第1個和最後的5個配置項。下面我們可以在application.properties配置Redis的緩存管理器。
配置Redis緩存管理器:
spring.cache.type=REDIS
spring.cache.cache-names=redisCache
spring.cache.type:緩存類型,爲Redis,Spring Boot會自動生成RedisCacheManager對象,
spring.cache.cache-names:緩存名稱,多個名稱可以使用逗號分隔,以便於緩存註解的引用。
啓用緩存機制:加入驅動緩存的註解**@EnableCaching**
@SpringBootApplication(scanBasePackages = "com.springboot.chapter7")
@MapperScan(basePackages = "com.springboot.chapter7", annotationClass = Repository.class)
@EnableCaching
public class Chapter7Application {
......
}
二、開發緩存註解
1.配置文件配置
#數據庫配置
spring.datasource.url=jdbc:mysql://localhost:3306/spring_boot_chapter7
spring.datasource.username=root
spring.datasource.password=123456
#可以不配置數據庫驅動,Spring Boot會自己發現
#spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.tomcat.max-idle=10
spring.datasource.tomcat.max-active=50
spring.datasource.tomcat.max-wait=10000
spring.datasource.tomcat.initial-size=5
#設置默認的隔離級別爲讀寫提交
spring.datasource.tomcat.default-transaction-isolation=2
#mybatis配置
mybatis.mapper-locations=classpath:com/springboot/chapter7/mapper/*.xml
mybatis.type-aliases-package=com.springboot.chapter7.pojo
#日誌配置爲DEBUG級別,這樣日誌最爲詳細
logging.level.root=DEBUG
logging.level.org.springframework=DEBUG
logging.level.org.org.mybatis=DEBUG
#Redis配置
spring.redis.jedis.pool.min-idle=5
spring.redis.jedis.pool.max-active=10
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.max-wait=2000
spring.redis.port=6379
spring.redis.host=192.168.11.131
spring.redis.password=123456
#緩存配置
spring.cache.type=REDIS
spring.cache.cache-names=redisCache
2.用戶POJO:需要實現了Serializable接口。
package com.springboot.chapter2.common;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
/**
* AppUser對象
*/
@Data
@TableName("tms_app_user")
public class AppUserEntity implements Serializable {
@TableId(value = "id")
private Integer id;
/**
* 姓名
**/
private String name;
/**
* 手機
**/
private String phone;
...
}
3.MyBatis用戶操作接口
package com.springboot.chapter2.redis6;
import com.springboot.chapter2.common.AppUserEntity;
import org.apache.ibatis.annotations.*;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface RedisUserDao {
// 獲取單個用戶
@Select("select * from tms_app_user where id = #{id}")
AppUserEntity getUser(Long id);
// 保存用戶
@Update("insert into tms_app_user (name,phone) Values ( #{user.name},#{user.phone})")
@Options(useGeneratedKeys = true, keyProperty = "user.id")
int insertUser(@Param("user") AppUserEntity user);
// 修改用戶
@Update("update tms_app_user set name = #{user.name} where id =#{user.id}")
int updateUser(@Param("user") AppUserEntity user);
// 查詢用戶,指定MyBatis的參數名稱
@Select("select * from tms_app_user where userName = #{userName} and phone = #{phone}")
List<AppUserEntity> findUsers(@Param("userName") String userName,
@Param("phone") String phone);
// 刪除用戶
@Delete("delete from tms_app_user where id =#{id}")
int deleteUser(Long id);
}
註解@Repository標識它是DAO層,將來可以定義掃描來使得這個接口被掃描爲Spring的Bean裝配到IoC容器中;
這裏還可以看到增刪查改的方法,通過它們就可以測試Spring的緩存註解了,
4.用戶服務接口
package com.springboot.chapter2.redis6;
import com.springboot.chapter2.common.AppUserEntity;
import java.util.List;
public interface RedisUserService {
// 獲取單個用戶
AppUserEntity getUser(Long id);
// 保存用戶
AppUserEntity insertUser(AppUserEntity user);
// 修改用戶,指定MyBatis的參數名稱
AppUserEntity updateUserName(Long id, String userName);
// 查詢用戶,指定MyBatis的參數名稱
List<AppUserEntity> findUsers(String userName, String phone);
// 刪除用戶
int deleteUser(Long id);
}
5.用戶實現類使用Spring緩存註解
package com.springboot.chapter2.redis6;
import com.springboot.chapter2.common.AppUserEntity;
import org.apache.ibatis.annotations.Select;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class RedisUserServiceImpl implements RedisUserService {
@Autowired
private RedisUserDao redisUserDao = null;
// 插入用戶,最後MyBatis會回填id,取結果id緩存用戶
@Override
@Transactional
@CachePut(value ="redisCache", key = "'redis_user_'+#result.id")
public AppUserEntity insertUser(AppUserEntity user) {
redisUserDao.insertUser(user);
return user;
}
// 獲取id,取參數id緩存用戶
@Override
@Cacheable(value ="redisCache", key = "'redis_user_'+#id")
public AppUserEntity getUser(Long id) {
return redisUserDao.getUser(id);
}
// 更新數據後,更新緩存,如果condition配置項使結果返回爲null,不緩存
@Override
@Transactional
@CachePut(value ="redisCache",
condition="#result != 'null'", key = "'redis_user_'+#id")
public AppUserEntity updateUserName(Long id, String userName) {
// 此處調用getUser方法,該方法緩存註解失效,
// 所以這裏還會執行SQL,將查詢到數據庫最新數據
AppUserEntity user =this.getUser(id);
if (user == null) {
return null;
}
user.setName(userName);
redisUserDao.updateUser(user);
return user;
}
// 命中率低,所以不採用緩存機制
@Override
public List<AppUserEntity> findUsers(String userName, String note) {
return redisUserDao.findUsers(userName, note);
}
// 移除緩存
@Override
@Transactional
@CacheEvict(value ="redisCache", key = "'redis_user_'+#id",
beforeInvocation = false)
public int deleteUser(Long id) {
return redisUserDao.deleteUser(id);
}
}
關注下註解@CachePut、@Cacheable和@CacheEvict。
•@CachePut表示將方法結果返回存放到緩存中。
•@Cacheable 表示先從緩存中通過定義的鍵查詢,如果可以查詢到數據,則返回,否則執行該方法,返回數據,並且將返回結果保存到緩存中。
•@CacheEvict 通過定義的鍵移除緩存,它有一個Boolean類型的配置項beforeInvocation,表示在方法之前或者之後移除緩存。因爲其默認值爲false,所以默認爲方法後將緩存移除。
其次,關注下配置value =“redisCache”,因爲我們在Spring Boot中配置了對應的緩存名稱爲“redisCache”,這樣它就能夠引用到對應的緩存了,而鍵配置項則是一個Spring EL,很多時候可以看到配置爲’redis_user_’+#id,其中#id代表參數,它是通過參數名稱來匹配,所以這樣配置要求方法存在一個參數且名稱爲id,除此之外還可以這樣引用參數,如#a[0]或者#p[0]代表第一個參數,#a[1]或者#p[1]代表第二個參數……但是這樣引用可讀性較差,所以我們一般不這麼寫,通過這樣定義,
Spring就會用EL返回字符串作爲鍵去存放緩存了。
再次,有時候我們希望使用返回結果的一些屬性緩存數據,如insertUser方法。在插入數據庫前,對應的用戶是沒有id的,而這個id值會在插入數據庫後由MyBatis的機制回填,所以我們希望使用返回結果,這樣使用#result就代表返回的結果對象了,它是一個User對象,所以#result.id是取出它的屬性id,這樣就可以引用這個由數據庫生成的id了。
第四,看到updateUserName方法,從代碼中可以看到方法,可能返回null。如果爲null,則不需要緩存數據,所以在註解@CachePut中加入了condition配置項,它也是一個Spring EL表達式,這個表達式要求返回Boolean類型值,如果爲true,則使用緩存操作,否則就不使用。這裏的表達式爲#result != ‘null’,意味着如果返回null,則方法結束後不再操作緩存。同樣地,@Cacheable和@CacheEvict也具備這個配置項。
第五,在updateUserName方法裏面我們先調用了getUser方法,因爲是更新數據,所以需要慎重一些。一般我們不要輕易地相信緩存,因爲緩存存在髒讀的可能性,這是需要注意的,在需要更新數據時我們往往考慮先從數據庫查詢出最新數據,而後再進行操作。因此,這裏使用了getUser方法,這裏會存在一個誤區,很多讀者認爲getUser方法因爲存在了註解@Cacheable,所以會從緩存中讀取數據,而從緩存中讀取去更新數據,是一個比較危險的行爲,因爲緩存的數據可能存在髒數據,然後這裏的事實是這個註解@Cacheable失效了,也就是說使用updateUserName方法調用getUser方法的邏輯,並不存在讀取緩存的可能,它每次都會執行SQL查詢數據。關於這個緩存註解失效的問題,在後續章節再給予說明,這裏只是提醒讀者,更新數據時應該謹慎一些,儘量避免讀取緩存數據,因爲緩存會存在髒數據的可能。
最後,我們看到findUsers方法,這個方法並沒有使用緩存,因爲查詢結果隨着用戶給出的查詢條件變化而變化,導致命中率很低。
對於命中率很低的場景,使用緩存並不能有效提供系統性能,所以這個方法並不採用緩存機制。此外,對於大數據量等消耗資源的數據,使用緩存也應該謹慎一些。
三、測試緩存註解
6.使用用戶控制器測試緩存註解
package com.springboot.chapter2.redis6;
import com.springboot.chapter2.common.AppUserEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Controller
@RequestMapping("/user")
public class RedisUserController {
@Autowired
private RedisUserService userService = null;
@RequestMapping("/getUser")
@ResponseBody
public AppUserEntity getUser(Long id) {
return userService.getUser(id);
}
@RequestMapping("/insertUser")
@ResponseBody
public AppUserEntity insertUser(String userName, String phone) {
AppUserEntity user = new AppUserEntity();
user.setName(userName);
user.setPhone(phone);
userService.insertUser(user);
return user;
}
@RequestMapping("/findUsers")
@ResponseBody
public List<AppUserEntity> findUsers(String userName, String phone) {
return userService.findUsers(userName, phone);
}
@RequestMapping("/updateUserName")
@ResponseBody
public Map<String, Object> updateUserName(Long id, String userName) {
AppUserEntity user = userService.updateUserName(id, userName);
boolean flag = user != null;
String message = flag? "更新成功" : "更新失敗";
return resultMap(flag, message);
}
@RequestMapping("/deleteUser")
@ResponseBody
public Map<String, Object> deleteUser(Long id) {
int result = userService.deleteUser(id);
boolean flag = result == 1;
String message = flag? "刪除成功" : "刪除失敗";
return resultMap(flag, message);
}
private Map<String, Object> resultMap(boolean success, String message) {
Map<String, Object> result = new HashMap<String, Object>();
result.put("success", success);
result.put("message", message);
return result;
}
}
7.Spring Boot啓動文件
package com.springboot.chapter2.redis6;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Repository;
import javax.annotation.PostConstruct;
@SpringBootApplication(scanBasePackages = "com.springboot.chapter2.redis6")
// 指定掃描的MyBatis Mapper
@MapperScan(basePackages = "com.springboot.chapter2.redis6", annotationClass = Repository.class)
// 使用註解驅動緩存機制
@EnableCaching
public class Redis6Application {
// 注入RedisTemplate
@Autowired
private RedisTemplate redisTemplate = null;
// 定義自定義後初始化方法
@PostConstruct
public void init() {
initRedisTemplate();
}
// 設置RedisTemplate的序列化器
private void initRedisTemplate() {
RedisSerializer stringSerializer = redisTemplate.getStringSerializer();
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
}
public static void main(String[] args) throws Exception {
SpringApplication.run(Redis6Application.class, args);
}
}
}
這裏定義了MyBatis Mapper的掃描包,並限定了在標註有@Repository的接口才會被掃描,同時使用@EnableCaching驅動Spring緩存機制運行,並且通過@PostConstruct定義自定義初始化方法去自定義RedisTemplate的一些特性。
8.測試
http://localhost:8080/user/getUser?id=1
http://localhost:8080/user/insertUser?userName=%27aa%27&&phone=13826005211
http://localhost:8080/user/updateUserName?id=1&userName=%27aa%27
http://localhost:8080/user/deleteUser?id=200020
打開Redis客戶端,可以查詢到:
Redis緩存機制會使用#{cacheName}:#{key}的形式作爲鍵保存數據,其次對於這個緩存是永遠不超時的,會帶來緩存不會被刷新的問題,需要根據業務來做相關處理。
四、緩存註解自調用失效問題
上面的updateUserName方法調用getUser方法中,getUser方法上的註解將會失效,原因:
Spring的緩存機制也是基於Spring AOP的原理,而在Spring中AOP是通過動態代理技術來實現的,
這裏的updateUserName方法調用getUser方法是類內部的自調用,並不存在代理對象的調用,這樣便不會出現AOP,也就不會使用到標註在getUser上的緩存註解去獲取緩存的值了。
五、緩存髒數據說明
對於數據的讀操作,一般而言是允許不是實時數據,如一些電商網站還存在一些排名榜單,而這個排名往往都不是實時的,它會存在延遲,其實對於查詢是可以存在延遲的,也就是存在髒數據是允許的。
但是如果一個髒數據始終存在就說不通了,這樣會造成數據失真比較嚴重。一般對於查詢而言,我們可以規定一個時間,讓緩存失效,在Redis中也可以設置超時時間,當緩存超過超時時間後,則應用不再能夠從緩存中獲取數據,而只能從數據庫中重新獲取最新數據,以保證數據失真不至於太離譜。對於那些要求實時性比較高的數據,我們可以把緩存時間設置得更少一些,這樣就會更加頻繁地刷新緩存,而不利的是會增加數據庫的壓力;對於那些要求不是那麼高的,則可以使超時時間長一些,這樣就可以降低數據庫的壓力。
對於數據的寫操作,往往採取的策略就完全不一樣,需要我們謹慎一些,一般會認爲緩存不可信,所以會考慮從數據庫中先讀取最新數據,然後再更新數據,以避免將緩存的髒數據寫入數據庫中,導致出現業務問題。
緩存談到了超時時間:在Spring Boot中,默認的RedisCacheManager會採用永不超時的機制,這樣便不利於數據的及時更新,可以自定義緩存管理器來解決這個問題。
六、自定義緩存管理器
如果我們並不希望採用Spring Boot機制帶來的鍵命名方式,也不希望緩存永不超時,這時我們可以自定義緩存管理器。在Spring中,我們有三種方法定製緩存管理器:
一種在使用時設置超時時間,例如:stringRedisTemplate.opsForValue().set(“name”,“aa”,2, TimeUnit.MINUTES),推薦這種。
一種是通過配置消除緩存鍵的前綴和自定義超時時間的屬性來定製生成RedisCacheManager;
一種是不採用Spring Boot爲我們生成的方式,而是完全通過自己的代碼創建緩存管理器,尤其是當需要比較多自定義的時候,更加推薦你採用自定義的代碼。
重置Redis緩存管理器
# 禁用前綴
spring.cache.redis.use-key-prefix=false
# 允許保存空值
#spring.cache.redis.cache-null-values=true
# 自定義前綴
#spring.cache.redis.key-prefix=
# 定義超時時間,單位毫秒
spring.cache.redis.time-to-live=600000
spring.cache.redis.use-key-prefix=false:消除了前綴的配置,
spring.cache.redis.time-to-live=600000:將超時時間設置爲10 min(600000 ms),這樣10 min過後Redis的鍵就會超時,就不能從Redis中讀取到數據了,而只能重新從數據庫讀取數據,
這樣就能有效刷新數據了。
經過上面的修改,清除Redis的數據,重啓Spring Boot應用,重新測試控制器的getUser方法,然後在10 min內打開Redis客戶端依次輸入以下命令:
keys * #查看Redis存在的鍵值對
get redis_user_1 #獲取id爲1的用戶信息
ttl redis_user_1 #查詢鍵的剩餘超時秒數
Spring Boot爲我們自定義的前綴消失了,而我們也成功地設置了超時時間。
有時候,在自定義時可能存在比較多的配置,也可以不採用Spring Boot自動配置的緩存管理器,而是使用自定義的緩存管理器,
自定義緩存管理器:給IoC容器增加緩存管理器
// 注入連接工廠,由Spring Boot自動配置生成
@Autowired
private RedisConnectionFactory connectionFactory = null;
// 自定義Redis緩存管理器
@Bean(name = "redisCacheManager" )
public RedisCacheManager initRedisCacheManager() {
// Redis加鎖的寫入器
RedisCacheWriter writer= RedisCacheWriter.lockingRedisCacheWriter(connectionFactory);
// 啓動Redis緩存的默認設置
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// 設置JDK序列化器
config = config.serializeValuesWith(
SerializationPair.fromSerializer(new JdkSerializationRedisSerializer()));
// 禁用前綴
config = config.disableKeyPrefix();
//設置10 min超時
config = config.entryTtl(Duration.ofMinutes(10));
// 創建緩Redis存管理器
RedisCacheManager redisCacheManager = new RedisCacheManager(writer, config);
return redisCacheManager;
}
這裏首先注入了RedisConnectionFactory對象,該對象是由Spring Boot自動生成的。
在創建Redis緩存管理器對象RedisCacheManager的時候,首先創建了帶鎖的RedisCacheWriter對象;
然後使用RedisCacheConfiguration對其屬性進行配置,這裏設置了禁用前綴,並且超時時間爲10 min;
最後就通過RedisCacheWriter對象和RedisCacheConfiguration對象去構建RedisCacheManager對象了,這樣就完成了Redis緩存管理器的自定義。