Spring AOP 用註解封裝 redis 緩存

2020-11-08 拒海

前言

面試時問到用沒用過 AOP,很多回答都是用 AOP 做過日誌統一處理。[what],給人感覺就是沒做過啊

今天介紹一個用註解封裝 redis 緩存的 AOP 實戰

redis 緩存加速的基本邏輯

用 redis 加速資料庫訪問,一般會寫出如下代碼

@Servicepublic class UserServiceImpl implements UserService { private final UserMapper userMapper; private final RedisClient redisClient; @Autowired public UserServiceImpl(UserMapper userMapper, RedisClient redisClient) { this.userMapper = userMapper; this.redisClient = redisClient; } @Override public User get(Long id) { String key = String.format("USER:%d", id); String value = redisClient.get(key); if (StringUtils.isNotEmpty(value)) { return JSON.parseObject(value, User.class); } User user = userMapper.get(id); if (user != null) { redisClient.set(key, JSON.toJSONString(user)); } return user; }}

其實現邏輯如下

  • 根據輸入參數構造 redis 的 key
  • 從 redis 裡獲取該 key 的值
    • 如果值不為空,則命中緩存,直接返回
  • redis 未命中
    • 穿透到資料庫查詢
    • 如果資料庫查詢到該值,緩存到 redis
    • 返回

實際上用 redis 緩存加速資料庫查詢基本都是這樣的套路,如果在每個 service 都要這樣寫一遍,太繁瑣了,用註解來封裝一下吧

思路

通過 Spring 的 AOP 技術,攔截從資料庫查詢的方法,在從資料庫獲取結果之前,先從 redis 獲取,如果 redis 命中,則直接返回;否則就繼續執行從資料庫獲取的方法,將返回值緩存到 reids 並返回

實際上不限於從資料庫獲取結果,如果是從遠程服務獲取值,也可以採用同樣的思路

實戰步驟

step1:標記要攔截的方法

很顯然用註解是個不錯的主義,定義如下注解

/** * 這個標註用來為redis的<b>通用化</b>存取設定參數 */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Redis { /** 非null值 默認過期時間 **/ int DEFAULT_TTL = 4 * 60 * 60 + 10 * 60; /** null值 默認過期時間 **/ int NULL_TTL = 5 * 60; /** redsi key,見 {@link RedisKeys} **/ String value(); /** * <pre> * 指示方法的哪些參數用來構造key,及其順序(編號由0開始) * * 示例 * keyArgs = {1,0,2},表示用方法的第二,第一,第三個參數,按順序來構造key * * 默認值的意思是方法的前 n 個參數來構造key,n 最大為10 * 這樣如果構造 key 的參數不多於 10 個且順序也和方法參數一致,則可以用默認值 * </pre> */ int[] keyArgs() default { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; /** 執行何種操作,默認是先訪問 redis **/ RedisAction action() default RedisAction.REDIS_FIRST; /** 過期時間,默認250分鐘 **/ int ttl() default DEFAULT_TTL; /** 是否以同步的方式操作redis,默認是false **/ boolean sync() default false; /** 是否要緩存 null 值,默認為true **/ boolean cacheNull() default true; /** 如果要緩存null值,過期時間是多少,默認5分鐘 **/ int nullTtl() default NULL_TTL;}public enum RedisAction { // 優先從 redis 獲取 REDIS_FIRST, // 穿透 redis 到 db 獲取 STAB_REDIS;}

這個註解用在需要攔截的方法上,還附帶了一些元信息

接下來是在目標方法上使用註解

@Servicepublic class UserServiceImpl2 implements UserService { private final UserMapper userMapper; @Autowired public UserServiceImpl2(UserMapper userMapper) { this.userMapper = userMapper; } @Redis("USER:%d") @Override public User get(Long id) { return userMapper.get(id); }}

現在代碼就很簡潔明了

step2:編寫攔截器

@Aspect@Componentpublic class RedisInterceptor { private static final Logger LOG = LoggerFactory.getLogger(RedisInterceptor.class); @Resource private RedisClient redisClient; @Around("@annotation(redis)") public Object doAround(ProceedingJoinPoint pjp, Redis redis) throws Throwable { MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); // 是否穿透 redis boolean stab = redis.action() == RedisAction.STAB_REDIS; Object[] keyArgs = getKeyArgs(pjp.getArgs(), redis.keyArgs()); String key = keyArgs == null ? redis.value() : String.format(redis.value(), keyArgs); Class<?> returnType = method.getReturnType(); Object result = stab ? null : get(key, returnType); if (result == null) { result = pjp.proceed(); if (result != null) { setex(key, redis.ttl(), result, redis.sync()); } else if (redis.cacheNull()) { setex(key, redis.nullTtl(), result, redis.sync()); } } return result; } /** 獲取構造 redis 的 key 的參數數組 */ private Object[] getKeyArgs(Object[] args, int[] keyArgs) { Object[] redisKeyArgs; int len = keyArgs.length; if (len == 0) { return null; } else { len = min(len, args.length); redisKeyArgs = new Object[len]; int i = 0; for (int n : keyArgs) { redisKeyArgs[i++] = args[n]; if (i >= len) { break; } } return redisKeyArgs; } } private int min(int i, int j) { return i > j ? j : i; } private <T> void setex(final String key, final int ttl, final T data, boolean sync) { try { redisClient.setex(key, ttl, data, sync); } catch (Exception e) { LOG.error("redis set error:{}", e.getMessage(), e); } } private <T> T get(String key, Class<T> clazz) { try { return redisClient.get(key, clazz); } catch (Exception e) { LOG.error("redis get error:{}", e.getMessage(), e); return null; } }}

step3:配置攔截器

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 開啟註解式 AOP --> <aop:aspectj-autoproxy proxy-target-class="false" /></beans>

這樣就 ok 了,在你想要使用 redis 緩存加速的方法上加上 redis 註解吧

關於 redis 穿透

通常情況下,都是優先從 redis 裡查詢結果。但也有時候需要穿透 redis,到 db 裡去獲取結果的。

@Redis 註解也支持這種操作,只需要設置 action 屬性為 STAB_REDIS 即可

但是,同一個方法,action 要麼是 STAB_REDIS,要麼是 REDIS_FIRST,攔截器只能實現其中一種操作,如何才能讓攔截器攔截同一個方法時,實現不同的 redis 操作呢?

有以下幾個辦法

  1. 方法的參數裡添加一個專門的變量,用來告訴攔截器做何種操作
  2. 複製該方法為另一個方法,2個方法作用一樣,註解也一樣,區別是註解的 action 屬性不同
  3. 兩個不同名的方法,實際上代碼是一樣的

經過考慮,最終採用了第2種辦法

總結

在 Spring 配置文件裡開啟註解式 AOP,使用如下配置

<aop:aspectj-autoproxy proxy-target-class="false" />

自定義註解,如下 2 個元註解必須

@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)

針對自定義的註解編寫攔截器代碼

攔截器要用到反射,泛型啥的,一切皆有可能

攔截器配置,注意如下幾個註解的使用

@Aspect@Component@Around("@annotation(******)")


相關焦點

  • Spring 極速集成註解 Redis 實踐
    Spring 團隊對 Jedis 進行了封裝,獨立為 spring-data-redis 項目,配合 spring 特性併集成 Jedis 的一些命令和方法。本文重點描述集成過程,能讓你迅速的通過 spring-data-redis 將 redis 集成到 spring 項目中,畢竟大家都忙的。1. 添加項目依賴<!
  • 點擊查看:AOP形式如何實現Redis緩存
    的切面類@Component//將類交給spring容器管理public class CacheAOP { //公式=切入點表達式 + 通知方法 /* *關於切入點表達式的使用說明 * 粗粒度: * 1.bean(bean的Id)一個類 bean的Id指的是交給spring容易管理的類的類名小寫 , * 也可以bean(*ServiceImpl) 多個類 * 2.within(包名.類名)
  • Spring極速集成註解redis實踐
    Spring 團隊對 Jedis 進行了封裝,獨立為 spring-data-redis 項目,配合 spring 特性併集成 Jedis 的一些命令和方法。本文重點描述集成過程,能讓你迅速的通過 spring-data-redis 將 redis 集成到 spring 項目中,畢竟大家都忙的。1. 添加項目依賴<!
  • Redis實現Spring緩存
    -- spring自己的緩存管理器,這裡定義了緩存位置名稱 ,即註解中的value --> <bean id="myCacheManager" class="org.springframework.cache.support.SimpleCacheManager"> <property
  • 利用AOP自定義Redis緩存註解
    背景在查詢類開發中我們有使用緩存的場景,一般可以使用Redis作為緩存,來緩解資料庫如MySQL的壓力。使用緩存的步驟為:「(1)從Redis緩存中獲取數據,如果存在數據,直接返回值。那我們何不用註解的形式代替a和c代碼呢。使用SpringBoot的緩存註解SpringBoot提供了現成可用的緩存註解@Cacheble。
  • SpringBoot2.0實戰(23)整合Redis之集成緩存SpringDataCache
    相關知識常用 Spring Cache 緩存註解@Caching 作用在方法上,以上的註解如果需要同時註解多個,可以包在 @Caching 內目標使用 Redis 作為緩存,實現對資料庫進行增刪改時,同步更新數據至緩存,查詢時優先從緩存中查找。
  • Spring框架IOC和AOP簡介
    ,用於訪問 ServletContext 環境下的資源◎InputStreamResource:獲取輸入流封裝的資源◎ByteArrayResource:獲取字節數組封裝的資源(6)Spring註解從Spring3.0開始,Spring JavaConfig項目提供了很多特性,包括使用java而不是XML定義bean,比如:@Configuration
  • Spring Boot 2.x基礎教程:使用集中式緩存Redis
    之前我們介紹了兩種進程內緩存的用法,包括Spring Boot默認使用的ConcurrentMap緩存以及緩存框架EhCache。雖然EhCache已經能夠適用很多應用場景,但是由於EhCache是進程內的緩存框架,在集群模式下時,各應用伺服器之間的緩存都是獨立的,因此在不同伺服器的進程間會存在緩存不一致的情況。
  • 從零搭建Spring Boot腳手架:整合Redis作為緩存
    依賴集成目前只需要引入下面的依賴即可: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency
  • 開源吧,整合Redis作為緩存搭建Spring Boot框架
    依賴集成目前只需要引入下面的依賴即可: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis
  • SpringBoot集成redis與spring-cache
    未月廿三 | 作者urlify.cn/nUn2Aj | 來源spring基於註解的緩存對於緩存聲明,spring的緩存提供了一組java註解:@Cacheable:觸發緩存寫入。最簡單的使用方式,註解名稱=緩存名稱,使用例子如下:@Cacheable(&34;)public Book findBook(ISBN isbn) {...}
  • spring和spring boot常用註解及使用
    @Autowired是spring的註解,是spring2.5版本引入的,@Autowired只根據type進行注入,不會去匹配name。如果涉及到type無法辨別注入對象時,那需要依賴@Qualifier註解一起來修飾。
  • Spring:AOP 面向切面編程
    AOP 相關術語Spring 的 AOP 實現底層就是對上面的動態代理的代碼進行了封裝,封裝後我們只需要對需要關注的部分進行代碼編寫,並通過配置的方式完成指定目標的方法增強。底層代理實現 在 Spring 中,框架會根據目標類是否實現了接口來決定採用哪種動態代理的方式:當 bean 實現接口時,會用 JDK 代理模式當 bean 沒有實現接口,用 cglib 實現( 可以強制使用 cglib(在 spring 配置中加入
  • 如何理解 Spring AOP 以及使用 AspectJ?
    這些術語包括Aspect、Joinpiont、Pointcut、Advice、Target Object、Proxy 和Weaving,對於這些專業術語具體的解釋如下:Aspect,切面在實際的應用中,切面通常是指封裝的用於橫向插入系統功能,比如事務日誌的類,該類被spring容器識別為切面,需要在配置文件中通過\<bean\>來指定。
  • spring框架的入門學習:AOP和面向切面的事務
    <context:component-scan base-package=&34;></context:component-scan>這個配置的就是說spring會去com.huanfeng.bean中掃描所有的註解(包括bean包下面的所有類以及子包下的所有類),那麼我們下面就可以在com.huanfeng.bean下的所有類中使用註解配置文件了
  • Redis整合Spring項目搭建實例
    本文介紹了如何使用註解的方式,將Redis緩存整合到你的Spring項目。首先我們將使用jedis驅動,進而開始配置我們的Gradle。) { gradleVersion = '2.3'}緊接著我們將使用Spring註解,繼續執行Redis裝載配置。
  • spring data redis集成以及session共享
    redis 是一款基於內存存儲的高性能緩存框架,支持多種數據結構如string, hash, list, set、sorted set、bitmap等多種數據類型,也提供了很多高可用功能如rdb和aof備份、哨兵模式和集群模式部署。redis有許多的應用場景,一般用於替代一些資料庫訪問熱點數據操作減少資料庫壓力以及提高性能,也可用於session共享、排行榜等。
  • Spring Boot+Redis輕鬆實現接口冪等性
    (有局限性)使用緩存,基於請求參數、session或者token(用戶信息)防止重複提交(使用redis用作緩存時和分布式鎖原理相同)。分布式鎖 -- redis、zookeeper等。四、本文採用的實現方式本文採用的第二種實現方式,即使用緩存,基於請求參數、session或者token(用戶信息)防止重複提交五、實現思路自定義註解@RepeatLimiting,把@RepeatLimiting註解作為AOP的切點去攔截所有標註@RepeatLimiting
  • Caffeine 和 Redis 居然可以這麼搭,想不到吧!
    Caffeine 來做緩存,那麼肯定會有人問,我用了 redis 已經很快了,為什麼還要結合使用其他的緩存呢,緩存最大的作用確實是提高效率,但是隨著業務需求的發展,業務體量的增大,多級緩存的作用就凸顯了出來,接下來讓我們盯緊了哦!
  • 深入Spring Boot (十一):整合Redis詳解
    整合Redis,可以通過使用spring-boot-starter-data-redis,這種方式下,Spring Boot默認使用的Redis客戶端是Lettuce。Spring Cache從Spring3.1開始,Spring框架提供了對Cache的支持,提供了一個對緩存使用的抽象,通過在既有代碼中添加少量它定義的各種annotation,即能夠達到緩存方法的返回對象的作用。提供的主要註解有@Cacheable、@CachePut、@CacheEvict和@Caching,具體見表1。