Redis結(jié)合AOP與自定義注解實(shí)現(xiàn)分布式緩存流程詳解
1、背景
項(xiàng)目中如果查詢數(shù)據(jù)是直接到MySQL數(shù)據(jù)庫中查詢的話,會查磁盤走IO,效率會比較低,所以現(xiàn)在一般項(xiàng)目中都會使用緩存,目的就是提高查詢數(shù)據(jù)的速度,將數(shù)據(jù)存入緩存中,也就是內(nèi)存中,這樣查詢效率大大提高
分布式緩存方案
優(yōu)點(diǎn):
- 使用Redis作為共享緩存 ,解決緩存不同步問題
- Redis是獨(dú)立的服務(wù),緩存不用占應(yīng)用本身的內(nèi)存空間
什么樣的數(shù)據(jù)適合放到緩存中呢?
同時(shí)滿足下面兩個(gè)條件的數(shù)據(jù)就適合放緩存:
- 經(jīng)常要查詢的數(shù)據(jù)
- 不經(jīng)常改變的數(shù)據(jù)
接下來我們使用 AOP技術(shù) 來實(shí)現(xiàn)分布式緩存,這樣做的好處是避免重復(fù)代碼,極大減少了工作量
2、目標(biāo)
我們希望分布式緩存能幫我們達(dá)到這樣的目標(biāo):
- 對業(yè)務(wù)代碼無侵入(或侵入性較小)
- 使用起來非常方便,最好是打一個(gè)注解就可以了,可插拔式的
- 對性能影響盡可能的小
- 要便于后期維護(hù)
3、方案
此處我們選擇的方案就是:AOP+自定義注解+Redis
- 自定義一個(gè)注解,需要做緩存的接口打上這個(gè)注解即可
- 使用Spring AOP的環(huán)繞通知增強(qiáng)被自定義注解修飾的方法,把緩存的存儲和刪除都放這里統(tǒng)一處理
- 那么需要用到分布式鎖的接口,只需要打一個(gè)注解即可,這樣才夠靈活優(yōu)雅
4、實(shí)戰(zhàn)編碼
4.1、環(huán)境準(zhǔn)備
首先我們需要一個(gè)簡單的SpringBoot項(xiàng)目環(huán)境,這里我寫了一個(gè)基礎(chǔ)Demo版本,地址如下:
https://gitee.com/colinWu_java/spring-boot-base.git
大家可以先下載下來,本文就是基于這份主干代碼進(jìn)行修改的
4.2、pom依賴
pom.xml中需要新增以下依賴:
<!-- aop --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!--redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--jackson--> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.10.5.1</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.11.1</version> </dependency>
4.3、自定義注解
添加緩存的注解
package org.wujiangbo.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @desc 自定義注解:向緩存中添加數(shù)據(jù) */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface MyCache { String cacheNames() default ""; String key() default ""; //緩存時(shí)間(單位:秒,默認(rèn)是無限期) int time() default -1; }
刪除緩存注解:
package org.wujiangbo.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @desc 自定義注解:從緩存中刪除數(shù)據(jù) */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface MyCacheEvict { String cacheNames() default ""; String key() default ""; }
4.4、切面處理類
下面兩個(gè)切面類實(shí)際上是可以寫在一個(gè)類中的,但是為了方便理解和觀看,我分開寫了
package org.wujiangbo.aop; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; import org.wujiangbo.annotation.MyCache; import org.wujiangbo.service.RedisService; import javax.annotation.Resource; import java.util.concurrent.TimeUnit; /** * @desc 切面類,處理分布式緩存添加功能 */ @Aspect @Component @Slf4j public class MyCacheAop { @Resource private RedisService redisService; /** * 定義切點(diǎn) */ @Pointcut("@annotation(myCache)") public void pointCut(MyCache myCache){ } /** * 環(huán)繞通知 */ @Around("pointCut(myCache)") public Object around(ProceedingJoinPoint joinPoint, MyCache myCache) { String cacheNames = myCache.cacheNames(); String key = myCache.key(); int time = myCache.time(); /** * 思路: * 1、拼裝redis中存緩存的key值 * 2、看redis中是否存在該key * 3、如果存在,直接取出來返回即可,不需要執(zhí)行目標(biāo)方法了 * 4、如果不存在,就執(zhí)行目標(biāo)方法,然后將緩存放一份到redis中 */ String redisKey = new StringBuilder(cacheNames).append(":").append(key).toString(); String methodPath = joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName(); Object result ; if (redisService.exists(redisKey)){ log.info("訪問接口:[{}],直接從緩存獲取數(shù)據(jù)", methodPath); return redisService.getCacheObject(redisKey); } try { //執(zhí)行接口 result = joinPoint.proceed(); //接口返回結(jié)果存Redis redisService.setCacheObject(redisKey, result, time, TimeUnit.SECONDS); log.info("訪問接口:[{}],返回值存入緩存成功", methodPath); } catch (Throwable e) { log.error("發(fā)生異常:{}", e); throw new RuntimeException(e); } return result; } }
還有一個(gè):
package org.wujiangbo.aop; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; import org.wujiangbo.annotation.MyCacheEvict; import org.wujiangbo.service.RedisService; import javax.annotation.Resource; /** * @desc 切面類,處理分布式緩存刪除功能 */ @Aspect @Component @Slf4j public class MyCacheEvictAop { @Resource private RedisService redisService; /** * 定義切點(diǎn) */ @Pointcut("@annotation(myCache)") public void pointCut(MyCacheEvict myCache){ } /** * 環(huán)繞通知 */ @Around("pointCut(myCache)") public Object around(ProceedingJoinPoint joinPoint, MyCacheEvict myCache) { String cacheNames = myCache.cacheNames(); String key = myCache.key(); /** * 思路: * 1、拼裝redis中存緩存的key值 * 2、刪除緩存 * 3、執(zhí)行目標(biāo)接口業(yè)務(wù)代碼 * 4、再刪除緩存 */ String redisKey = new StringBuilder(cacheNames).append(":").append(key).toString(); String methodPath = joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName(); Object result ; //刪除緩存 redisService.deleteObject(redisKey); try { //執(zhí)行接口 result = joinPoint.proceed(); //刪除緩存 redisService.deleteObject(redisKey); log.info("訪問接口:[{}],緩存刪除成功", methodPath); } catch (Throwable e) { log.error("發(fā)生異常:{}", e); throw new RuntimeException(e); } return result; } }
4.5、工具類
Redis的工具類:
package org.wujiangbo.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.BoundSetOperations; import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Component; import java.util.*; import java.util.concurrent.TimeUnit; /** * @desc Redis工具類 */ @Component //交給Spring來管理 的自定義組件 public class RedisService { @Autowired public RedisTemplate redisTemplate; /** * 查看key是否存在 */ public boolean exists(String key) { return redisTemplate.hasKey(key); } /** * 清空Redis所有緩存數(shù)據(jù) */ public void clearAllRedisData() { Set<String> keys = redisTemplate.keys("*"); redisTemplate.delete(keys); } /** * 緩存基本的對象,Integer、String、實(shí)體類等 * * @param key 緩存的鍵值 * @param value 緩存的值 */ public <T> void setCacheObject(final String key, final T value) { redisTemplate.opsForValue().set(key, value); } /** * 緩存基本的對象,Integer、String、實(shí)體類等 * * @param key 緩存的鍵值 * @param value 緩存的值 * @param timeout 時(shí)間 * @param timeUnit 時(shí)間顆粒度 */ public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) { if(timeout == -1){ //永久有效 redisTemplate.opsForValue().set(key, value); } else{ redisTemplate.opsForValue().set(key, value, timeout, timeUnit); } } /** * 設(shè)置有效時(shí)間 * * @param key Redis鍵 * @param timeout 超時(shí)時(shí)間 * @return true=設(shè)置成功;false=設(shè)置失敗 */ public boolean expire(final String key, final long timeout) { return expire(key, timeout, TimeUnit.SECONDS); } /** * 設(shè)置有效時(shí)間 * * @param key Redis鍵 * @param timeout 超時(shí)時(shí)間 * @param unit 時(shí)間單位 * @return true=設(shè)置成功;false=設(shè)置失敗 */ public boolean expire(final String key, final long timeout, final TimeUnit unit) { return redisTemplate.expire(key, timeout, unit); } /** * 獲得緩存的基本對象。 * * @param key 緩存鍵值 * @return 緩存鍵值對應(yīng)的數(shù)據(jù) */ public <T> T getCacheObject(final String key) { ValueOperations<String, T> operation = redisTemplate.opsForValue(); return operation.get(key); } /** * 刪除單個(gè)對象 * * @param key */ public boolean deleteObject(final String key) { if(exists(key)){ redisTemplate.delete(key); } return true; } /** * 刪除集合對象 * * @param collection 多個(gè)對象 * @return */ public long deleteObject(final Collection collection) { return redisTemplate.delete(collection); } /** * 緩存List數(shù)據(jù) * * @param key 緩存的鍵值 * @param dataList 待緩存的List數(shù)據(jù) * @return 緩存的對象 */ public <T> long setCacheList(final String key, final List<T> dataList) { Long count = redisTemplate.opsForList().rightPushAll(key, dataList); return count == null ? 0 : count; } /** * 獲得緩存的list對象 * * @param key 緩存的鍵值 * @return 緩存鍵值對應(yīng)的數(shù)據(jù) */ public <T> List<T> getCacheList(final String key) { return redisTemplate.opsForList().range(key, 0, -1); } /** * 緩存Set * * @param key 緩存鍵值 * @param dataSet 緩存的數(shù)據(jù) * @return 緩存數(shù)據(jù)的對象 */ public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) { BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key); Iterator<T> it = dataSet.iterator(); while (it.hasNext()) { setOperation.add(it.next()); } return setOperation; } /** * 獲得緩存的set * * @param key * @return */ public <T> Set<T> getCacheSet(final String key) { return redisTemplate.opsForSet().members(key); } /** * 緩存Map * * @param key * @param dataMap */ public <T> void setCacheMap(final String key, final Map<String, T> dataMap) { if (dataMap != null) { redisTemplate.opsForHash().putAll(key, dataMap); } } /** * 獲得緩存的Map * * @param key * @return */ public <T> Map<String, T> getCacheMap(final String key) { return redisTemplate.opsForHash().entries(key); } /** * 往Hash中存入數(shù)據(jù) * * @param key Redis鍵 * @param hKey Hash鍵 * @param value 值 */ public <T> void setCacheMapValue(final String key, final String hKey, final T value) { redisTemplate.opsForHash().put(key, hKey, value); } /** * 獲取Hash中的數(shù)據(jù) * * @param key Redis鍵 * @param hKey Hash鍵 * @return Hash中的對象 */ public <T> T getCacheMapValue(final String key, final String hKey) { HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash(); return opsForHash.get(key, hKey); } /** * 獲取多個(gè)Hash中的數(shù)據(jù) * * @param key Redis鍵 * @param hKeys Hash鍵集合 * @return Hash對象集合 */ public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) { return redisTemplate.opsForHash().multiGet(key, hKeys); } /** * 獲得緩存的基本對象列表 * * @param pattern 字符串前綴 * @return 對象列表 */ public Collection<String> keys(final String pattern) { return redisTemplate.keys(pattern); } }
4.6、配置類
package org.wujiangbo.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.serializer.StringRedisSerializer; import javax.annotation.Resource; /** * @desc redis配置類 */ @Configuration public class RedisSerializableConfig extends CachingConfigurerSupport { @Resource private RedisConnectionFactory factory; @Bean public RedisTemplate<Object, Object> redisTemplate() { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class); ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); serializer.setObjectMapper(mapper); // 使用StringRedisSerializer來序列化和反序列化redis的key值 template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); // Hash的key也采用StringRedisSerializer的序列化方式 template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; } @Bean public DefaultRedisScript<Long> limitScript() { DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(); redisScript.setScriptText(limitScriptText()); redisScript.setResultType(Long.class); return redisScript; } /** * 限流腳本 */ private String limitScriptText() { return "local key = KEYS[1]\n" + "local count = tonumber(ARGV[1])\n" + "local time = tonumber(ARGV[2])\n" + "local current = redis.call('get', key);\n" + "if current and tonumber(current) > count then\n" + " return tonumber(current);\n" + "end\n" + "current = redis.call('incr', key)\n" + "if tonumber(current) == 1 then\n" + " redis.call('expire', key, time)\n" + "end\n" + "return tonumber(current);"; } }
FastJson2JsonRedisSerializer類:
package org.wujiangbo.config; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.ParserConfig; import com.alibaba.fastjson.serializer.SerializerFeature; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.type.TypeFactory; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; import org.springframework.util.Assert; import java.nio.charset.Charset; /** * @desc Redis使用FastJson序列化 */ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> { @SuppressWarnings("unused") private ObjectMapper objectMapper = new ObjectMapper(); public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); private Class<T> clazz; static { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); } public FastJson2JsonRedisSerializer(Class<T> clazz) { super(); this.clazz = clazz; } @Override public byte[] serialize(T t) throws SerializationException { if (t == null) { return new byte[0]; } return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET); } @Override public T deserialize(byte[] bytes) throws SerializationException { if (bytes == null || bytes.length <= 0) { return null; } String str = new String(bytes, DEFAULT_CHARSET); return JSON.parseObject(str, clazz); } public void setObjectMapper(ObjectMapper objectMapper) { Assert.notNull(objectMapper, "'objectMapper' must not be null"); this.objectMapper = objectMapper; } protected JavaType getJavaType(Class<?> clazz) { return TypeFactory.defaultInstance().constructType(clazz); } }
4.7、yml配置
server:
port: 8001
undertow:
# 設(shè)置IO線程數(shù), 它主要執(zhí)行非阻塞的任務(wù),它們會負(fù)責(zé)多個(gè)連接, 默認(rèn)設(shè)置每個(gè)CPU核心一個(gè)線程
# 不要設(shè)置過大,如果過大,啟動項(xiàng)目會報(bào)錯(cuò):打開文件數(shù)過多(CPU有幾核,就填寫幾)
io-threads: 6
# 阻塞任務(wù)線程池, 當(dāng)執(zhí)行類似servlet請求阻塞IO操作, undertow會從這個(gè)線程池中取得線程
# 它的值設(shè)置取決于系統(tǒng)線程執(zhí)行任務(wù)的阻塞系數(shù),默認(rèn)值是:io-threads * 8
worker-threads: 48
# 以下的配置會影響buffer,這些buffer會用于服務(wù)器連接的IO操作,有點(diǎn)類似netty的池化內(nèi)存管理
# 每塊buffer的空間大小,越小的空間被利用越充分,不要設(shè)置太大,以免影響其他應(yīng)用,合適即可
buffer-size: 1024
# 每個(gè)區(qū)分配的buffer數(shù)量 , 所以pool的大小是buffer-size * buffers-per-region
buffers-per-region: 1024
# 是否分配的直接內(nèi)存(NIO直接分配的堆外內(nèi)存)
direct-buffers: true
spring:
#配置數(shù)據(jù)庫鏈接信息
datasource:
url: jdbc:mysql://127.0.0.1:3306/test1?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&rewriteBatchedStatements=true
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
application:
name: springboot #服務(wù)名
#redis配置
redis:
# 數(shù)據(jù)庫索引
database: 0
# 地址
host: 127.0.0.1
# 端口,默認(rèn)為6379
port: 6379
# 密碼
password: 123456
# 連接超時(shí)時(shí)間
timeout: 10000#MyBatis-Plus相關(guān)配置
mybatis-plus:
#指定Mapper.xml路徑,如果與Mapper路徑相同的話,可省略
mapper-locations: classpath:org/wujiangbo/mapper/*Mapper.xml
configuration:
map-underscore-to-camel-case: true #開啟駝峰大小寫自動轉(zhuǎn)換
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #開啟控制臺sql輸出
4.8、使用
Controller中寫兩個(gè)接口分別測試一下緩存的新增和刪除
package org.wujiangbo.controller; import lombok.extern.slf4j.Slf4j; import org.wujiangbo.annotation.CheckPermission; import org.wujiangbo.annotation.MyCache; import org.wujiangbo.annotation.MyCacheEvict; import org.wujiangbo.result.JSONResult; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * @desc 測試接口類 */ @RestController @Slf4j public class TestController { //測試刪除緩存 @GetMapping("/deleteCache") @MyCacheEvict(cacheNames = "cacheTest", key = "userData") public JSONResult deleteCache(){ System.out.println("deleteCache success"); return JSONResult.success("deleteCache success"); } //測試新增緩存 @GetMapping("/addCache") @MyCache(cacheNames = "cacheTest", key = "userData") public JSONResult addCache(){ System.out.println("addCache success"); return JSONResult.success("addCache success"); } }
4.9、測試
瀏覽器先訪問:http://localhost:8001/addCache
然后再通過工具查看Redis中是不是添加了緩存數(shù)據(jù),正確情況應(yīng)該是緩存添加進(jìn)去了
然后再訪問:http://localhost:8001/deleteCache
再通過工具查看Redis,緩存應(yīng)該是被刪除了,沒有了
到此完全符合預(yù)期,測試成功
總結(jié)
本文主要是介紹了分布式緩存利用AOP+注解的方式處理,方便使用和擴(kuò)展希望對大家有所幫助
最后本案例代碼已全部提交到gitee中了,地址如下:
https://gitee.com/colinWu_java/spring-boot-base.git
本文新增的代碼在【RedisDistributedCache】分支中
到此這篇關(guān)于Redis結(jié)合AOP與自定義注解實(shí)現(xiàn)分布式緩存流程詳解的文章就介紹到這了,更多相關(guān)Redis分布式緩存內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java基于JDBC實(shí)現(xiàn)事務(wù),銀行轉(zhuǎn)賬及貨物進(jìn)出庫功能示例
這篇文章主要介紹了Java基于JDBC實(shí)現(xiàn)事務(wù),銀行轉(zhuǎn)賬及貨物進(jìn)出庫功能,較為詳細(xì)的分析了事務(wù)操作的原理、實(shí)現(xiàn)方法及java基于jdbc連接數(shù)據(jù)庫實(shí)現(xiàn)銀行事務(wù)操作的相關(guān)技巧,需要的朋友可以參考下2017-12-12spring-core組件詳解——PropertyResolver屬性解決器
這篇文章主要介紹了spring-core組件詳解——PropertyResolver屬性解決器,需要的朋友可以參考下2016-05-05淺析Java中靜態(tài)代理和動態(tài)代理的應(yīng)用與區(qū)別
代理模式在我們生活中很常見,而Java中常用的兩個(gè)的代理模式就是動態(tài)代理與靜態(tài)代理,這篇文章主要為大家介紹了二者的應(yīng)用與區(qū)別,需要的可以參考下2023-08-08Java使用wait/notify實(shí)現(xiàn)線程間通信上篇
wait()和notify()是直接隸屬于Object類,也就是說所有對象都擁有這一對方法,下面這篇文章主要給大家介紹了關(guān)于使用wait/notify實(shí)現(xiàn)線程間通信的相關(guān)資料,需要的朋友可以參考下2022-12-12Java NIO:淺析IO模型_動力節(jié)點(diǎn)Java學(xué)院整理
在進(jìn)入Java NIO編程之前,我們今天先來討論一些比較基礎(chǔ)的知識:I/O模型。對java io nio相關(guān)知識感興趣的朋友一起學(xué)習(xí)吧2017-05-05