之前一篇文章 SpringBoot整合Redis 已經介紹了在SpringBoot中使用redisTemplate手動
操作redis數據庫的方法了。其實這個時候我們就已經可以拿redis來做項目了,需要緩存服務的地方
就引入redisTemplate就行啦。
但是這里有個問題,緩存歸根結底不是業務的核心,只是作為功能和效率上的輔助,如果我現在在某個
項目中用到緩存的地方特別多,那豈不是意味着我需要大量的寫redisTempate的增刪改的方法。而且還需要很多判斷,
比如查詢的時候判斷緩存是否已經存在,如果存在則直接返回,如果不存在則先去查詢關系型數據庫,再將返回的值存入緩存,
在返回給控制器,類似這樣的邏輯要是多了還是比較麻煩,比如下面這段偽代碼。
1 if(cache.has(userId)) 2 return cache.get(userId); 3 else 4 { 5 User user = db.get(userId); 6 cache.put(userId, user); 7 return user; 8 }
這個時候怎么辦呢,我們可以很自然的想到Spring的核心之一AOP,它可以幫助我們實現橫切關注點分離。就是把分散在
各段代碼里面的非業務邏輯給抽取成一個切面,然后在需要切面的方法上加上注解。我們再看上面的場景,不難發現,針對redis
增刪改的操作都可以抽取出來,我們自己只需要編寫必需的業務邏輯,比如我從mysql查詢某個用戶,這段代碼自己實現,然后加上注解
之后,會通過動態代理在我的方法前后加上對應的緩存邏輯。
說了這么多,就是讓看官知道此處切面的必要性。那么可能看官又要問了,我們自己需要去實現這些切面,一個切面還好,要是針對不同的方法
有不同的切面,那也很麻煩啊。不用慌,Spring已經為我們考慮到了。Spring3.0后提供了Cache和CacheManager等接口,其他緩存服務
可以去實現Spring的接口,然后按Spring的語法來使用,我這里使用是Redis來和SpringCache做集成。
關於SpringCache的詳細知識和代碼需要看官自行研究,本文只是淺顯的介紹使用,其原理的實現我也還沒太搞清楚。
下面請看代碼實現:
開啟Spring對緩存的支持
@EnableCaching注解
1 @SpringBootApplication 2 @EnableCaching 3 public class RedisApplication { 4 5 public static void main(String[] args) { 6 SpringApplication.run(RedisApplication.class, args); 7 } 8 9 }
編寫業務邏輯
User
1 package com.example.redis.domain; 2 3 import java.io.Serializable; 4 5 public class User implements Serializable { 6 7 private static final long serialVersionUID = 10000000L; 8 9 private Long id; 10 11 private String name; 12 13 private Integer age; 14 15 public User() { 16 17 } 18 19 public User(Long id, String name, Integer age) { 20 this.id = id; 21 this.name = name; 22 this.age = age; 23 } 24 25 public Long getId() { 26 return id; 27 } 28 29 public void setId(Long id) { 30 this.id = id; 31 } 32 33 public String getName() { 34 return name; 35 } 36 37 public void setName(String name) { 38 this.name = name; 39 } 40 41 public Integer getAge() { 42 return age; 43 } 44 45 public void setAge(Integer age) { 46 this.age = age; 47 } 48 49 @Override 50 public String toString() { 51 return "User{" + 52 "id=" + id + 53 ", username='" + name + '\'' + 54 ", age=" + age + 55 '}'; 56 } 57 58 }
UserService
1 package com.example.redis.service; 2 3 import com.example.redis.domain.User; 4 5 public interface UserService { 6 7 /** 8 * 刪除 9 * 10 * @param user 用戶對象 11 * @return 操作結果 12 */ 13 User saveOrUpdate(User user); 14 15 /** 16 * 添加 17 * 18 * @param id key值 19 * @return 返回結果 20 */ 21 User get(Long id); 22 23 /** 24 * 刪除 25 * 26 * @param id key值 27 */ 28 void delete(Long id); 29 30 }
UserServiceImpl
1 package com.example.redis.service; 2 3 import com.example.redis.domain.User; 4 import org.slf4j.Logger; 5 import org.slf4j.LoggerFactory; 6 import org.springframework.cache.annotation.CacheEvict; 7 import org.springframework.cache.annotation.CachePut; 8 import org.springframework.cache.annotation.Cacheable; 9 import org.springframework.stereotype.Service; 10 11 import java.util.HashMap; 12 import java.util.Map; 13 14 @Service 15 public class UserServiceImpl implements UserService{ 16 17 private static final Map<Long, User> DATABASES = new HashMap<>(); 18 19 static { 20 DATABASES.put(1L, new User(1L, "張三", 18)); 21 DATABASES.put(2L, new User(2L, "李三", 19)); 22 DATABASES.put(3L, new User(3L, "王三", 20)); 23 } 24 25 private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class); 26 27 @Cacheable(value = "user", unless = "#result == null") 28 @Override 29 public User get(Long id) { 30 // TODO 我們就假設它是從數據庫讀取出來的 31 log.info("進入 get 方法"); 32 return DATABASES.get(id); 33 } 34 35 @CachePut(value = "user") 36 @Override 37 public User saveOrUpdate(User user) { 38 DATABASES.put(user.getId(), user); 39 log.info("進入 saveOrUpdate 方法"); 40 return user; 41 } 42 43 @CacheEvict(value = "user") 44 @Override 45 public void delete(Long id) { 46 DATABASES.remove(id); 47 log.info("進入 delete 方法"); 48 } 49 50 }
UserServcieImpl這里面就是重頭戲了,可以看到其中的每個方法上面都有@Cache...注解,我先介紹一下
這些注解是干嘛的。
我簡單的歸納一下,@Cacheable注解適用於查詢,@CachePut適用於修改和新增,@CacheEvict則適用於刪除。
@Caching呢我還沒用使用,就不說了,也簡單。
對應到UserServiceImpl中的get,saveOrUpdate,delete三個方法則很好理解了,我描述一下增刪改查的邏輯哈:
查詢:如果緩存中由則直接返回,如果無則從數據源中拿到,放入緩存中。
新增和修改:如果緩存中沒有則新增,如果有則修改。
刪除:如果由則刪除。
注意我這里的數據源使用的是Map,因為電腦上沒有安裝Mysql所以就簡單直接使用map了。
就把map當成一個簡單的mysql數據庫吧。
業務邏輯寫完了還需要配置一下,因為SpringCache最終操作redis數據庫也要用到redisTemplate,而redisTemplate
默認使用的序列化器是JdkSerializationRedisSerializer,對中文和對象的支持不太友好,所以需要配置下redisTemplate的
序列化器:
1 package com.example.redis.config; 2 3 import org.springframework.boot.autoconfigure.AutoConfigureAfter; 4 import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; 5 import org.springframework.context.annotation.Bean; 6 import org.springframework.context.annotation.Configuration; 7 import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; 8 import org.springframework.data.redis.core.RedisTemplate; 9 import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; 10 import org.springframework.data.redis.serializer.StringRedisSerializer; 11 12 import java.io.Serializable; 13 14 @Configuration 15 @AutoConfigureAfter(RedisAutoConfiguration.class) 16 public class RedisConfig { 17 18 /** 19 * 配置自定義redisTemplate 20 */ 21 @Bean 22 public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory redisConnectionFactory) { 23 RedisTemplate<String, Serializable> template = new RedisTemplate<>(); 24 template.setKeySerializer(new StringRedisSerializer()); 25 template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); 26 template.setConnectionFactory(redisConnectionFactory); 27 return template; 28 } 29 30 }
然后還需要配置一下CacheManager,主要配置自定義的key的超時時間,是否使用前綴來拼接key,是否允許空值入庫
等等。另外還定義了key的生成器,因為像@Cacheable(value = "user", unless = "#result == null")這種沒有定義key的,
默認的keyGenerator會使用方法的參數來拼湊一個key出來。拼湊的規則就像下面這樣:
這樣的話是有問題的,因為很多方法可能具有相同的參數名,它們生成的key是一樣的,這樣就會取到同一個鍵對象。
所以需要自定義一個keyGenerator。
1 package com.example.redis.config; 2 3 import com.example.redis.utils.BaseUtil; 4 import org.slf4j.Logger; 5 import org.slf4j.LoggerFactory; 6 import org.springframework.beans.factory.annotation.Autowired; 7 import org.springframework.cache.CacheManager; 8 import org.springframework.cache.annotation.CachingConfigurerSupport; 9 import org.springframework.cache.interceptor.KeyGenerator; 10 import org.springframework.context.annotation.Configuration; 11 import org.springframework.data.redis.cache.RedisCacheConfiguration; 12 import org.springframework.data.redis.cache.RedisCacheManager; 13 import org.springframework.data.redis.cache.RedisCacheWriter; 14 import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; 15 import org.springframework.data.redis.core.script.DigestUtils; 16 import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; 17 import org.springframework.data.redis.serializer.RedisSerializationContext; 18 import org.springframework.data.redis.serializer.StringRedisSerializer; 19 20 import java.lang.reflect.Field; 21 import java.lang.reflect.Method; 22 import java.time.Duration; 23 24 @Configuration 25 //@AutoConfigureAfter(RedisCacheConfiguration.class) 26 public class RedisCacheConfig extends CachingConfigurerSupport { 27 28 private Logger logger = LoggerFactory.getLogger(RedisCacheConfig.class); 29 30 @Autowired 31 private LettuceConnectionFactory redisConnectionFactory; 32 33 @Override 34 public CacheManager cacheManager() { 35 // 重新配置緩存 36 RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig(); 37 38 //設置緩存的默認超時時間:30分鍾 39 redisCacheConfiguration = redisCacheConfiguration.entryTtl(Duration.ofMinutes(30L)) 40 .disableCachingNullValues() 41 .disableKeyPrefix() 42 .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) 43 .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer((new GenericJackson2JsonRedisSerializer()))); 44 45 return RedisCacheManager.builder(RedisCacheWriter 46 .nonLockingRedisCacheWriter(redisConnectionFactory)) 47 .cacheDefaults(redisCacheConfiguration).build(); 48 } 49 50 @Override 51 public KeyGenerator keyGenerator(){ 52 return new KeyGenerator() { 53 @Override 54 public Object generate(Object target, Method method, Object... params) { 55 StringBuilder sb = new StringBuilder(); 56 sb.append(target.getClass().getName()); 57 sb.append("&"); 58 for (Object obj : params) { 59 if (obj != null){ 60 if(!BaseUtil.isBaseType(obj)) { 61 try { 62 Field id = obj.getClass().getDeclaredField("id"); 63 id.setAccessible(true); 64 sb.append(id.get(obj)); 65 } catch (NoSuchFieldException | IllegalAccessException e) { 66 logger.error(e.getMessage()); 67 } 68 } else{ 69 sb.append(obj); 70 } 71 } 72 } 73 74 logger.info("redis cache key str: " + sb.toString()); 75 logger.info("redis cache key sha256Hex: " + DigestUtils.sha1DigestAsHex(sb.toString())); 76 return DigestUtils.sha1DigestAsHex(sb.toString()); 77 } 78 }; 79 } 80 }
需要說明的是,這里的keyGenerator和cacheManager需要各位看官根據自己的業務場景來自行定義,
切勿模仿,我都是亂寫的。
另外還有配置文件怎么配置的,很簡單,我直接貼出來:
application.yml
server:
port: 80
spring:
cache:
type:
redis
profile: dev
application-dev.yml
spring:
redis:
host: localhost
port: 6379
# 如果使用的jedis 則將lettuce改成jedis即可
lettuce:
pool:
# 最大活躍鏈接數 默認8
max-active: 8
# 最大空閑連接數 默認8
max-idle: 8
# 最小空閑連接數 默認0
min-idle: 0
我本地做測試把配置文件分成了兩個,看官合成一個就可以了。
好,到此一步,邏輯、Java配置和配置文件都編寫好了,接下來測試程序:
1 package com.example.redis; 2 3 import com.example.redis.domain.User; 4 import com.example.redis.service.UserService; 5 import org.junit.runner.RunWith; 6 import org.slf4j.Logger; 7 import org.slf4j.LoggerFactory; 8 import org.springframework.beans.factory.annotation.Autowired; 9 import org.springframework.boot.test.context.SpringBootTest; 10 import org.springframework.test.context.junit4.SpringRunner; 11 12 @RunWith(SpringRunner.class) 13 @SpringBootTest 14 public class Test { 15 16 private static final Logger log = LoggerFactory.getLogger(Test.class); 17 18 @Autowired 19 private UserService userService; 20 21 @org.junit.Test 22 public void contextLoads() { 23 User user = userService.saveOrUpdate(new User(1L, "張三", 21)); 24 log.info("[saveOrUpdate] - [{}]", user); 25 final User user1 = userService.get(1L); 26 log.info("[get] - [{}]", user1); 27 } 28 }
再來看一下數據源里面有哪些數據:
現在的張三是{id:1, name:'張三',age:18},我對id為1這條數據修改一下,把age改成了21,按照我們之前的
邏輯此時應該修改完成之后會把修改的結果放入緩存中,那么下面查詢的時候應該不會走真實的查詢邏輯,而是直接從
緩存里面取數據,此時查詢不應該輸出log.info("進入 get 方法");這段日志。
運行程序看下結果:
從日志上看是沒啥問題的,再看看redis數據庫:
對的,存的確實是修改過后的值21,所以我們就算是基本成功了。
需要說明的是,關於SpringCache這塊還有很多需要學習的地方,比如如何配置多個緩存服務,如何配置二級緩存等等,
我也還在學習中。
本文到此結束,謝謝各位看官。
參考資料
《Spring實踐》第四版,也就是《Spring In Action》
https://www.jianshu.com/p/6ba2d2dbf36e
https://www.iteye.com/blog/412887952-qq-com-2397144
https://www.iteye.com/blog/anhongyang125-2354554
https://www.cnblogs.com/wsjun/p/9777575.html
https://www.jianshu.com/p/2a584aaafad3