2021-05-12 14:32:11
Spring AOP與Redis搭建快取
近期專案查詢資料庫太慢,持久層也沒有開啟二級快取,現希望採用Redis作為快取。為了不改寫原來程式碼,在此採用AOP+Redis實現。
目前由於專案需要,只需要做查詢部分:
資料查詢時每次都需要從資料庫查詢資料,資料庫壓力很大,查詢速度慢,因此設定快取層,查詢資料時先從redis中查詢,如果查詢不到,則到資料庫中查詢,然後將資料庫中查詢的資料放到redis中一份,下次查詢時就能直接從redis中查到,不需要查詢資料庫了。
redis作為快取的優勢:
1.記憶體級別快取,查詢速度毋庸置疑。
2.高效能的K-V儲存系統,支援String,Hash,List,Set,Sorted Set等資料型別,能夠應用在很多場景中。
3.redis3.0版本以上支援叢集部署。
4.redis支援資料的持久化,AOF,RDB方式。
實體類與表:
public class RiskNote implements Serializable { private static final long serialVersionUID = 4758331879028183605L; private Integer ApplId; private Integer allqyorg3monNum; private Double loanF6endAmt; private String isHighRisk1; private Date createDate; private String risk1Detail; private Integer risk2; private String risk3; private String creditpaymonth; ......
Redis與Spring整合引數:
redis.properties
#redis settings redis.minIdle=5 redis.maxIdle=10 redis.maxTotal=50 redis.maxWaitMillis=1500 redis.testOnBorrow=true redis.numTestsPerEvictionRun=1024 redis.timeBetweenEvictionRunsMillis=30000 redis.minEvictableIdleTimeMillis=1800000 redis.softMinEvictableIdleTimeMillis=10000 redis.testWhileIdle=true redis.blockWhenExhausted=false #redisConnectionFactory settings redis.host=192.168.200.128 redis.port=6379
整合組態檔:applicationContext_redis.xml
<!-- 載入設定資料 --> <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="systemPropertiesModeName" value="SYSTEM_PROPERTIES_MODE_OVERRIDE" /> <property name="ignoreResourceNotFound" value="true" /> <property name="locations"> <list> <value>classpath*:/redis.properties</value> </list> </property> </bean> <!-- 註解掃描 --> <context:component-scan base-package="com.club.common.redis"/> <!-- jedis連線池設定 --> <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig"> <!-- 最小空閒連線數 --> <property name="minIdle" value="${redis.minIdle}"/> <!-- 最大空閒連線數 --> <property name="maxIdle" value="${redis.maxIdle}"/> <!-- 最大連線數 --> <property name="maxTotal" value="${redis.maxTotal}"/> <!-- 獲取連線時的最大等待毫秒數,小於零:阻塞不確定的時間,預設-1 --> <property name="maxWaitMillis" value="${redis.maxWaitMillis}"/> <!-- 在獲取連線的時候檢查有效性, 預設false --> <property name="testOnBorrow" value="${redis.testOnBorrow}"/> <!-- 每次釋放連線的最大數目 --> <property name="numTestsPerEvictionRun" value="${redis.numTestsPerEvictionRun}"/> <!-- 釋放連線的掃描間隔(毫秒) --> <property name="timeBetweenEvictionRunsMillis" value="${redis.timeBetweenEvictionRunsMillis}"/> <!-- 連線最小空閒時間 --> <property name="minEvictableIdleTimeMillis" value="${redis.minEvictableIdleTimeMillis}"/> <!-- 連線空閒多久後釋放, 當空閒時間>該值 且 空閒連線>最大空閒連線數 時直接釋放 --> <property name="softMinEvictableIdleTimeMillis" value="${redis.softMinEvictableIdleTimeMillis}"/> <!-- 在空閒時檢查有效性, 預設false --> <property name="testWhileIdle" value="${redis.testWhileIdle}"/> <!-- 連線耗盡時是否阻塞, false報異常,ture阻塞直到超時, 預設true --> <property name="blockWhenExhausted" value="${redis.blockWhenExhausted}"/> </bean> <!-- redis連線池 --> <bean id="jedisPool" class="redis.clients.jedis.JedisPool" destroy-method="close"> <constructor-arg name="poolConfig" ref="poolConfig"/> <constructor-arg name="host" value="${redis.host}"/> <constructor-arg name="port" value="${redis.port}"/> </bean> <bean id="redisCache" class="com.club.common.redis.RedisCache"> <property name="jedisPool" ref="jedisPool"></property> </bean> <bean id="testDao" class="com.club.common.redis.TestDao"></bean> <bean id="testService" class="com.club.common.redis.service.TestService"></bean> <!-- 開啟Aspect切面支援 --> <aop:aspectj-autoproxy/> </beans>
測試,所以各層級沒有寫介面。
DAO層查詢資料,封裝物件:
public class TestDao { //查詢 public RiskNote getByApplId(Integer applId) throws Exception{ Class.forName("Oracle.jdbc.driver.OracleDriver"); Connection connection = DriverManager.getConnection("jdbc:oracle:thin:@192.168.11.215:1521:MFTEST01", "datacenter", "datacenter"); PreparedStatement statement = connection.prepareStatement("select * from TEMP_RISK_NOTE where appl_id=?"); //執行 statement.setInt(1, applId); ResultSet resultSet = statement.executeQuery(); RiskNote riskNote = new RiskNote(); //解析 while (resultSet.next()) { riskNote.setApplId(resultSet.getInt("APPL_ID")); riskNote.setAllqyorg3monNum(resultSet.getInt("ALLQYORG3MON_NUM")); riskNote.setLoanF6endAmt(resultSet.getDouble("LOAN_F6END_AMT")); riskNote.setIsHighRisk1(resultSet.getString("IS_HIGH_RISK_1")); riskNote.setCreateDate(resultSet.getDate("CREATE_DATE")); riskNote.setRisk1Detail(resultSet.getString("RISK1_DETAIL")); riskNote.setRisk2(resultSet.getInt("RISK2")); riskNote.setRisk3(resultSet.getString("RISK3")); riskNote.setCreditpaymonth(resultSet.getString("CREDITPAYMONTH")); } return riskNote; } }
Service層呼叫DAO:
@Service public class TestService { @Autowired private TestDao testDao; public Object get(Integer applId) throws Exception{ RiskNote riskNote = testDao.getByApplId(applId); return riskNote; } }
測試:
public class TestQueryRiskNote { @Test public void testQuery() throws Exception{ ApplicationContext ac = new FileSystemXmlApplicationContext("src/main/resources/spring/applicationContext_redis.xml"); TestService testService = (TestService) ac.getBean("testService"); RiskNote riskNote = (RiskNote)testService.get(91193); System.out.println(riskNote); } }
此時測試程式碼輸出的是查詢到的RiskNote物件,可以重寫toString方法檢視
結果如下:最後輸出的物件
在虛擬機器Linux系統上搭建Redis,具體教學請自行百度
redis支援多種資料結構,查詢的物件可以直接使用hash結構存入redis。
因為專案中各個方法查詢的資料不一致,比如有簡單物件,有List集合,有Map集合,List中套Map套物件等複雜結構,為了實現統一性和通用性,redis中也剛好提供了set(byte[],byte[])方法,所以可以將物件序列化後存入redis,取出後反序列化為物件。
序列化與反序列化工具類:
/** * * @Description: 序列化反序列化工具 */ public class SerializeUtil { /** * * 序列化 */ public static byte[] serialize(Object obj){ ObjectOutputStream oos = null; ByteArrayOutputStream baos = null; try { //序列化 baos = new ByteArrayOutputStream(); oos = new ObjectOutputStream(baos); oos.writeObject(obj); byte[] byteArray = baos.toByteArray(); return byteArray; } catch (IOException e) { e.printStackTrace(); } return null; } /** * * 反序列化 * @param bytes * @return */ public static Object unSerialize(byte[] bytes){ ByteArrayInputStream bais = null; try { //反序列化為物件 bais = new ByteArrayInputStream(bytes); ObjectInputStream ois = new ObjectInputStream(bais); return ois.readObject(); } catch (Exception e) { e.printStackTrace(); } return null; } }
切面分析:
切面:查詢前先查詢redis,如果查詢不到穿透到資料庫,從資料庫查詢到資料後,儲存到redis,然後下次查詢可直接命中快取
目標方法是查詢資料庫,查詢之前需要查詢redis,這是前置
假設從redis中沒有查到,則查詢資料庫,執行完目標方法後,需要將查詢的資料放到redis以便下次查詢時不需要再到資料庫中查,這是後置
所以,可以將切面中的通知定為環繞通知
切麵類編寫如下:
/** * @Description: 切面:查詢前先查詢redis,如果查詢不到穿透到資料庫,從資料庫查詢到資料後,儲存到redis,然後下次查詢可直接命中快取 */ @Component @Aspect public class RedisAspect { @Autowired @Qualifier("redisCache") private RedisCache redisCache; //設定切點:使用xml,在xml中設定 @Pointcut("execution(* com.club.common.redis.service.TestService.get(Java.lang.Integer)) and args(applId)") //測試用,這裡還額外指定了方法名稱,方法引數型別,方法形參等,比較完整的切點表示式
public void myPointCut(){ } @Around("myPointCut()") public Object around(ProceedingJoinPoint joinPoint){ //前置:到redis中查詢快取 System.out.println("呼叫從redis中查詢的方法..."); //先獲取目標方法引數 String applId = null; Object[] args = joinPoint.getArgs(); if (args != null && args.length > 0) { applId = String.valueOf(args[0]); } //redis中key格式: applId String redisKey = applId; //獲取從redis中查詢到的物件 Object objectFromRedis = redisCache.getDataFromRedis(redisKey); //如果查詢到了 if(null != objectFromRedis){ System.out.println("從redis中查詢到了資料...不需要查詢資料庫"); return objectFromRedis; } System.out.println("沒有從redis中查到資料..."); //沒有查到,那麼查詢資料庫 Object object = null; try { object = joinPoint.proceed(); } catch (Throwable e) { e.printStackTrace(); } System.out.println("從資料庫中查詢的資料..."); //後置:將資料庫中查詢的資料放到redis中 System.out.println("呼叫把資料庫查詢的資料儲存到redis中的方法..."); redisCache.setDataToRedis(redisKey, object); //將查詢到的資料返回 return object; } }
從redis中查詢資料,以及將資料庫查詢的資料儲存到redis的方法:
/** * * @Description:Redis快取 */ public class RedisCache { @Resource private JedisPool jedisPool; public JedisPool getJedisPool() { return jedisPool; } public void setJedisPool(JedisPool jedisPool) { this.jedisPool = jedisPool; } //從redis快取中查詢,反序列化 public Object getDataFromRedis(String redisKey){ //查詢 Jedis jedis = jedisPool.getResource(); byte[] result = jedis.get(redisKey.getBytes()); //如果查詢沒有為空 if(null == result){ return null; } //查詢到了,反序列化 return SerializeUtil.unSerialize(result); } //將資料庫中查詢到的資料放入redis public void setDataToRedis(String redisKey, Object obj){ //序列化 byte[] bytes = SerializeUtil.serialize(obj); //存入redis Jedis jedis = jedisPool.getResource(); String success = jedis.set(redisKey.getBytes(), bytes); if("OK".equals(success)){ System.out.println("資料成功儲存到redis..."); } } }
測試1:此時redis中沒有查詢物件的資料
結果是:先到redis中查詢,沒有查到資料,然後代理執行從資料庫中查詢,然後把資料存入到redis中一份,那麼下次查詢就可以直接從redis中查詢了
測試2:此時redis中已經有上一次從資料庫中查詢的資料了
在專案中測試後:效果還是非常明顯的,有一個超級複雜的查詢,格式化之後的sql是688行,每次重新整理頁面都需要重新查詢,耗時10秒左右。
在第一次查詢放到redis之後,從redis中查詢能夠在2秒內得到結果,速度非常快。
上面的是在專案改造前寫的一個Demo,實際專案複雜的多,切點表示式是有兩三個一起組成的,也著重研究了一下切點表示式的寫法
如:
@Pointcut("(execution(* com.club.risk.center.service.impl.*.*(java.lang.String))) || (execution(* com.club.risk.py.service.impl.PyServcieImpl.queryPyReportByApplId(java.lang.String))) || (execution(* com.club.risk.zengxintong.service.Impl.ZXTServiceImpl.queryZxtReportByApplId(..)))")
這是多個切點組合形成使用||連線。
我在實際專案中使用的key也比applId複雜,因為可能只使用applId的話導致key衝突,
所以專案中使用的key是applId:方法全限定名,,這樣的話key能夠保證是一定不一致的。
如下:
//先獲取目標方法引數 String applId = null; Object[] args = joinPoint.getArgs(); if (args != null && args.length > 0) { applId = String.valueOf(args[0]); } //獲取目標方法所在類 String target = joinPoint.getTarget().toString(); String className = target.split("@")[0]; //獲取目標方法的方法名稱 String methodName = joinPoint.getSignature().getName(); //redis中key格式: applId:方法名稱 String redisKey = applId + ":" + className + "." + methodName;
所以上面的是一種通用的處理,具體到專案中還要看具體情況。
以前沒有自己寫過AOP程式碼,這次使用突然發現AOP確實強大,在整個過程中除了組態檔我沒有改任何以前的原始碼,功能全部是切入進去的。
這個Demo也基本上實現了需求,只需要設定切點,能夠將快取應用到各種查詢方法中,或設定切點為service.impl包,直接作用於所有service方法。
Spring AOP四種實現方式 http://www.linuxidc.com/Linux/2016-10/135993.htm
Spring AOP自定義注解方式實現紀錄檔管理 http://www.linuxidc.com/Linux/2015-11/125019.htm
Spring AOP進行紀錄檔記錄 http://www.linuxidc.com/Linux/2015-11/124731.htm
使用Spring AOP進行效能監控 http://www.linuxidc.com/Linux/2012-07/64681.htm
利用Spring AOP 更新Memcached 快取策略的實現 http://www.linuxidc.com/Linux/2012-03/56503.htm
Spring AOP的兩種代理 http://www.linuxidc.com/Linux/2015-11/125017.htm
Spring AOP的註解範例 http://www.linuxidc.com/Linux/2015-11/125018.htm
Spring AOP 簡介以及簡單用法 http://www.linuxidc.com/Linux/2016-12/138188.htm
本文永久更新連結地址:http://www.linuxidc.com/Linux/2016-12/138337.htm
相關文章