賊厲害澎嚣,手?jǐn)]的 SpringBoot 緩存系統(tǒng),性能杠杠的瘟芝!

image
  • 一、通用緩存接口

  • 二褥琐、本地緩存

  • 三锌俱、分布式緩存

  • 四、緩存“及時(shí)”過期問題

  • 五敌呈、二級(jí)緩存

緩存是最直接有效提升系統(tǒng)性能的手段之一贸宏。個(gè)人認(rèn)為用好用對(duì)緩存是優(yōu)秀程序員的必備基本素質(zhì)。

本文結(jié)合實(shí)際開發(fā)經(jīng)驗(yàn)磕洪,從簡(jiǎn)單概念原理和代碼入手吭练,一步一步搭建一個(gè)簡(jiǎn)單的二級(jí)緩存系統(tǒng)。

一析显、通用緩存接口

1鲫咽、緩存基礎(chǔ)算法

(1)、FIFO(First In First Out),先進(jìn)先出分尸,和OS里的FIFO思路相同锦聊,如果一個(gè)數(shù)據(jù)最先進(jìn)入緩存中,當(dāng)緩存滿的時(shí)候箩绍,應(yīng)當(dāng)把最先進(jìn)入緩存的數(shù)據(jù)給移除掉孔庭。(2)、LFU(Least Frequently Used)材蛛,最不經(jīng)常使用圆到,如果一個(gè)數(shù)據(jù)在最近一段時(shí)間內(nèi)使用次數(shù)很少,那么在將來一段時(shí)間內(nèi)被使用的可能性也很小卑吭。(3)芽淡、LRU(Least Recently Used),最近最少使用陨簇,如果一個(gè)數(shù)據(jù)在最近一段時(shí)間沒有被訪問到吐绵,那么在將來它被訪問的可能性也很小。也就是說河绽,當(dāng)限定的空間已存滿數(shù)據(jù)時(shí)己单,應(yīng)當(dāng)把最久沒有被訪問到的數(shù)據(jù)移除。

2耙饰、接口定義

簡(jiǎn)單定義緩存接口纹笼,大致可以抽象如下:

<pre class="brush:java;toolbar:false" style="margin: 0.5em 0px; padding: 0.4em 0.6em; border-radius: 8px; background: rgb(248, 248, 248); color: rgb(0, 0, 0); font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">package com.power.demo.cache.contract;
import java.util.function.Function;
/**

  • 緩存提供者接口
    /
    public interface CacheProviderService {
    /
    • 查詢緩存
    • @param key 緩存鍵 不可為空
      /
      <T extends Object> T get(String key);
      /
    • 查詢緩存
    • @param key 緩存鍵 不可為空
    • @param function 如沒有緩存,調(diào)用該callable函數(shù)返回對(duì)象 可為空
      /
      <T extends Object> T get(String key, Function<String, T> function);
      /
    • 查詢緩存
    • @param key 緩存鍵 不可為空
    • @param function 如沒有緩存苟跪,調(diào)用該callable函數(shù)返回對(duì)象 可為空
    • @param funcParm function函數(shù)的調(diào)用參數(shù)
      /
      <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm);
      /
    • 查詢緩存
    • @param key 緩存鍵 不可為空
    • @param function 如沒有緩存廷痘,調(diào)用該callable函數(shù)返回對(duì)象 可為空
    • @param expireTime 過期時(shí)間(單位:毫秒) 可為空
      /
      <T extends Object> T get(String key, Function<String, T> function, Long expireTime);
      /
    • 查詢緩存
    • @param key 緩存鍵 不可為空
    • @param function 如沒有緩存,調(diào)用該callable函數(shù)返回對(duì)象 可為空
    • @param funcParm function函數(shù)的調(diào)用參數(shù)
    • @param expireTime 過期時(shí)間(單位:毫秒) 可為空
      /
      <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm, Long expireTime);
      /
    • 設(shè)置緩存鍵值
    • @param key 緩存鍵 不可為空
    • @param obj 緩存值 不可為空
      /
      <T extends Object> void set(String key, T obj);
      /
    • 設(shè)置緩存鍵值
    • @param key 緩存鍵 不可為空
    • @param obj 緩存值 不可為空
    • @param expireTime 過期時(shí)間(單位:毫秒) 可為空
      /
      <T extends Object> void set(String key, T obj, Long expireTime);
      /
    • 移除緩存
    • @param key 緩存鍵 不可為空
      /
      void remove(String key);
      /
    • 是否存在緩存
    • @param key 緩存鍵 不可為空
      **/
      boolean contains(String key);
      }</pre>

注意件已,這里列出的只是常見緩存功能接口笋额,一些在特殊場(chǎng)景下用到的統(tǒng)計(jì)類的接口、分布式鎖篷扩、自增(減)等功能不在討論范圍之內(nèi)兄猩。

Get相關(guān)方法,注意多個(gè)參數(shù)的情況鉴未,緩存接口里面?zhèn)魅说腇unction枢冤,這是Java8提供的函數(shù)式接口,雖然支持的入?yún)€(gè)數(shù)有限(這里你會(huì)非常懷念.NET下的Func委托)铜秆,但是僅對(duì)Java這個(gè)語言來說淹真,這真是一個(gè)重大的進(jìn)步_

接口定義好了连茧,下面就要實(shí)現(xiàn)緩存提供者程序了核蘸。按照存儲(chǔ)類型的不同巍糯,本文簡(jiǎn)單實(shí)現(xiàn)最常用的兩種緩存提供者:本地緩存和分布式緩存。

二值纱、本地緩存

本地緩存鳞贷,也就是JVM級(jí)別的緩存(本地緩存可以認(rèn)為是直接在進(jìn)程內(nèi)通信調(diào)用,而分布式緩存則需要通過網(wǎng)絡(luò)進(jìn)行跨進(jìn)程通信調(diào)用)虐唠,一般有很多種實(shí)現(xiàn)方式搀愧,比如直接使用Hashtable、ConcurrentHashMap等天生線程安全的集合作為緩存容器疆偿,或者使用一些成熟的開源組件咱筛,如EhCache、Guava Cache等杆故。本文選擇上手簡(jiǎn)單的Guava緩存迅箩。

1、什么是Guava

Guava处铛,簡(jiǎn)單來說就是一個(gè)開發(fā)類庫饲趋,且是一個(gè)非常豐富強(qiáng)大的開發(fā)工具包,號(hào)稱可以讓使用Java語言更令人愉悅撤蟆,主要包括基本工具類庫和接口奕塑、緩存、發(fā)布訂閱風(fēng)格的事件總線等家肯。在實(shí)際開發(fā)中龄砰,我用的最多的是集合、緩存和常用類型幫助類讨衣,很多人都對(duì)這個(gè)類庫稱贊有加换棚。

2、添加依賴

<pre class="brush:java;toolbar:false" style="margin: 0.5em 0px; padding: 0.4em 0.6em; border-radius: 8px; background: rgb(248, 248, 248); color: rgb(0, 0, 0); font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> <dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency></pre>

3反镇、實(shí)現(xiàn)接口

<pre class="brush:java;toolbar:false" style="margin: 0.5em 0px; padding: 0.4em 0.6em; border-radius: 8px; background: rgb(248, 248, 248); color: rgb(0, 0, 0); font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">package com.power.demo.cache.impl;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Maps;
import com.power.demo.cache.contract.CacheProviderService;
import com.power.demo.common.AppConst;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
/*

  • 本地緩存提供者服務(wù) (Guava Cache)
  • /
    @Configuration
    @ComponentScan(basePackages = AppConst.BASE_PACKAGE_NAME)
    @Qualifier("localCacheService")
    public class LocalCacheProviderImpl implements CacheProviderService {
    private static Map<String, Cache<String, Object>> _cacheMap = Maps.newConcurrentMap();
    static {
    Cache<String, Object> cacheContainer = CacheBuilder.newBuilder()
    .maximumSize(AppConst.CACHE_MAXIMUM_SIZE)
    .expireAfterWrite(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS)//最后一次寫入后的一段時(shí)間移出
    //.expireAfterAccess(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS) //最后一次訪問后的一段時(shí)間移出
    .recordStats()//開啟統(tǒng)計(jì)功能
    .build();
    _cacheMap.put(String.valueOf(AppConst.CACHE_MINUTE), cacheContainer);
    }
    /
    *
    • 查詢緩存
    • @param key 緩存鍵 不可為空
      /
      public <T extends Object> T get(String key) {
      T obj = get(key, null, null, AppConst.CACHE_MINUTE);
      return obj;
      }
      /
    • 查詢緩存
    • @param key 緩存鍵 不可為空
    • @param function 如沒有緩存固蚤,調(diào)用該callable函數(shù)返回對(duì)象 可為空
      /
      public <T extends Object> T get(String key, Function<String, T> function) {
      T obj = get(key, function, key, AppConst.CACHE_MINUTE);
      return obj;
      }
      /
    • 查詢緩存
    • @param key 緩存鍵 不可為空
    • @param function 如沒有緩存,調(diào)用該callable函數(shù)返回對(duì)象 可為空
    • @param funcParm function函數(shù)的調(diào)用參數(shù)
      /
      public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm) {
      T obj = get(key, function, funcParm, AppConst.CACHE_MINUTE);
      return obj;
      }
      /
    • 查詢緩存
    • @param key 緩存鍵 不可為空
    • @param function 如沒有緩存歹茶,調(diào)用該callable函數(shù)返回對(duì)象 可為空
    • @param expireTime 過期時(shí)間(單位:毫秒) 可為空
      /
      public <T extends Object> T get(String key, Function<String, T> function, Long expireTime) {
      T obj = get(key, function, key, expireTime);
      return obj;
      }
      /
    • 查詢緩存
    • @param key 緩存鍵 不可為空
    • @param function 如沒有緩存夕玩,調(diào)用該callable函數(shù)返回對(duì)象 可為空
    • @param funcParm function函數(shù)的調(diào)用參數(shù)
    • @param expireTime 過期時(shí)間(單位:毫秒) 可為空
      /
      public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm, Long expireTime) {
      T obj = null;
      if (StringUtils.isEmpty(key) == true) {
      return obj;
      }
      expireTime = getExpireTime(expireTime);
      Cache<String, Object> cacheContainer = getCacheContainer(expireTime);
      try {
      if (function == null) {
      obj = (T) cacheContainer.getIfPresent(key);
      } else {
      final Long cachedTime = expireTime;
      obj = (T) cacheContainer.get(key, () -> {
      T retObj = function.apply(funcParm);
      return retObj;
      });
      }
      } catch (Exception e) {
      e.printStackTrace();
      }
      return obj;
      }
      /
    • 設(shè)置緩存鍵值 直接向緩存中插入值,這會(huì)直接覆蓋掉給定鍵之前映射的值
    • @param key 緩存鍵 不可為空
    • @param obj 緩存值 不可為空
      /
      public <T extends Object> void set(String key, T obj) {
      set(key, obj, AppConst.CACHE_MINUTE);
      }
      /
    • 設(shè)置緩存鍵值 直接向緩存中插入值辆亏,這會(huì)直接覆蓋掉給定鍵之前映射的值
    • @param key 緩存鍵 不可為空
    • @param obj 緩存值 不可為空
    • @param expireTime 過期時(shí)間(單位:毫秒) 可為空
      /
      public <T extends Object> void set(String key, T obj, Long expireTime) {
      if (StringUtils.isEmpty(key) == true) {
      return;
      }
      if (obj == null) {
      return;
      }
      expireTime = getExpireTime(expireTime);
      Cache<String, Object> cacheContainer = getCacheContainer(expireTime);
      cacheContainer.put(key, obj);
      }
      /
    • 移除緩存
    • @param key 緩存鍵 不可為空
      /
      public void remove(String key) {
      if (StringUtils.isEmpty(key) == true) {
      return;
      }
      long expireTime = getExpireTime(AppConst.CACHE_MINUTE);
      Cache<String, Object> cacheContainer = getCacheContainer(expireTime);
      cacheContainer.invalidate(key);
      }
      /
    • 是否存在緩存
    • @param key 緩存鍵 不可為空
      /
      public boolean contains(String key) {
      boolean exists = false;
      if (StringUtils.isEmpty(key) == true) {
      return exists;
      }
      Object obj = get(key);
      if (obj != null) {
      exists = true;
      }
      return exists;
      }
      private static Lock lock = new ReentrantLock();
      private Cache<String, Object> getCacheContainer(Long expireTime) {
      Cache<String, Object> cacheContainer = null;
      if (expireTime == null) {
      return cacheContainer;
      }
      String mapKey = String.valueOf(expireTime);
      if (_cacheMap.containsKey(mapKey) == true) {
      cacheContainer = _cacheMap.get(mapKey);
      return cacheContainer;
      }
      try {
      lock.lock();
      cacheContainer = CacheBuilder.newBuilder()
      .maximumSize(AppConst.CACHE_MAXIMUM_SIZE)
      .expireAfterWrite(expireTime, TimeUnit.MILLISECONDS)//最后一次寫入后的一段時(shí)間移出
      //.expireAfterAccess(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS) //最后一次訪問后的一段時(shí)間移出
      .recordStats()//開啟統(tǒng)計(jì)功能
      .build();
      _cacheMap.put(mapKey, cacheContainer);
      } finally {
      lock.unlock();
      }
      return cacheContainer;
      }
      /
    • 獲取過期時(shí)間 單位:毫秒
    • @param expireTime 傳人的過期時(shí)間 單位毫秒 如小于1分鐘,默認(rèn)為10分鐘
      **/
      private Long getExpireTime(Long expireTime) {
      Long result = expireTime;
      if (expireTime == null || expireTime < AppConst.CACHE_MINUTE / 10) {
      result = AppConst.CACHE_MINUTE;
      }
      return result;
      }
      }</pre>

4鳖目、注意事項(xiàng)

Guava Cache初始化容器時(shí)扮叨,支持緩存過期策略,類似FIFO领迈、LRU和LFU等算法彻磁。

expireAfterWrite:最后一次寫入后的一段時(shí)間移出碍沐。

expireAfterAccess:最后一次訪問后的一段時(shí)間移出。

Guava Cache對(duì)緩存過期時(shí)間的設(shè)置實(shí)在不夠友好衷蜓。常見的應(yīng)用場(chǎng)景累提,比如,有些幾乎不變的基礎(chǔ)數(shù)據(jù)緩存1天磁浇,有些熱點(diǎn)數(shù)據(jù)緩存2小時(shí)斋陪,有些會(huì)話數(shù)據(jù)緩存5分鐘等等。

通常我們認(rèn)為設(shè)置緩存的時(shí)候帶上緩存的過期時(shí)間是非常容易的置吓,而且只要一個(gè)緩存容器實(shí)例即可无虚,比如.NET下的ObjectCache、System.Runtime.Cache等等衍锚。

但是Guava Cache不是這個(gè)實(shí)現(xiàn)思路友题,如果緩存的過期時(shí)間不同,Guava的CacheBuilder要初始化多份Cache實(shí)例戴质。

好在我在實(shí)現(xiàn)的時(shí)候注意到了這個(gè)問題度宦,并且提供了解決方案,可以看到getCacheContainer這個(gè)函數(shù)告匠,根據(jù)過期時(shí)長(zhǎng)做緩存實(shí)例判斷戈抄,就算不同過期時(shí)間的多實(shí)例緩存也是完全沒有問題的。

三凫海、分布式緩存

分布式緩存產(chǎn)品非常多呛凶,本文使用應(yīng)用普遍的Redis,在Spring Boot應(yīng)用中使用Redis非常簡(jiǎn)單行贪。

1漾稀、什么是Redis

Redis是一款開源(BSD許可)的、用C語言寫成的高性能的鍵-值存儲(chǔ)(key-value store)建瘫。它常被稱作是一款數(shù)據(jù)結(jié)構(gòu)服務(wù)器(data structure server)崭捍。它可以被用作緩存、消息中間件和數(shù)據(jù)庫啰脚,在很多應(yīng)用中殷蛇,經(jīng)常看到有人選擇使用Redis做緩存橄浓,實(shí)現(xiàn)分布式鎖和分布式Session等粒梦。作為緩存系統(tǒng)時(shí),和經(jīng)典的KV結(jié)構(gòu)的Memcached非常相似荸实,但又有很多不同匀们。Redis支持豐富的數(shù)據(jù)類型。Redis的鍵值可以包括字符串(strings)類型准给,同時(shí)它還包括哈希(hashes)泄朴、列表(lists)重抖、集合(sets)和有序集合(sorted sets)等數(shù)據(jù)類型。對(duì)于這些數(shù)據(jù)類型祖灰,你可以執(zhí)行原子操作钟沛。例如:對(duì)字符串進(jìn)行附加操作(append);遞增哈希中的值局扶;向列表中增加元素恨统;計(jì)算集合的交集、并集與差集等详民。

Redis的數(shù)據(jù)類型:Keys:非二進(jìn)制安全的字符類型( not binary-safe strings )延欠,由于key不是binary safe的字符串,所以像“my key”和“mykey\n”這樣包含空格和換行的key是不允許的沈跨。Values:Strings由捎、Hash、Lists饿凛、 Sets狞玛、 Sorted sets〗е希考慮到Redis單線程操作模式心肪,Value的粒度不應(yīng)該過大,緩存的值越大纠吴,越容易造成阻塞和排隊(duì)硬鞍。

為了獲得優(yōu)異的性能,Redis采用了內(nèi)存中(in-memory)數(shù)據(jù)集(dataset)的方式戴已。同時(shí)固该,Redis支持?jǐn)?shù)據(jù)的持久化,你可以每隔一段時(shí)間將數(shù)據(jù)集轉(zhuǎn)存到磁盤上(snapshot)糖儡,或者在日志尾部追加每一條操作命令(append only file,aof)伐坏。Redis同樣支持主從復(fù)制(master-slave replication),并且具有非澄樟快速的非阻塞首次同步( non-blocking first synchronization)桦沉、網(wǎng)絡(luò)斷開自動(dòng)重連等功能。同時(shí)Redis還具有其它一些特性金闽,其中包括簡(jiǎn)單的事物支持纯露、發(fā)布訂閱 ( pub/sub)、管道(pipeline)和虛擬內(nèi)存(vm)等 代芜。

2埠褪、添加依賴

<pre class="brush:java;toolbar:false" style="margin: 0.5em 0px; padding: 0.4em 0.6em; border-radius: 8px; background: rgb(248, 248, 248); color: rgb(0, 0, 0); font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency></pre>

3、配置Redis

在application.properties配置文件中,配置Redis常用參數(shù):

Redis緩存相關(guān)配置

Redis數(shù)據(jù)庫索引(默認(rèn)為0)

spring.redis.database=0

Redis服務(wù)器地址

spring.redis.host=127.0.0.1

Redis服務(wù)器端口

spring.redis.port=6379

Redis服務(wù)器密碼(默認(rèn)為空)

spring.redis.password=123321

Redis連接超時(shí)時(shí)間 默認(rèn):5分鐘(單位:毫秒)

spring.redis.timeout=300000ms

Redis連接池最大連接數(shù)(使用負(fù)值表示沒有限制)

spring.redis.jedis.pool.max-active=512

Redis連接池中的最小空閑連接

spring.redis.jedis.pool.min-idle=0

Redis連接池中的最大空閑連接

spring.redis.jedis.pool.max-idle=8

Redis連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒有限制)

spring.redis.jedis.pool.max-wait=-1ms

常見的需要注意的是最大連接數(shù)(spring.redis.jedis.pool.max-active )和超時(shí)時(shí)間(spring.redis.jedis.pool.max-wait)组橄。Redis在生產(chǎn)環(huán)境中出現(xiàn)故障的頻率經(jīng)常和這兩個(gè)參數(shù)息息相關(guān)。

接著定義一個(gè)繼承自CachingConfigurerSupport(請(qǐng)注意cacheManager和keyGenerator這兩個(gè)方法在子類的實(shí)現(xiàn))的RedisConfig類:

<pre class="brush:java;toolbar:false" style="margin: 0.5em 0px; padding: 0.4em 0.6em; border-radius: 8px; background: rgb(248, 248, 248); color: rgb(0, 0, 0); font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">package com.power.demo.cache.config;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**

  • Redis緩存配置類
    */
    @Configuration
    @EnableCaching
    public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
    return RedisCacheManager.create(connectionFactory);
    }
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    //Jedis的Key和Value的序列化器默認(rèn)值是JdkSerializationRedisSerializer
    //經(jīng)實(shí)驗(yàn)罚随,JdkSerializationRedisSerializer通過RedisDesktopManager看到的鍵值對(duì)不能正常解析
    //設(shè)置key的序列化器
    template.setKeySerializer(new StringRedisSerializer());
    ////設(shè)置value的序列化器 默認(rèn)值是JdkSerializationRedisSerializer
    //使用Jackson序列化器的問題是玉工,復(fù)雜對(duì)象可能序列化失敗,比如JodaTime的DateTime類型
    // //使用Jackson2淘菩,將對(duì)象序列化為JSON
    // Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
    // //json轉(zhuǎn)對(duì)象類遵班,不設(shè)置默認(rèn)的會(huì)將json轉(zhuǎn)成hashmap
    // ObjectMapper om = new ObjectMapper();
    // om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    // om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    // jackson2JsonRedisSerializer.setObjectMapper(om);
    // template.setValueSerializer(jackson2JsonRedisSerializer);
    //將redis連接工廠設(shè)置到模板類中
    template.setConnectionFactory(factory);
    return template;
    }
    // //自定義緩存key生成策略
    // @Bean
    // public KeyGenerator keyGenerator() {
    // return new KeyGenerator() {
    // @Override
    // public Object generate(Object target, java.lang.reflect.Method method, Object... params) {
    // StringBuffer sb = new StringBuffer();
    // sb.append(target.getClass().getName());
    // sb.append(method.getName());
    // for (Object obj : params) {
    // if (obj == null) {
    // continue;
    // }
    // sb.append(obj.toString());
    // }
    // return sb.toString();
    // }
    // };
    // }
    }</pre>

在RedisConfig這個(gè)類上加上@EnableCaching這個(gè)注解,這個(gè)注解會(huì)被Spring發(fā)現(xiàn)潮改,并且會(huì)創(chuàng)建一個(gè)切面(aspect) 并觸發(fā)Spring緩存注解的切點(diǎn)(pointcut)狭郑。據(jù)所使用的注解以及緩存的狀態(tài),這個(gè)切面會(huì)從緩存中獲取數(shù)據(jù)汇在,將數(shù)據(jù)添加到緩存之中或者從緩存中移除某個(gè)值翰萨。cacheManager方法,申明一個(gè)緩存管理器(CacheManager)的bean糕殉,作用就是@EnableCaching這個(gè)切面在新增緩存或者刪除緩存的時(shí)候會(huì)調(diào)用這個(gè)緩存管理器的方法亩鬼。keyGenerator方法,可以根據(jù)需求自定義緩存key生成策略阿蝶。

而redisTemplate方法雳锋,則主要是設(shè)置Redis模板類垃喊,比如鍵和值的序列化器(從這里可以看出靴寂,Redis的鍵值對(duì)必須可序列化)市框、redis連接工廠等缘挽。

RedisTemplate支持的序列化器主要有如下幾種:

JdkSerializationRedisSerializer:使用Java序列化掖肋;StringRedisSerializer:序列化String類型的key和value油啤;GenericToStringSerializer:使用Spring轉(zhuǎn)換服務(wù)進(jìn)行序列化色罚;JacksonJsonRedisSerializer:使用Jackson 1窟扑,將對(duì)象序列化為JSON咆瘟;Jackson2JsonRedisSerializer:使用Jackson 2嚼隘,將對(duì)象序列化為JSON;OxmSerializer:使用Spring O/X映射的編排器和解排器(marshaler和unmarshaler)實(shí)現(xiàn)序列化袒餐,用于XML序列化飞蛹;

注意:RedisTemplate的鍵和值序列化器,默認(rèn)情況下都是JdkSerializationRedisSerializer灸眼,它們都可以自定義設(shè)置序列化器卧檐。推薦將字符串鍵使用StringRedisSerializer序列化器,因?yàn)檫\(yùn)維的時(shí)候好排查問題焰宣,JDK序列化器的也能識(shí)別霉囚,但是可讀性稍差(是因?yàn)榫彺娣?wù)器沒有JRE嗎?)匕积,見如下效果:

image

而值序列化器則要復(fù)雜的多盈罐,很多人推薦使用Jackson2JsonRedisSerializer序列化器榜跌,但是實(shí)際開發(fā)過程中,經(jīng)常有人碰到反序列化錯(cuò)誤盅粪,經(jīng)過排查多數(shù)都和Jackson2JsonRedisSerializer這個(gè)序列化器有關(guān)钓葫。

4、實(shí)現(xiàn)接口

使用RedisTemplate票顾,在Spring Boot中調(diào)用Redis接口比直接調(diào)用Jedis簡(jiǎn)單多了础浮。

<pre class="brush:java;toolbar:false" style="margin: 0.5em 0px; padding: 0.4em 0.6em; border-radius: 8px; background: rgb(248, 248, 248); color: rgb(0, 0, 0); font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">package com.power.demo.cache.impl;
import com.power.demo.cache.contract.CacheProviderService;
import com.power.demo.common.AppConst;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
@Configuration
@ComponentScan(basePackages = AppConst.BASE_PACKAGE_NAME)
@Qualifier("redisCacheService")
public class RedisCacheProviderImpl implements CacheProviderService {
@Resource
private RedisTemplate<Serializable, Object> redisTemplate;
/**
* 查詢緩存
*
* @param key 緩存鍵 不可為空
/
public <T extends Object> T get(String key) {
T obj = get(key, null, null, AppConst.CACHE_MINUTE);
return obj;
}
/

* 查詢緩存
*
* @param key 緩存鍵 不可為空
* @param function 如沒有緩存,調(diào)用該callable函數(shù)返回對(duì)象 可為空
/
public <T extends Object> T get(String key, Function<String, T> function) {
T obj = get(key, function, key, AppConst.CACHE_MINUTE);
return obj;
}
/

* 查詢緩存
*
* @param key 緩存鍵 不可為空
* @param function 如沒有緩存奠骄,調(diào)用該callable函數(shù)返回對(duì)象 可為空
* @param funcParm function函數(shù)的調(diào)用參數(shù)
/
public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm) {
T obj = get(key, function, funcParm, AppConst.CACHE_MINUTE);
return obj;
}
/

* 查詢緩存
*
* @param key 緩存鍵 不可為空
* @param function 如沒有緩存豆同,調(diào)用該callable函數(shù)返回對(duì)象 可為空
* @param expireTime 過期時(shí)間(單位:毫秒) 可為空
/
public <T extends Object> T get(String key, Function<String, T> function, Long expireTime) {
T obj = get(key, function, key, expireTime);
return obj;
}
/

* 查詢緩存
*
* @param key 緩存鍵 不可為空
* @param function 如沒有緩存,調(diào)用該callable函數(shù)返回對(duì)象 可為空
* @param funcParm function函數(shù)的調(diào)用參數(shù)
* @param expireTime 過期時(shí)間(單位:毫秒) 可為空
/
public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm, Long expireTime) {
T obj = null;
if (StringUtils.isEmpty(key) == true) {
return obj;
}
expireTime = getExpireTime(expireTime);
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
obj = (T) operations.get(key);
if (function != null && obj == null) {
obj = function.apply(funcParm);
if (obj != null) {
set(key, obj, expireTime);//設(shè)置緩存信息
}
}
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
/

* 設(shè)置緩存鍵值 直接向緩存中插入值含鳞,這會(huì)直接覆蓋掉給定鍵之前映射的值
*
* @param key 緩存鍵 不可為空
* @param obj 緩存值 不可為空
/
public <T extends Object> void set(String key, T obj) {
set(key, obj, AppConst.CACHE_MINUTE);
}
/

* 設(shè)置緩存鍵值 直接向緩存中插入值影锈,這會(huì)直接覆蓋掉給定鍵之前映射的值
*
* @param key 緩存鍵 不可為空
* @param obj 緩存值 不可為空
* @param expireTime 過期時(shí)間(單位:毫秒) 可為空
/
public <T extends Object> void set(String key, T obj, Long expireTime) {
if (StringUtils.isEmpty(key) == true) {
return;
}
if (obj == null) {
return;
}
expireTime = getExpireTime(expireTime);
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, obj);
redisTemplate.expire(key, expireTime, TimeUnit.MILLISECONDS);
}
/

* 移除緩存
*
* @param key 緩存鍵 不可為空
/
public void remove(String key) {
if (StringUtils.isEmpty(key) == true) {
return;
}
redisTemplate.delete(key);
}
/

* 是否存在緩存
*
* @param key 緩存鍵 不可為空
/
public boolean contains(String key) {
boolean exists = false;
if (StringUtils.isEmpty(key) == true) {
return exists;
}
Object obj = get(key);
if (obj != null) {
exists = true;
}
return exists;
}
/

* 獲取過期時(shí)間 單位:毫秒
*
* @param expireTime 傳人的過期時(shí)間 單位毫秒 如小于1分鐘,默認(rèn)為10分鐘
**/
private Long getExpireTime(Long expireTime) {
Long result = expireTime;
if (expireTime == null || expireTime < AppConst.CACHE_MINUTE / 10) {
result = AppConst.CACHE_MINUTE;
}
return result;
}
}</pre>

注意:很多教程里都講到通過注解的方式(@Cacheable蝉绷,@CachePut精居、@CacheEvict和@Caching)實(shí)現(xiàn)數(shù)據(jù)緩存,根據(jù)實(shí)踐潜必,我個(gè)人是不推崇這種使用方式的靴姿。

四、緩存“及時(shí)”過期問題

這個(gè)也是開發(fā)和運(yùn)維過程中非常經(jīng)典的問題磁滚。

有些公司寫緩存客戶端的時(shí)候佛吓,會(huì)給每個(gè)團(tuán)隊(duì)分別定義一個(gè)Area,但是這個(gè)只能做到緩存鍵的分布區(qū)分垂攘,不能保證緩存“實(shí)時(shí)”有效的過期维雇。

多年以前我寫過一篇結(jié)合實(shí)際情況的文章,也就是加上緩存版本晒他,請(qǐng)猛擊這里 吱型,算是提供了一種相對(duì)有效的方案,不過高并發(fā)站點(diǎn)要慎重陨仅,防止發(fā)生雪崩效應(yīng)津滞。

Redis還有一些其他常見問題,比如:Redis的字符串類型Key和Value都有限制灼伤,且都是不能超過512M触徐,請(qǐng)猛擊這里。還有最大連接數(shù)和超時(shí)時(shí)間設(shè)置等問題狐赡,本文就不再一一列舉了撞鹉。

五、二級(jí)緩存

在配置文件中,加上緩存提供者開關(guān):

是否啟用本地緩存

spring.power.isuselocalcache=1

是否啟用Redis緩存

spring.power.isuserediscache=1

緩存提供者程序都實(shí)現(xiàn)好了鸟雏,我們會(huì)再包裝一個(gè)調(diào)用外觀類PowerCacheBuilder享郊,加上緩存版本控制,可以輕松自如地控制和切換緩存孝鹊,code talks:

<pre class="brush:java;toolbar:false" style="margin: 0.5em 0px; padding: 0.4em 0.6em; border-radius: 8px; background: rgb(248, 248, 248); color: rgb(0, 0, 0); font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">package com.power.demo.cache;
import com.google.common.collect.Lists;
import com.power.demo.cache.contract.CacheProviderService;
import com.power.demo.common.AppConst;
import com.power.demo.common.AppField;
import com.power.demo.util.ConfigUtil;
import com.power.demo.util.PowerLogger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
/*

  • 支持多緩存提供程序多級(jí)緩存的緩存幫助類
  • /
    @Configuration
    @ComponentScan(basePackages = AppConst.BASE_PACKAGE_NAME)
    public class PowerCacheBuilder {
    @Autowired
    @Qualifier("localCacheService")
    private CacheProviderService localCacheService;
    @Autowired
    @Qualifier("redisCacheService")
    private CacheProviderService redisCacheService;
    private static List<CacheProviderService> _listCacheProvider = Lists.newArrayList();
    private static final Lock providerLock = new ReentrantLock();
    /
    *
    • 初始化緩存提供者 默認(rèn)優(yōu)先級(jí):先本地緩存拂蝎,后分布式緩存
      /
      private List<CacheProviderService> getCacheProviders() {
      if (_listCacheProvider.size() > 0) {
      return _listCacheProvider;
      }
      //線程安全
      try {
      providerLock.tryLock(1000, TimeUnit.MILLISECONDS);
      if (_listCacheProvider.size() > 0) {
      return _listCacheProvider;
      }
      String isUseCache = ConfigUtil.getConfigVal(AppField.IS_USE_LOCAL_CACHE);
      CacheProviderService cacheProviderService = null;
      //啟用本地緩存
      if ("1".equalsIgnoreCase(isUseCache)) {
      _listCacheProvider.add(localCacheService);
      }
      isUseCache = ConfigUtil.getConfigVal(AppField.IS_USE_REDIS_CACHE);
      //啟用Redis緩存
      if ("1".equalsIgnoreCase(isUseCache)) {
      _listCacheProvider.add(redisCacheService);
      resetCacheVersion();//設(shè)置分布式緩存版本號(hào)
      }
      PowerLogger.info("初始化緩存提供者成功,共有" + _listCacheProvider.size() + "個(gè)");
      } catch (Exception e) {
      e.printStackTrace();
      _listCacheProvider = Lists.newArrayList();
      PowerLogger.error("初始化緩存提供者發(fā)生異常:{}", e);
      } finally {
      providerLock.unlock();
      }
      return _listCacheProvider;
      }
      /
    • 查詢緩存
    • @param key 緩存鍵 不可為空
      /
      public <T extends Object> T get(String key) {
      T obj = null;
      //key = generateVerKey(key);//構(gòu)造帶版本的緩存鍵
      for (CacheProviderService provider : getCacheProviders()) {
      obj = provider.get(key);
      if (obj != null) {
      return obj;
      }
      }
      return obj;
      }
      /
    • 查詢緩存
    • @param key 緩存鍵 不可為空
    • @param function 如沒有緩存惶室,調(diào)用該callable函數(shù)返回對(duì)象 可為空
      /
      public <T extends Object> T get(String key, Function<String, T> function) {
      T obj = null;
      for (CacheProviderService provider : getCacheProviders()) {
      if (obj == null) {
      obj = provider.get(key, function);
      } else if (function != null && obj != null) {//查詢并設(shè)置其他緩存提供者程序緩存
      provider.get(key, function);
      }
      //如果callable函數(shù)為空 而緩存對(duì)象不為空 及時(shí)跳出循環(huán)并返回
      if (function == null && obj != null) {
      return obj;
      }
      }
      return obj;
      }
      /
    • 查詢緩存
    • @param key 緩存鍵 不可為空
    • @param function 如沒有緩存,調(diào)用該callable函數(shù)返回對(duì)象 可為空
    • @param funcParm function函數(shù)的調(diào)用參數(shù)
      /
      public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm) {
      T obj = null;
      for (CacheProviderService provider : getCacheProviders()) {
      if (obj == null) {
      obj = provider.get(key, function, funcParm);
      } else if (function != null && obj != null) {//查詢并設(shè)置其他緩存提供者程序緩存
      provider.get(key, function, funcParm);
      }
      //如果callable函數(shù)為空 而緩存對(duì)象不為空 及時(shí)跳出循環(huán)并返回
      if (function == null && obj != null) {
      return obj;
      }
      }
      return obj;
      }
      /
    • 查詢緩存
    • @param key 緩存鍵 不可為空
    • @param function 如沒有緩存玄货,調(diào)用該callable函數(shù)返回對(duì)象 可為空
    • @param expireTime 過期時(shí)間(單位:毫秒) 可為空
      /
      public <T extends Object> T get(String key, Function<String, T> function, long expireTime) {
      T obj = null;
      for (CacheProviderService provider : getCacheProviders()) {
      if (obj == null) {
      obj = provider.get(key, function, expireTime);
      } else if (function != null && obj != null) {//查詢并設(shè)置其他緩存提供者程序緩存
      provider.get(key, function, expireTime);
      }
      //如果callable函數(shù)為空 而緩存對(duì)象不為空 及時(shí)跳出循環(huán)并返回
      if (function == null && obj != null) {
      return obj;
      }
      }
      return obj;
      }
      /
    • 查詢緩存
    • @param key 緩存鍵 不可為空
    • @param function 如沒有緩存皇钞,調(diào)用該callable函數(shù)返回對(duì)象 可為空
    • @param funcParm function函數(shù)的調(diào)用參數(shù)
    • @param expireTime 過期時(shí)間(單位:毫秒) 可為空
      /
      public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm, long expireTime) {
      T obj = null;
      for (CacheProviderService provider : getCacheProviders()) {
      if (obj == null) {
      obj = provider.get(key, function, funcParm, expireTime);
      } else if (function != null && obj != null) {//查詢并設(shè)置其他緩存提供者程序緩存
      provider.get(key, function, funcParm, expireTime);
      }
      //如果callable函數(shù)為空 而緩存對(duì)象不為空 及時(shí)跳出循環(huán)并返回
      if (function == null && obj != null) {
      return obj;
      }
      }
      return obj;
      }
      /
    • 設(shè)置緩存鍵值 直接向緩存中插入或覆蓋值
    • @param key 緩存鍵 不可為空
    • @param obj 緩存值 不可為空
      /
      public <T extends Object> void set(String key, T obj) {
      //key = generateVerKey(key);//構(gòu)造帶版本的緩存鍵
      for (CacheProviderService provider : getCacheProviders()) {
      provider.set(key, obj);
      }
      }
      /
    • 設(shè)置緩存鍵值 直接向緩存中插入或覆蓋值
    • @param key 緩存鍵 不可為空
    • @param obj 緩存值 不可為空
    • @param expireTime 過期時(shí)間(單位:毫秒) 可為空
      /
      public <T extends Object> void set(String key, T obj, Long expireTime) {
      //key = generateVerKey(key);//構(gòu)造帶版本的緩存鍵
      for (CacheProviderService provider : getCacheProviders()) {
      provider.set(key, obj, expireTime);
      }
      }
      /
    • 移除緩存
    • @param key 緩存鍵 不可為空
      /
      public void remove(String key) {
      //key = generateVerKey(key);//構(gòu)造帶版本的緩存鍵
      if (StringUtils.isEmpty(key) == true) {
      return;
      }
      for (CacheProviderService provider : getCacheProviders()) {
      provider.remove(key);
      }
      }
      /
    • 是否存在緩存
    • @param key 緩存鍵 不可為空
      /
      public boolean contains(String key) {
      boolean exists = false;
      //key = generateVerKey(key);//構(gòu)造帶版本的緩存鍵
      if (StringUtils.isEmpty(key) == true) {
      return exists;
      }
      Object obj = get(key);
      if (obj != null) {
      exists = true;
      }
      return exists;
      }
      /
    • 獲取分布式緩存版本號(hào)
      /
      public String getCacheVersion() {
      String version = "";
      boolean isUseCache = checkUseRedisCache();
      //未啟用Redis緩存
      if (isUseCache == false) {
      return version;
      }
      version = redisCacheService.get(AppConst.CACHE_VERSION_KEY);
      return version;
      }
      /
    • 重置分布式緩存版本 如果啟用分布式緩存,設(shè)置緩存版本
      /
      public String resetCacheVersion() {
      String version = "";
      boolean isUseCache = checkUseRedisCache();
      //未啟用Redis緩存
      if (isUseCache == false) {
      return version;
      }
      //設(shè)置緩存版本
      version = String.valueOf(Math.abs(UUID.randomUUID().hashCode()));
      redisCacheService.set(AppConst.CACHE_VERSION_KEY, version);
      return version;
      }
      /
    • 如果啟用分布式緩存松捉,獲取緩存版本夹界,重置查詢的緩存key,可以實(shí)現(xiàn)相對(duì)實(shí)時(shí)的緩存過期控制
    • <p>
    • 如沒有啟用分布式緩存隘世,緩存key不做修改可柿,直接返回
      /
      public String generateVerKey(String key) {
      String result = key;
      if (StringUtils.isEmpty(key) == true) {
      return result;
      }
      boolean isUseCache = checkUseRedisCache();
      //沒有啟用分布式緩存,緩存key不做修改丙者,直接返回
      if (isUseCache == false) {
      return result;
      }
      String version = redisCacheService.get(AppConst.CACHE_VERSION_KEY);
      if (StringUtils.isEmpty(version) == true) {
      return result;
      }
      result = String.format("%s_%s", result, version);
      return result;
      }
      /
    • 驗(yàn)證是否啟用分布式緩存
      **/
      private boolean checkUseRedisCache() {
      boolean isUseCache = false;
      String strIsUseCache = ConfigUtil.getConfigVal(AppField.IS_USE_REDIS_CACHE);
      isUseCache = "1".equalsIgnoreCase(strIsUseCache);
      return isUseCache;
      }
      }</pre>

單元測(cè)試如下:

<pre class="brush:java;toolbar:false" style="margin: 0.5em 0px; padding: 0.4em 0.6em; border-radius: 8px; background: rgb(248, 248, 248); color: rgb(0, 0, 0); font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> @Test public void testCacheVerson() throws Exception { String version = cacheBuilder.getCacheVersion(); System.out.println(String.format("當(dāng)前緩存版本:%s", version)); String cacheKey = cacheBuilder.generateVerKey("goods778899"); GoodsVO goodsVO = new GoodsVO(); goodsVO.setGoodsId(UUID.randomUUID().toString()); goodsVO.setCreateTime(new Date()); goodsVO.setCreateDate(new DateTime(new Date())); goodsVO.setGoodsType(1024); goodsVO.setGoodsCode("123456789"); goodsVO.setGoodsName("我的測(cè)試商品"); cacheBuilder.set(cacheKey, goodsVO); GoodsVO goodsVO1 = cacheBuilder.get(cacheKey); Assert.assertNotNull(goodsVO1); version = cacheBuilder.resetCacheVersion(); System.out.println(String.format("重置后的緩存版本:%s", version)); cacheKey = cacheBuilder.generateVerKey("goods112233"); cacheBuilder.set(cacheKey, goodsVO); GoodsVO goodsVO2 = cacheBuilder.get(cacheKey); Assert.assertNotNull(goodsVO2); Assert.assertTrue("兩個(gè)緩存對(duì)象的主鍵相同", goodsVO1.getGoodsId().equals(goodsVO2.getGoodsId())); }
</pre>

一個(gè)滿足基本功能的多級(jí)緩存系統(tǒng)就好了复斥。

在Spring Boot應(yīng)用中使用緩存則非常簡(jiǎn)潔,選擇調(diào)用上面包裝好的緩存接口即可械媒。

<pre class="brush:java;toolbar:false" style="margin: 0.5em 0px; padding: 0.4em 0.6em; border-radius: 8px; background: rgb(248, 248, 248); color: rgb(0, 0, 0); font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">String cacheKey = _cacheBuilder.generateVerKey("com.power.demo.apiservice.impl.getgoodsbyid." + request.getGoodsId());
GoodsVO goodsVO = _cacheBuilder.get(cacheKey, _goodsService::getGoodsByGoodsId, request.getGoodsId());</pre>

?到這里Spring Boot業(yè)務(wù)系統(tǒng)開發(fā)中最常用到的ORM目锭,緩存和隊(duì)列三板斧就介紹完了。

在開發(fā)的過程中你會(huì)發(fā)現(xiàn)纷捞,Java真的是非常非常中規(guī)中矩的語言痢虹,你需要不斷折騰并熟悉常見的開源中間件和工具,開源的輪子實(shí)在是太豐富主儡,多嘗試幾個(gè)奖唯,實(shí)踐出真知。

Java 的知識(shí)面非常廣糜值,面試問的涉及也非常廣泛丰捷,重點(diǎn)包括:Java 基礎(chǔ)、Java 并發(fā)寂汇,JVM瓢阴、MySQL、數(shù)據(jù)結(jié)構(gòu)健无、算法荣恐、Spring、微服務(wù)、MQ 等等叠穆,涉及的知識(shí)點(diǎn)何其龐大少漆,所以我們?cè)趶?fù)習(xí)的時(shí)候也往往無從下手,今天小編給大家?guī)硪惶?Java 面試題硼被,題庫非常全面示损,包括 Java 基礎(chǔ)、Java 集合嚷硫、JVM检访、Java 并發(fā)、Spring全家桶仔掸、Redis脆贵、MySQL、Dubbo起暮、Netty卖氨、MQ 等等,包含 Java 后端知識(shí)點(diǎn) 2000 + 负懦,部分如下:

image
image
image
image
image
image

資料獲取方式:關(guān)注公眾號(hào):“程序員白楠楠”獲取上述資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末筒捺,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子纸厉,更是在濱河造成了極大的恐慌系吭,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件颗品,死亡現(xiàn)場(chǎng)離奇詭異村斟,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)抛猫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門蟆盹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人闺金,你說我怎么就攤上這事逾滥。” “怎么了败匹?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵寨昙,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我掀亩,道長(zhǎng)舔哪,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任槽棍,我火速辦了婚禮捉蚤,結(jié)果婚禮上抬驴,老公的妹妹穿的比我還像新娘。我一直安慰自己缆巧,他們只是感情好布持,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著陕悬,像睡著了一般题暖。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上捉超,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天胧卤,我揣著相機(jī)與錄音,去河邊找鬼拼岳。 笑死枝誊,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的裂问。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼牛柒,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼堪簿!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起皮壁,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤椭更,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后蛾魄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體虑瀑,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年滴须,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了舌狗。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡扔水,死狀恐怖痛侍,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情魔市,我是刑警寧澤主届,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站待德,受9級(jí)特大地震影響君丁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜将宪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一绘闷、第九天 我趴在偏房一處隱蔽的房頂上張望橡庞。 院中可真熱鬧,春花似錦簸喂、人聲如沸毙死。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽扼倘。三九已至,卻和暖如春除呵,著一層夾襖步出監(jiān)牢的瞬間再菊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工颜曾, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留纠拔,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓泛豪,卻偏偏與公主長(zhǎng)得像稠诲,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子诡曙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345