SpringAOP與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方法。

