在應用程序中,數據通常是存在數據庫中(磁盤介質),對於某些被頻繁訪問的數據,若是每次都訪問數據庫,不只涉及到網絡io,還受到數據庫查詢的影響;而目前一般會將頻繁使用,而且不常常改變的數據放入緩存中,從緩存中查詢數據的效率要高於數據庫,由於緩存通常KV形式存儲,而且是將數據存在「內存」中,從內存訪問數據是至關快的。html
對於頻繁訪問,須要緩存的數據,咱們通常是這樣作的:java
一、當收到查詢請求,先去查詢緩存,若是緩存中查詢到數據,那麼直接將查到的數據做爲響應數據;node
二、若是緩存中沒有找到要查詢的數據,那麼就從其餘地方,好比數據庫中查詢出來,若是從數據庫中查到了數據,就將數據放入緩存後,再將數據返回,下一次能夠直接從緩存查詢;git
這裏就不進一步探究「緩存穿透」的問題,有興趣能夠本身學習一下。github
本文就根據Spring框架分別對ConcurrentHashMap、Guava Cache、Redis進行闡釋如何使用,完整代碼已上傳到github:https://github.com/searchingbeyond/ssm web
ConcurrentHashMap是JDK自帶的,因此不須要多餘的jar包;redis
使用ConcurrentHashMap,是直接使用將數據存放在內存中,而且沒有數據過時的概念,也沒有數據容量的限制,因此只要不主動清理數據,那麼數據將一直不會減小。spring
另外,ConcurrentHashMap在多線程狀況下也是安全的,不要使用HashMap存緩存數據,由於HashMap在多線程操做時容易出現問題。數據庫
下面是user類代碼:apache
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; }
建立一個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; } }
這裏就不寫貼出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; } }
上面方法聲明上有@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),緩存項的值就是被註解的方法的返回值。
代碼以下:
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實現,其實只是替換ConcurrentHashMap,其餘的邏輯都是同樣的。
Guava是google開源的一個集成包,用途特別廣,在Cache也佔有一席之地,對於Guava Cache的用法,若是沒有用過,能夠參考:guava cache使用方式
使用Guava 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() { } }
因爲ConcurrentHashMap和Guava Cache都是將數據直接緩存在服務主機上,很顯然,緩存數據量的多少和主機的內存直接相關,通常不會用來緩存特別大的數據量;
而比較大的數據量,咱們通常用Redis進行緩存。
使用Redis整合Spring Cache,其實和ConcurrentHashMap和Guava Cache同樣,只是在實現Cache接口的類中,使用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; } }
只須要在涉及到數據操做的時候,使用上面的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已經可以勝任了。