1. 程式人生 > >spring boot使用Jedis整合Redis實現快取(AOP)

spring boot使用Jedis整合Redis實現快取(AOP)

一:環境準備

1:準備Redis環境

使用redis做快取的話,需要有redis服務,可以將服務部署在遠端伺服器上,也可以部署到本機上。

1.1. 部署在linux伺服器

1.1.1安裝Redis

#安裝redis,當前最新的版本是redis-5.0.0.tar.gz,可以通過http://download.redis.io/releases地址檢視最新版本
$ wget http://download.redis.io/releases/redis-5.0.0.tar.gz
$ tar xzf redis-5.0.0.tar.gz
$ cd redis-5.0.0
$ make

1.1.2啟動Redis服務並使用

#啟動redis服務
$ cd src
$ ./redis-server
#使用redis客戶端測試redis
$ cd src
$ ./redis-cli
redis> set testkey  testvalue
OK
redis> get testkey
"testvalue"

如果上述過程沒有報錯的話,那麼恭喜你啟動redis服務成功,下面我們將會使用jedis操作redis來實現快取

1.2. 部署在windows伺服器

2.2啟動redis服務並使用

開啟cmd,切換到解壓的Redis資料夾中,執行如下命令,
會發現出現”The server is now ready to accept connections on port 6379“字樣表示啟動成功

redis-server.exe redis.windows.conf

再開啟一個cmd,原來的cmd不要關閉,保持開啟狀態,輸入以下命令:
其中:127.0.0.1:為你的redis服務ip地址,如果是本機安裝的就是127.0.0.1,埠6379是redis預設監聽的埠

redis-cli.exe -h 127.0.0.1 -p 6379
#如果redis設定了密碼,可以新增引數-a指定密碼,例如:
redis-cli.exe -h 127.0.0.1 -p 6379 -a 12345

可以使用redis命令測試是否可以正常使用,至此redis服務便準備完畢了~

2:準備專案環境

  1. 首先spring boot專案,當然不是boot專案也可以,我是以boot專案舉例的
  2. pom檔案新增依賴,只列出了此功能設計特殊所需的
		<!--jedis依賴-->
		<dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>
        <!--用於序列化 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
  1. application.yml新增配置,如果你是xml格式的檔案,yml格式和xml格式類似,只是yml格式更加明瞭一些,google一下轉換一下格式就行
spring: 
  jedis:
    max:
      total: 1000 #jedis總量
      idle: 50 #空閒jedis例項最大值
      waitmillis: 100 #等待時間
      timout: 100
      active: 1000  # 最大活躍數量jedis例項
    host: 127.0.0.1  # redis服務ip地址
    port: 6379   # 埠
    password: test  # redis密碼

至此,環境配置完成了,現在只需要操作redis實現快取了~~

二:快取功能實現

1:過程簡介

  1. 對於不加快取的專案,我們每一次的請求都會去資料庫中查詢,即使兩次請求一樣並且獲取的資料一樣,也是會去查詢資料庫,這就造成了資料庫資源的浪費,並且如果併發量特別高的話,資料庫的壓力太大,容易造成查詢緩慢、資料庫宕機、查詢失敗等問題。
  2. 專案新增快取之後,請求查詢資料的時候會先查詢快取,快取(這裡指只有一級快取)中沒有才會到達資料庫。相同的請求在快取還沒有過期 的情況下,會得到快取中的資料並返回,不會到達資料庫,這樣做即減少了資料庫的壓力提高了併發量又提升了查詢速度。
  3. 流程圖:
資料不在快取中資料在快取中請求查詢快取查詢資料庫獲得快取中資料返回資料將獲得資料快取到快取中

2:程式碼實現與介紹

2.1.執行過程

  1. 請求到達Controller中的介面時,因為我們在CacheAspect類中配置的切入點包含這個介面,所以進入CacheAspect類的doAround方法中執行快取操作
  2. 在doAround中,首先獲取key,判斷redis中是否包含key,包含就返回快取中的資料,完成請求
  3. 不包含就執行呼叫的介面通過查詢資料庫獲取資料,並將其快取到redis中,完成一次請求不包含就執行呼叫的介面通過查詢資料庫獲取資料,並將其快取到redis中,完成請求

2.2. 組成部分與實現

  • 自定義註解:NeedCacheAop

用在方法上面標識呼叫該方法的請求需要被快取
其中的nxxx、expx、time等引數是為了可以更靈活的空值快取的方式與過期時間,具體含義請看下面”其他“中的set方法引數解析

/**
* 自定義註解,用於標識方法是否需要使用快取
*/
@Target({ElementType.PARAMETER, ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NeedCacheAop {
   //代表快取策咯,nx:代表key不存在再進行快取kv,xx:代表key存在再進行快取kv  預設為"不存在key快取key"
   String nxxx() default "nx";
   //代表過期時間單位,ex:秒 px:毫秒    預設為"秒"
   String expx() default "ex";
   //過期時間
   long time() default 30*60;
}
  • 序列化工具類:SerializeUtil

使用FastJso對要快取的資料進行序列化後儲存與獲取快取中的反序列化
使用fastjson對資料進行序列化與反序列化,非常簡單

public class SerializeUtil {
    private  static Logger logger = LoggerFactory.getLogger("SerializeUtil");
    public static String serializeObject(Object obj){
        logger.info("serialize object :"+obj);
        String jsonObj = JSON.toJSONString(obj);
        return jsonObj;
    }
    public static JSONObject unserializeObject(String serobj){
        logger.info("unserialize object :"+serobj);
        JSONObject jsonObj = JSON.parseObject(serobj);
        return jsonObj;
    }
}
  • 操作快取service類:CacheService介面 與其實現類 CacheServiceImpl

方法內部封裝了關於快取的get set containKey getKeyAop等方法

public interface CacheService {
    /**獲取jedis例項*/
    Jedis getResource() throws Exception;
    /**設定key與value*/
    void set(String key, String value, String nxxx, String expx, long time);
    /**根據key獲取value*/
    String get(String key);
    /**判斷是否存在key*/
    boolean containKey(String key);
    /**釋放jedis例項資源*/
    void returnResource(Jedis jedis);
    /**獲取key*/
    String getKeyForAop(JoinPoint joinPoint, HttpServletRequest request);
}
@Service
public class CacheServiceImpl implements CacheService {
    private static Logger logger = LoggerFactory.getLogger(CacheServiceImpl.class);

    @Autowired
    private JedisPool jedisPool;

    /**獲取jedis例項*/
    public Jedis getResource() throws Exception{
        return jedisPool.getResource();
    }

    /**設定key與value*/
    public void set(String key, String value,String nxxx,String expx,long time) {
        Jedis jedis=null;
        try{
            jedis = getResource();
            jedis.set(key,value,nxxx,expx,time);
        } catch (Exception e) {
            logger.error("Redis set error: "+ e.getMessage() +" - " + key + ", value:" + value);
        }finally{
            returnResource(jedis);
        }
    }

    /**根據key獲取value*/
    public String get(String key) {
        String result = null;
        Jedis jedis=null;
        try{
            jedis = getResource();
            result = jedis.get(key);
        } catch (Exception e) {
            logger.error("Redis set error: "+ e.getMessage() +" - " + key + ", value:" + result);
        }finally{
            returnResource(jedis);
        }
        return result;
    }

    /**判斷是否存在key*/
    public boolean containKey(String key){
        boolean b;
        Jedis jedis = null;
        try{
            jedis = getResource();
            b = jedis.exists(key);
            return b;
        }catch (Exception e){
            logger.error("Redis server error::"+e.getMessage());
            return false;
        }finally {
            returnResource(jedis);
        }
    }

    /**釋放jedis例項資源*/
    public void returnResource(Jedis jedis) {
        if(jedis != null){
            jedis.close();
        }
    }

    /**獲取key*/
    public String getKeyForAop(JoinPoint joinPoint, HttpServletRequest request){
        //獲取引數的序列化
        Object[] objects = joinPoint.getArgs();
        String args = SerializeUtil.serializeObject(objects[0]);
        //獲取請求url
        String url = request.getRequestURI();
        //獲取請求的方法
        String method = request.getMethod();
        //獲取當前日期,規避預設init情況
        String date = LocalDate.now().toString();
        //key值獲取
        return args + url + method + date;
    }
}
  • 切面類:CacheAspect

用於對相應的請求介面切入快取存取的相關邏輯,使用AOP可以對程式碼0侵入性,是一個很好的方法

@Component
@Aspect
public class CacheAspect {

    @Autowired
    CacheService cacheService;

    /**設定切入點*/
    //方法上面有@NeedCacheAop的方法,增加靈活性
    @Pointcut("@annotation(com.xcar.data.web.backend.util.annotation.NeedCacheAop)")
    public void annotationAspect(){}
    //相應包下所有以XcarIndex開頭的類中的所有方法,減少程式碼侵入性
    @Pointcut("execution(public * com.xcar.data.web.backend.controller.XcarIndex*.*(..))")
    public void controllerAspect(){}

    /**環繞通知*/
    @Around(value = "controllerAspect()||annotationAspect()")
    public Object doAround(ProceedingJoinPoint joinPoint){
        //獲取請求
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes()).getRequest();
        //儲存介面返回值
        Object object = new Object();

        //獲取註解對應配置過期時間
        NeedCacheAop cacheAop = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(NeedCacheAop.class);  //獲取註解自身
        String nxxx;String expx;long time;
        if (cacheAop == null){//規避使用第二種切點進行快取操作的情況
            nxxx = "nx";
            expx = "ex";
            time = 30*60;  //預設過期時間為30分鐘
        }else{
            nxxx = cacheAop.nxxx();
            expx = cacheAop.expx();
            time = cacheAop.time();
        }
        //獲取key
        String key = cacheService.getKeyForAop(joinPoint,request);
        if (cacheService.containKey(key)){
            String obj = cacheService.get(key);
            if ("fail".endsWith(obj)){  //規避redis服務不可用
                try {
                	//執行介面呼叫的方法
                    joinPoint.proceed();
                } catch (Throwable throwable) {
                    throwable.printStackTrace();
                }
            }else{
                JSONObject klass =  SerializeUtil.unserializeObject(obj);
                return new ResponseEntity<>(klass.get("body"), HttpStatus.OK) ;
            }
        }else{
            try {
            	////執行介面呼叫的方法並獲取返回值
                object = joinPoint.proceed();
                String serobj = SerializeUtil.serializeObject(object);
                cacheService.set(key,serobj,nxxx,expx,time);
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        }
        return object;
    }
}
  • jedis配置類:JedisConfiguration

用於配置JedisPool的相關引數,與建立JedisPool物件,便於後面注入使用

@Configuration
public class JedisConfiguration extends CachingConfigurerSupport {
    private Logger logger = LoggerFactory.getLogger(JedisConfiguration.class);
    @Value("${spring.jedis.port}")
    private Integer port;
    @Value("${spring.jedis.host}")
    private String host;
    @Value("${spring.jedis.max.total}")
    private Integer maxTotal;
    @Value("${spring.jedis.max.idle}")
    private Integer maxIdle;
    @Value("${spring.jedis.max.waitmillis}")
    private Long maxWaitMillis;
    @Value("${spring.jedis.password}")
    private String password;

    public JedisConfiguration() {}
    /**設定*/
    @Bean
    public JedisPool redisPoolFactory(){
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(maxIdle);
        jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
        jedisPoolConfig.setMaxTotal(maxTotal);
        JedisPool jedisPool = new JedisPool(jedisPoolConfig,host,port,1000,password);
        logger.info("JedisPool build success!");
        logger.info("Redis host:" + host + ":" + port);
        return  jedisPool;
    }
    //下面屬性是get set方法省略
 }
  • 請求資料物件

請求資料封裝的物件,用於組成key,這個物件應該

  • 響應資料物件

響應的資料物件,快取就是對其進行序列化後快取
該物件類一定繼承Serializable介面,使其可被序列化,例如:

public class XcarIndexCarAttentionIndexResponse implements Serializable{
    priate List<BaseChartsResponse.Line> lines = new ArrayList<>();
    private Series_DateBubble series_datebubble = new Series_DateBubble();
    private String flag = "1";
    public Series_DateBubble getSeries_datebubble() {
        if (series_datebubble == null) {
            series_datebubble = new Series_DateBubble();
        }
        return series_datebubble;
    }
    public String getFlag() {
        return flag;
    }
    public void setFlag(String flag) {
        this.flag = flag;
    }
    public void setSeries_datebubble(Series_DateBubble series_datebubble) {
        this.series_datebubble = series_datebubble;
    }
    public List<BaseChartsResponse.Line> getLines() {
        return lines;
    }
    public void setLines(List<BaseChartsResponse.Line> lines) {
        this.lines = lines;
    }
    public class Series_DateBubble {
        private List<BaseChartsResponse.Series_DateBubble> datas = new ArrayList<>();
        private String[] dataRange = {};
        public List<BaseChartsResponse.Series_DateBubble> getDatas() {
            return datas;
        }
        public void setDatas(List<BaseChartsResponse.Series_DateBubble> datas) {
            this.datas = datas;
        }
        public String[] getDataRange() {
            return dataRange;
        }
        public void setDataRange(String[] dataRange) {
            this.dataRange = dataRange;
        }
    }
}
  • 其他:

我們要新增快取的Controller介面的實現,例如:我要切入的介面

package com.xcar.data.web.backend.controller;
.....
	@RequestMapping(value = "/page/trend", method = RequestMethod.POST)
    public ResponseEntity<XcarIndexCarIntentionIndexResponse> getTrendPage(@RequestBody XcarIndexCarIntentionIndexRequest ro, HttpServletRequest request) throws Exception {
        XcarIndexCarIntentionIndexResponse res = new XcarIndexCarIntentionIndexResponse();
        try {
            res = delegate.getTrendPage(ro);
        } catch (Exception e) {
            throw e;
        }
        return new ResponseEntity(res, HttpStatus.OK);
    }

三:其他相關

3.1.jedis中set方法引數:

  • key :快取的key值
  • value :快取的value值
  • nxxx: NX|XX兩種選擇, NX – 快取不存在時才進行快取. XX – 快取存在時再進行快取
  • expx :EX|PX兩種選擇, 過期時間的代為,EX 代表秒; PX 代表毫秒
  • time :過期時間的數值

3.2.AOP面向切面程式設計

AOP(Aspect Oriented Programing):面向切面程式設計,將通用的邏輯從業務邏輯中分離出來。AOP把軟體系統分為兩個部分:核心關注點和橫切關注點。業務處理的主要流程是核心關注點,與之關係不大的部分是橫切關注點。橫切關注點的一個特點是,他們經常發生在核心關注點的多處,而各處都基本相似。比如許可權認證、日誌、事務處理。Aop 的作用在於分離系統中的各種關注點,將核心關注點和橫切關注點分離開來。正如Avanade公司的高階方案構架師Adam Magee所說,AOP的核心思想就是“將應用程式中的商業邏輯同對其提供支援的通用服務進行分離”。
相關概念:

  • 連線點(Joinpoint): 表示需要在程式中插入橫切關注點的擴充套件點,連線點可能是類初始化、方法執行、方法呼叫、欄位呼叫或處理異常等等,Spring只支援方法執行連線點;在AOP中表示為“在哪裡幹”;
  • 切入點(Pointcut): 選擇一組相關連線點的模式,即可以認為連線點的集合,Spring支援perl5正則表示式和AspectJ切入點模式,Spring預設使用AspectJ語法;在AOP中表示為“在哪裡乾的集合”;
  • 通知(Advice): 在連線點上執行的行為,通知提供了在AOP中需要在切入點所選擇的連線點處進行擴充套件現有行為的手段;包括前置通知(b