Spring配置cache(concurrentHashMap,guava cache、redis實現)附源碼

  在應用程序中,數據一般是存在數據庫中(磁盤介質),對於某些被頻繁訪問的數據,如果每次都訪問數據庫,不僅涉及到網絡io,還受到數據庫查詢的影響;而目前通常會將頻繁使用,並且不經常改變的數據放入緩存中,從緩存中查詢數據的效率要高於數據庫,因為緩存一般KV形式存儲,並且是將數據存在“內存”中,從內存訪問數據是相當快的。

  對於頻繁訪問,需要緩存的數據,我們一般是這樣做的:

  1、當收到查詢請求,先去查詢緩存,如果緩存中查詢到數據,那麼直接將查到的數據作為響應數據;

  2、如果緩存中沒有找到要查詢的數據,那麼就從其他地方,比如數據庫中查詢出來,如果從數據庫中查到了數據,就將數據放入緩存後,再將數據返回,下一次可以直接從緩存查詢;

  這裡就不進一步探究“緩存穿透”的問題,有興趣可以自己學習一下。

  本文就根據Spring框架分別對ConcurrentHashMap、Guava Cache、Redis進行闡釋如何使用,完整代碼已上傳到github:https://github.com/searchingbeyond/ssm 

 

一、使用ConcurrentHashMap

1.1、特點說明

  ConcurrentHashMap是JDK自帶的,所以不需要多餘的jar包;

  使用ConcurrentHashMap,是直接使用將數據存放在內存中,並且沒有數據過期的概念,也沒有數據容量的限制,所以只要不主動清理數據,那麼數據將一直不會減少。

  另外,ConcurrentHashMap在多線程情況下也是安全的,不要使用HashMap存緩存數據,因為HashMap在多線程操作時容易出現問題。

 

1.2、創建user類

  下面是user類代碼:

package cn.ganlixin.ssm.model.entity;    import lombok.Data;    @Data  public class UserDO {      private Integer id;      private String name;      private Integer age;      private Integer gender;      private String addr;      private Integer status;  }  

  

1.3、創建spring cache的實現類

  創建一個UserCache類(類名隨意),實現org.springframework.cache.Cache接口,然後override需要實現的接口方法,主要針對getName、get、put、evict這4個方法進行重寫。

  注意,我在緩存user數據時,指定了緩存的規則:key用的是user的id,value就是user對象的json序列化字符。

package cn.ganlixin.ssm.cache.origin;    import cn.ganlixin.ssm.constant.CacheNameConstants;  import cn.ganlixin.ssm.model.entity.UserDO;  import cn.ganlixin.ssm.util.common.JsonUtils;  import org.apache.commons.lang3.StringUtils;  import org.springframework.cache.Cache;  import org.springframework.cache.support.SimpleValueWrapper;  import org.springframework.stereotype.Component;    import java.util.Map;  import java.util.Objects;  import java.util.concurrent.Callable;  import java.util.concurrent.ConcurrentHashMap;    @Component  public class UserCache implements Cache {        // 使用ConcurrentHashMap作為數據的存儲      private Map<String, String> storage = new ConcurrentHashMap<>();        // getName獲取cache的名稱,存取數據的時候用來區分是針對哪個cache操作      @Override      public String getName() {          return CacheNameConstants.USER_ORIGIN_CACHE;// 我用一個常量類來保存cache名稱      }        // put方法,就是執行將數據進行緩存      @Override      public void put(Object key, Object value) {          if (Objects.isNull(value)) {              return;          }            // 注意我在緩存的時候,緩存的值是把對象序列化後的(當然可以修改storage直接存放UserDO類也行)          storage.put(key.toString(), JsonUtils.encode(value, true));      }        // get方法,就是進行查詢緩存的操作,注意返回的是一個包裝後的值      @Override      public ValueWrapper get(Object key) {          String k = key.toString();          String value = storage.get(k);            // 注意返回的數據,要和存放時接收到數據保持一致,要將數據反序列化回來。          return StringUtils.isEmpty(value) ? null : new SimpleValueWrapper(JsonUtils.decode(value, UserDO.class));      }        // evict方法,是用來清除某個緩存項      @Override      public void evict(Object key) {          storage.remove(key.toString());      }        /*----------------------------下面的方法暫時忽略不管-----------------*/        @Override      public Object getNativeCache() { return null; }        @Override      public void clear() { }        @Override      public <T> T get(Object key, Class<T> type) { return null; }        @Override      public <T> T get(Object key, Callable<T> valueLoader) { return null; }  }  

  

1.4、創建service

  這裡就不寫貼出UserMapper的代碼了,直接看接口就明白了:

package cn.ganlixin.ssm.service;    import cn.ganlixin.ssm.model.entity.UserDO;    public interface UserService {        UserDO findUserById(Integer id);        Boolean removeUser(Integer id);        Boolean addUser(UserDO user);        Boolean modifyUser(UserDO user);  }  

  實現UserService,代碼如下:

package cn.ganlixin.ssm.service.impl;    import cn.ganlixin.ssm.constant.CacheNameConstants;  import cn.ganlixin.ssm.mapper.UserMapper;  import cn.ganlixin.ssm.model.entity.UserDO;  import cn.ganlixin.ssm.service.UserService;  import lombok.extern.slf4j.Slf4j;  import org.springframework.cache.annotation.CacheEvict;  import org.springframework.cache.annotation.Cacheable;  import org.springframework.stereotype.Service;    import javax.annotation.Resource;  import java.util.Objects;    @Service  @Slf4j  public class UserServiceImpl implements UserService {        @Resource      private UserMapper userMapper;        @Override      @Cacheable(value = CacheNameConstants.USER_ORIGIN_CACHE, key = "#id")      public UserDO findUserById(Integer id) {          try {              log.info("從DB查詢id為{}的用戶", id);              return userMapper.selectById(id);          } catch (Exception e) {              log.error("查詢用戶數據失敗,id:{}, e:{}", id, e);          }            return null;      }        @Override      @CacheEvict(              value = CacheNameConstants.USER_ORIGIN_CACHE,              key = "#id",              condition = "#result != false"      )      public Boolean removeUser(Integer id) {          if (Objects.isNull(id) || id <= 0) {              return false;          }            try {              int cnt = userMapper.deleteUserById(id);              return cnt > 0;          } catch (Exception e) {              log.error("刪除用戶數據失敗,id:{}, e:{}", id, e);          }            return false;      }        @Override      public Boolean addUser(UserDO user) {          if (Objects.isNull(user)) {              log.error("添加用戶異常,參數不能為null");              return false;          }            try {              return userMapper.insertUserSelectiveById(user) > 0;          } catch (Exception e) {              log.error("添加用戶失敗,data:{}, e:{}", user, e);          }            return false;      }        @Override      @CacheEvict(              value = CacheNameConstants.USER_ORIGIN_CACHE,              key = "#user.id",              condition = "#result != false"      )      public Boolean modifyUser(UserDO user) {          if (Objects.isNull(user) || Objects.isNull(user.getId()) || user.getId() <= 0) {              log.error("更新用戶異常,參數不合法,data:{}", user);              return false;          }            try {              return userMapper.updateUserSelectiveById(user) > 0;          } catch (Exception e) {              log.error("添加用戶失敗,data:{}, e:{}", user, e);          }            return false;      }  }  

 

1.5、@Cachable、@CachePut、@CacheEvict

  上面方法聲明上有@Cachable、@CachePut、@CacheEvict註解,用法如下:

  @Cachable註解的方法,先查詢緩存中有沒有,如果已經被緩存,則從緩存中查詢數據並返回給調用方;如果查緩存沒有查到數據,就執行被註解的方法(一般是從DB中查詢),然後將從DB查詢的結果進行緩存,然後將結果返回給調用方;

  @CachePut註解的方法,不會查詢緩存是否存在要查詢的數據,而是每次都執行被註解的方法,然後將結果的返回值先緩存,然後返回給調用方;

  @CacheEvict註解的方法,每次都會先執行被註解的方法,然後再將緩存中的緩存項給清除;

  這三個註解都有幾個參數,分別是value、key、condition,這些參數的含義如下:

  value,用來指定將數據放入哪個緩存,比如上面是將數據緩存到UserCache中;

  key,表示放入緩存的key,也就是UserCache中的put方法的key;

  condition,表示數據進行緩存的條件,condition為true時才會緩存數據;

  最後緩存項的值,這個值是指的K-V的V,其實只有@Cachable和@CachePut才需要注意緩存項的值(也就是put方法的value),緩存項的值就是被註解的方法的返回值。

 

1.6、創建一個controller進行測試

  代碼如下:

package cn.ganlixin.ssm.controller;    import cn.ganlixin.ssm.enums.ResultStatus;  import cn.ganlixin.ssm.model.Result;  import cn.ganlixin.ssm.model.entity.UserDO;  import cn.ganlixin.ssm.service.UserService;  import org.springframework.web.bind.annotation.*;    import javax.annotation.Resource;  import java.util.Objects;    @RestController  @RequestMapping("/user")  public class UserController {        @Resource      private UserService userService;        @GetMapping(value = "/getUserById")      public Result<UserDO> getUserById(Integer id) {          UserDO data = userService.findUserById(id);            if (Objects.isNull(data)) {              return new Result<>(ResultStatus.DATA_EMPTY.getCode(), ResultStatus.DATA_EMPTY.getMsg(), null);          }            return new Result<>(ResultStatus.OK.getCode(), ResultStatus.OK.getMsg(), data);      }        @PostMapping(value = "removeUser")      public Result<Boolean> removeUser(Integer id) {          Boolean res = userService.removeUser(id);          return res ? new Result<>(ResultStatus.OK.getCode(), ResultStatus.OK.getMsg(), true)                  : new Result<>(ResultStatus.FAILED.getCode(), ResultStatus.FAILED.getMsg(), false);      }        @PostMapping(value = "addUser")      public Result<Boolean> addUser(@RequestBody UserDO user) {          Boolean res = userService.addUser(user);            return res ? new Result<>(ResultStatus.OK.getCode(), ResultStatus.OK.getMsg(), true)                  : new Result<>(ResultStatus.FAILED.getCode(), ResultStatus.FAILED.getMsg(), false);      }        @PostMapping(value = "modifyUser")      public Result<Boolean> modifyUser(@RequestBody UserDO user) {          Boolean res = userService.modifyUser(user);            return res ? new Result<>(ResultStatus.OK.getCode(), ResultStatus.OK.getMsg(), true)                  : new Result<>(ResultStatus.FAILED.getCode(), ResultStatus.FAILED.getMsg(), false);      }    }  

  

 

 

二、使用Guava Cache實現

  使用Guava Cache實現,其實只是替換ConcurrentHashMap,其他的邏輯都是一樣的。

2.1、特點說明

  Guava是google開源的一個集成包,用途特別廣,在Cache也佔有一席之地,對於Guava Cache的用法,如果沒有用過,可以參考:guava cache使用方式

  使用Guava Cache,可以設置緩存的容量以及緩存的過期時間

 

2.2、實現spring cache接口

  仍舊使用之前的示例,重新創建一個Cache實現類,這裡對“Book”進行緩存,所以緩存名稱為BookCache。

package cn.ganlixin.ssm.cache.guava;    import cn.ganlixin.ssm.constant.CacheNameConstants;  import cn.ganlixin.ssm.model.entity.BookDO;  import com.google.common.cache.Cache;  import com.google.common.cache.CacheBuilder;  import org.springframework.cache.support.SimpleValueWrapper;  import org.springframework.stereotype.Component;    import javax.annotation.PostConstruct;  import java.util.Objects;  import java.util.concurrent.Callable;  import java.util.concurrent.TimeUnit;    /**   * 書籍數據緩存   */  @Component  public class BookCache implements org.springframework.cache.Cache {        // 下面的Cache是Guava對cache      private Cache<String, BookDO> storage;        @PostConstruct      private void init() {          storage = CacheBuilder.newBuilder()                  // 設置緩存的容量為100                  .maximumSize(100)                  // 設置初始容量為16                  .initialCapacity(16)                  // 設置過期時間為寫入緩存後10分鐘過期                  .refreshAfterWrite(10, TimeUnit.MINUTES)                  .build();      }        @Override      public String getName() {          return CacheNameConstants.BOOK_GUAVA_CACHE;      }        @Override      public ValueWrapper get(Object key) {          if (Objects.isNull(key)) {              return null;          }            BookDO data = storage.getIfPresent(key.toString());          return Objects.isNull(data) ? null : new SimpleValueWrapper(data);      }        @Override      public void evict(Object key) {          if (Objects.isNull(key)) {              return;          }            storage.invalidate(key.toString());      }        @Override      public void put(Object key, Object value) {          if (Objects.isNull(key) || Objects.isNull(value)) {              return;          }            storage.put(key.toString(), (BookDO) value);      }        /*-----------------------忽略下面的方法-----------------*/        @Override      public <T> T get(Object key, Class<T> type) { return null; }        @Override      public Object getNativeCache() { return null; }        @Override      public <T> T get(Object key, Callable<T> valueLoader) { return null; }        @Override      public void clear() { }  }  

  

 

三、使用Redis實現

3.1、特點說明

  由於ConcurrentHashMap和Guava Cache都是將數據直接緩存在服務主機上,很顯然,緩存數據量的多少和主機的內存直接相關,一般不會用來緩存特別大的數據量;

  而比較大的數據量,我們一般用Redis進行緩存。

  使用Redis整合Spring Cache,其實和ConcurrentHashMap和Guava Cache一樣,只是在實現Cache接口的類中,使用Redis進行存儲接口。

 

3.2、創建Redis集群操作類

  建議自己搭建一個redis測試集群,可以參考:

  redis配置如下(application.properties)

#redis集群的節點信息  redis.cluster.nodes=192.168.1.3:6379,192.168.1.4:6379,192.168.1.5:6379  # redis連接池的配置  redis.cluster.pool.max-active=8  redis.cluster.pool.max-idle=5  redis.cluster.pool.min-idle=3  

  

  代碼如下:

package cn.ganlixin.ssm.config;    import org.apache.commons.collections4.CollectionUtils;  import org.slf4j.Logger;  import org.slf4j.LoggerFactory;  import org.springframework.beans.factory.annotation.Value;  import org.springframework.context.annotation.Bean;  import org.springframework.context.annotation.Configuration;  import redis.clients.jedis.HostAndPort;  import redis.clients.jedis.JedisCluster;  import redis.clients.jedis.JedisPoolConfig;    import java.util.Set;  import java.util.stream.Collectors;    @Configuration  public class RedisClusterConfig {        private static final Logger log = LoggerFactory.getLogger(RedisClusterConfig.class);        @Value("${redis.cluster.nodes}")      private Set<String> redisNodes;        @Value("${redis.cluster.pool.max-active}")      private int maxTotal;        @Value("${redis.cluster.pool.max-idle}")      private int maxIdle;        @Value("${redis.cluster.pool.min-idle}")      private int minIdle;        // 初始化redis配置      @Bean      public JedisCluster redisCluster() {            if (CollectionUtils.isEmpty(redisNodes)) {              throw new RuntimeException();          }            // 設置redis集群的節點信息          Set<HostAndPort> nodes = redisNodes.stream().map(node -> {              String[] nodeInfo = node.split(":");              if (nodeInfo.length == 2) {                  return new HostAndPort(nodeInfo[0], Integer.parseInt(nodeInfo[1]));              } else {                  return new HostAndPort(nodeInfo[0], 6379);              }          }).collect(Collectors.toSet());            // 配置連接池          JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();          jedisPoolConfig.setMaxTotal(maxTotal);          jedisPoolConfig.setMaxIdle(maxIdle);          jedisPoolConfig.setMinIdle(minIdle);            // 創建jediscluster,傳入節點列表和連接池配置          JedisCluster cluster = new JedisCluster(nodes, jedisPoolConfig);          log.info("finish jedis cluster initailization");            return cluster;      }  }  

  

 3.3、創建spring cache實現類

  只需要在涉及到數據操作的時候,使用上面的jedisCluster即可,這裡存在redis的數據,我設置為Music,所以叫做music cache:

package cn.ganlixin.ssm.cache.redis;    import cn.ganlixin.ssm.constant.CacheNameConstants;  import cn.ganlixin.ssm.model.entity.MusicDO;  import cn.ganlixin.ssm.util.common.JsonUtils;  import com.google.common.base.Joiner;  import org.apache.commons.lang3.StringUtils;  import org.springframework.cache.Cache;  import org.springframework.cache.support.SimpleValueWrapper;  import org.springframework.stereotype.Component;  import redis.clients.jedis.JedisCluster;    import javax.annotation.Resource;  import java.util.Objects;  import java.util.concurrent.Callable;    @Component  public class MusicCache implements Cache {        // 使用自定義的redisCluster      @Resource      private JedisCluster redisCluster;        /**       * 構建redis緩存的key       *       * @param type   類型       * @param params 參數(不定長)       * @return 構建的key       */      private String buildKey(String type, Object... params) {          // 自己設定構建方式          return Joiner.on("_").join(type, params);      }        @Override      public String getName() {          return CacheNameConstants.MUSIC_REDIS_CACHE;      }        @Override      public void put(Object key, Object value) {          if (Objects.isNull(value)) {              return;          }            // 自己定義數據類型和格式          redisCluster.set(buildKey("music", key), JsonUtils.encode(value, true));      }        @Override      public ValueWrapper get(Object key) {          if (Objects.isNull(key)) {              return null;          }            // 自己定義數據類型和格式          String music = redisCluster.get(buildKey("music", key));          return StringUtils.isEmpty(music) ? null : new SimpleValueWrapper(JsonUtils.decode(music, MusicDO.class));      }        @Override      public void evict(Object key) {          if (Objects.isNull(key)) {              return;          }            redisCluster.del(buildKey("music", key));      }        @Override      public <T> T get(Object key, Class<T> type) { return null; }        @Override      public <T> T get(Object key, Callable<T> valueLoader) { return null; }        @Override      public void clear() { }        @Override      public Object getNativeCache() { return null; }  }  

  

總結

  使用spring cache的便捷之處在於@Cachable、@CachePut、@CacheEvict等幾個註解的使用,可以讓數據的處理變得更加的便捷,但其實,也並不是很便捷,因為我們需要對數據的存儲格式進行設定,另外還要根據不同情況來選擇使用哪一種緩存(ConcurrentHashMap、Guava Cache、Redis?);

  其實使用@Cachable、@CachePut、@CacheEvict也有很多局限的地方,比如刪除某項數據的時候,我希望清空多個緩存,因為這一項數據關聯的數據比較多,此時要麼在實現spring cache的接口方法上進行這些操作,但是這就涉及到在一個cache service中操作另外一個cache。

  針對上面說的情況,就不推薦使用spring cache,而是應該自己手動實現緩存的處理,這樣可以做到條理清晰;但是一般的情況,spring cache已經能夠勝任了。