SpringAOP與Redis搭建緩存

来源:http://www.cnblogs.com/mrlinfeng/archive/2016/09/13/5857775.html
-Advertisement-
Play Games

近期項目查詢資料庫太慢,持久層也沒有開啟二級緩存,現希望採用Redis作為緩存。為了不改寫原來代碼,在此採用AOP+Redis實現。 目前由於項目需要,只需要做查詢部分: 數據查詢時每次都需要從資料庫查詢數據,資料庫壓力很大,查詢速度慢,因此設置緩存層,查詢數據時先從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方法。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • NRF24L01 是一款工作在2.4-2.5GHz通用ISM頻段的單片收發晶元 工作電壓:1.9~3.6V低電壓工作 高速率:2Mbps,由於空中傳輸時間很短,極大的降低了無線傳輸中的碰撞現象 多頻點:125 頻點,滿足多點通信和跳頻通信需要 超小型:內置2.4GHz天線,體積小巧,15x29mm( ...
  • 由於各Linux開發廠商的不同,因此不同開發廠商的Linux版本操作細節也不一樣,今天就來說一下CentOS下JDK的安裝: 方法一:手動解壓JDK的壓縮包,然後設置環境變數 1.在/usr/目錄下創建java目錄 2.下載jdk,然後解壓 3.設置環境變數 在profile中添加如下內容: 讓修改 ...
  • 系統 # uname -a # 查看內核/操作系統/CPU信息# head -n 1 /etc/issue # 查看操作系統版本# cat /proc/cpuinfo # 查看CPU信息# hostname # 查看電腦名# lspci -tv # 列出所有PCI設備# lsusb -tv # 列 ...
  • 1、ifconfig命令找不到 解決方法:安裝net-tools.x86_64工具包 yum install net-tools.x86_64 2、iptables無法使用 解決方法:yum install iptables-services vi /etc/sysconfig/iptables # ...
  • 一、安裝GCC編譯環境,如果有則不需要 1)安裝mpc庫 2)安裝gmp庫 3)安裝mpfr庫 4) 安裝GCC 以上GCC的安裝不綴述,可參考各種大神步驟; 二、安裝pcre庫 https://sourceforge.net/projects/pcre/files/pcre/ 下載後解壓併在解壓文 ...
  • 這種情況一般是由於系統防火牆設置問題導致的,這次遇到的系統是centos 7.2,防火牆由iptables改成了firewall,因此停止防火牆的命令應該是: 禁止防火牆啟動的命令應該是: 暫時只寫這麼多,關於防火牆的配置等以後有時間了再說. ...
  • 繼續解決mplayer安裝不上的問題: 多次嘗試後,把源換回官方然後 update&upgrade後安裝 問題解決 時區問題解決: 裡面的第五項 Internationalisation Options --> Change Timezone --> Asia --> Chongqing(找了半天沒 ...
  • 之前裝的是live版 就是沒有桌面的版本,想看能hdmi看電影,於是找了教程安裝omxplayer 用 命令 通過hdmi播放電影 具體安裝過程發在貼吧里了:http://tieba.baidu.com/p/4766986525?see_lz=1 但是依然不能掛字幕.... 無奈今天重裝rasbia ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...