1. 程式人生 > >Spring配置cache(concurrentHashMap,guava cache、redis實現)附原始碼

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已經能夠勝任了。

&n