1. 程式人生 > >Spring優雅整合Redis快取

Spring優雅整合Redis快取

 

“小明,多系統的session共享,怎麼處理?”“Redis快取啊!” “小明,我想實現一個簡單的訊息佇列?”“Redis快取啊!”

“小明,分散式鎖這玩意有什麼方案?”“Redis快取啊!” “小明,公司系統響應如蝸牛,咋整?”“Redis快取啊!”

本著研究的精神,我們來分析下小明的第四個問題。

 

準備:

Idea2019.03/Gradle6.0.1/Maven3.6.3/JDK11.0.4/Lombok0.28/SpringBoot2.2.4RELEASE/mybatisPlus3.3.0/Soul2.1.2/

Dubbo2.7.5/Druid1.2.21/Zookeeper3.5.5/Mysql8.0.11/Vue2.5/Redis3.2

難度: 新手--戰士--老兵--大師

目標:

  1. Spring優雅整合Redis做資料庫快取

步驟:

為了遇見各種問題,同時保持時效性,我儘量使用最新的軟體版本。原始碼地址:https://github.com/xiexiaobiao/vehicle-shop-admin

1 先說結論

Redis快取不是金彈,若系統DB毫無壓力,系統性能瓶頸不在DB上,不建議強加快取層!

  1. 增加業務複雜度:同一快取必須被全部相關方法所覆蓋,如訂單快取,只要涉及到訂單資料更新的方法都要進行快取邏輯處理。

    同時,KV儲存時,因各方法返回的型別不同,這樣就需要多個快取池,但各方法後臺的資料又存在關聯,往往導致一個方法需

    要處理關聯的多個快取,從而形成網狀處理邏輯。

    2. 存在併發問題:快取沒有鎖機制,B執行緒進行DB更新,同時A執行緒請求資料,快取中存在即返回,但B執行緒還未更新到快取,導

    致快取與DB不一致;或者A執行緒B執行緒都進行DB更新,但寫入快取的順序發生顛倒,也會導致快取與DB不一致,請看官君想想如何解決;

    3.記憶體消耗:小資料量可直接全部進記憶體,但海量資料不可能全部直接進入Redis,機器吃不消!可考慮只快取DB資料索引,然後配合

    “布隆過濾器”攔截無效請求,有效請求再去DB查詢;

    4. 快取位置:快取註解的方法,執行時序上應儘量靠近DB,遠離前端,如放dao層,請看官君思考下為啥。

適用場景:1.確認DB為系統性能瓶頸,2.資料內容穩定,低頻更新,高頻查詢,如歷史訂單資料;3.熱點資料,如新上市商品;

2 步驟

2.1 原理

這裡我說的是註解模式,有四個註解,SpringCache快取原理即註解+攔截器 org.springframework.cache.interceptor.CacheInterceptor 對方法進行攔截處理:

 

@Cacheable:可標記在類或方法上。標記在類上則快取該類所有方法的返回值。請求方法時,先在快取進行key匹配,存在則直接取快取資料並返回。主要引數表:

 

@CacheEvict:從快取中移除相應資料。主要引數表:

 

@CachePut:方法支援快取功能。與@Cacheable不同的是使用@CachePut標註的方法在執行前不會去檢查快取中是否存在之前執行過的結果,

而是每次都會執行該方法,並將執行結果以鍵值對的形式存入指定的快取中。主要引數表:

 

@Caching: 多個Cache註解組合使用,比如新增使用者時,同時要刪除其他快取,並更新使用者資訊快取,即以上三個註解的集合。

2.2 編碼

專案有五個微服務,我僅改造了customer服務模組:

引入依賴,build.gradle檔案:

 

Redis配置項,resources/config/application-dev.yml檔案:

 

檔案: com.biao.shop.customer.conf.RedisConf

@Configuration
@EnableCaching
public class RedisConf {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory){
        return RedisCacheManager.create(redisConnectionFactory);
    }

    @Bean
    public CacheManager cacheManager() {
        // configure and return an implementation of Spring's CacheManager SPI
         SimpleCacheManager cacheManager = new SimpleCacheManager();
         cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("default")));
         return cacheManager;
    }

    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
        RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        // 設定key的序列化器
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // 設定value的序列化器,使用Jackson 2,將物件序列化為JSON
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =
                new Jackson2JsonRedisSerializer(Object.class);
        // json轉物件類,不設定,預設的會將json轉成hashmap
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(mapper);
        return redisTemplate;
    }
}
以上程式碼解析:1.宣告快取管理器CacheManager,會建立一個切面(aspect)並觸發Spring快取註解的切點,根據類或者方法所使用的註解以及快取的狀態,

這個切面會從快取中獲取資料,將資料新增到快取之中或者從快取中移除某個值 2. RedisTemplate即為Redis聯結器,實際上即為jedis客戶端。

 

檔案: com.biao.shop.customer.impl.ShopClientServiceImpl

@org.springframework.stereotype.Service
@Slf4j
public class ShopClientServiceImpl extends ServiceImpl<ShopClientDao, ShopClientEntity> implements ShopClientService {

    private final Logger logger = LoggerFactory.getLogger(ShopClientServiceImpl.class);

    private ShopClientDao shopClientDao;

    @Autowired
    public ShopClientServiceImpl(ShopClientDao shopClientDao){
        this.shopClientDao = shopClientDao;
    }

    @Override
    public String getMaxClientUuId() {
        return shopClientDao.selectList(new LambdaQueryWrapper<ShopClientEntity>()
                .isNotNull(ShopClientEntity::getClientUuid).orderByDesc(ShopClientEntity::getClientUuid))
                .stream().limit(1).collect(Collectors.toList())
                .get(0).getClientUuid();
    }

    @Override
    @Caching(put = @CachePut(cacheNames = {"shopClient"},key = "#root.args[0].clientUuid"),
            evict = @CacheEvict(cacheNames = {"shopClientPage","shopClientPlateList","shopClientList"},allEntries = true))
    public int createClient(ShopClientEntity clientEntity) {
        clientEntity.setGenerateDate(LocalDateTime.now());
        return shopClientDao.insert(clientEntity);
    }

    /** */
    @Override
    @CacheEvict(cacheNames = {"shopClient","shopClientPage","shopClientPlateList","shopClientList"},allEntries = true)
    public int deleteBatchById(Collection<Integer> ids) {
        logger.info("deleteBatchById 刪除Redis快取");
        return shopClientDao.deleteBatchIds(ids);
    }

    @Override
    @CacheEvict(cacheNames = {"shopClient","shopClientPage","shopClientPlateList","shopClientList"},allEntries = true)
    public int deleteById(int id) {
        logger.info("deleteById 刪除Redis快取");
        return shopClientDao.deleteById(id);
    }

    @Override
    @Caching(evict = {@CacheEvict(cacheNames = "shopClient",key = "#root.args[0]"),
            @CacheEvict(cacheNames = {"shopClientPage","shopClientPlateList","shopClientList"},allEntries = true)})
    public int deleteByUUid(String uuid) {
        logger.info("deleteByUUid 刪除Redis快取");
        QueryWrapper<ShopClientEntity> qw = new QueryWrapper<>();
        qw.eq(true,"uuid",uuid);
        return shopClientDao.delete(qw);
    }

    @Override
    @Caching(put = @CachePut(cacheNames = "shopClient",key = "#root.args[0].clientUuid"),
            evict = @CacheEvict(cacheNames = {"shopClientPage","shopClientPlateList","shopClientList"},allEntries = true))
    public int updateClient(ShopClientEntity clientEntity) {
        logger.info("updateClient 更新Redis快取");
        clientEntity.setModifyDate(LocalDateTime.now());
        return shopClientDao.updateById(clientEntity);
    }


    @Override
    @CacheEvict(cacheNames = {"shopClient","shopClientPage","shopClientPlateList","shopClientList"},allEntries = true)
    public int addPoint(String uuid,int pointToAdd) {
        ShopClientEntity clientEntity =  this.queryByUuId(uuid);
        log.debug(clientEntity.toString());
        clientEntity.setPoint(Objects.isNull(clientEntity.getPoint()) ? 0 : clientEntity.getPoint() + pointToAdd);
        return shopClientDao.updateById(clientEntity);
    }

    @Override
    @Cacheable(cacheNames = "shopClient",key = "#root.args[0]")
    public ShopClientEntity queryByUuId(String uuid) {
        logger.info("queryByUuId 未使用Redis快取");
        QueryWrapper<ShopClientEntity> qw = new QueryWrapper<>();
        qw.eq(true,"client_uuid",uuid);
        return shopClientDao.selectOne(qw);
    }

    @Override
    @Cacheable(cacheNames = "shopClientById",key = "#root.args[0]")
    public ShopClientEntity queryById(int id) {
        logger.info("queryById 未使用Redis快取");
        return shopClientDao.selectById(id);
    }

    @Override
    @Cacheable(cacheNames = "shopClientPage")
    public PageInfo<ShopClientEntity> listClient(Integer current, Integer size, String clientUuid, String name,
                                                 String vehiclePlate, String phone) {
        logger.info("listClient 未使用Redis快取");
        QueryWrapper<ShopClientEntity> qw = new QueryWrapper<>();
        Map<String,Object> map = new HashMap<>(4);
        map.put("client_uuid",clientUuid);
        map.put("vehicle_plate",vehiclePlate);
        map.put("phone",phone);
        // "name" 模糊匹配
        boolean valid = Objects.isNull(name);
        qw.allEq(true,map,false).like(!valid,"client_name",name);
        PageHelper.startPage(current,size);
        List<ShopClientEntity> clientEntities = shopClientDao.selectList(qw);
        return  PageInfo.of(clientEntities);
    }

    // java Stream
    @Override
    @Cacheable(cacheNames = "shopClientPlateList")
    public List<String> listPlate() {
        logger.info("listPlate 未使用Redis快取");
        List<ShopClientEntity> clientEntities =
                shopClientDao.selectList(new LambdaQueryWrapper<ShopClientEntity>().isNotNull(ShopClientEntity::getVehiclePlate));
        return clientEntities.stream().map(ShopClientEntity::getVehiclePlate).collect(Collectors.toList());
    }

    @Override
    @Cacheable(cacheNames = "shopClientList",key = "#root.args[0].toString()")
    public List<ShopClientEntity> listByClientDto(ClientQueryDTO clientQueryDTO) {
        logger.info("listByClientDto 未使用Redis快取");
        QueryWrapper<ShopClientEntity> qw = new QueryWrapper<>();
        boolean phoneFlag = Objects.isNull(clientQueryDTO.getPhone());
        boolean clientNameFlag = Objects.isNull(clientQueryDTO.getClientName());
        boolean vehicleSeriesFlag = Objects.isNull(clientQueryDTO.getVehicleSeries());
        boolean vehiclePlateFlag = Objects.isNull(clientQueryDTO.getVehiclePlate());
        //如有null的條件直接不參與查詢
        qw.eq(!phoneFlag,"phone",clientQueryDTO.getPhone())
                .like(!clientNameFlag,"client_name",clientQueryDTO.getClientName())
                .like(!vehicleSeriesFlag,"vehicle_plate",clientQueryDTO.getVehiclePlate())
                .like(!vehiclePlateFlag,"vehicle_series",clientQueryDTO.getVehicleSeries());
        return shopClientDao.selectList(qw);
    }
}
以上程式碼解析:

1. 因方法返回型別不同,故建立了5個快取  2. 使用SpEL表示式#root.args[0]取得方法第一個引數,使用#result取得返回物件,

用於構造key  3. 對於@Cacheable不能使用#result返回物件做key值,如queryById(int id)方法,會導致NPE,,因為此註解將在方法執行前先

進入快取匹配,而#result則是在方法執行後計算  4. @Caching註解可一次集合多個註解,如deleteByUUid(String uuid)方法,刪除一個使用者記錄,

需同時進行更新shopClient,並清空其他幾個快取。

2.3 測試

執行起來整個專案,啟動順序:souladmin -> soulbootstrap -> zookeeper -> authority -> customer -> stock -> order -> business -> vue前端 ,

進入後端管理頁: 按頁瀏覽客戶資訊,分別點選頁籤:

 

可以看到快取shopClientPage快取了4項資料,key值即為方法的引數組合,再去點選頁籤,則系統後臺無DB請求記錄輸出,說明直接使用了快取:

 

編輯客戶資訊,我隨意打開了兩個:

 

可以看到快取shopClientById增加了兩個物件,再去點選編輯,則系統後臺無DB查詢記錄輸出,說明直接使用了快取:

 

按條件查詢客戶:

 

可以看到快取shopClientPage增加一項,因為key值不一樣,故獨立為一項快取資料,多次點查詢,則系統後臺無DB查詢SQL輸出,說明直接使用了快取:

 

新增客戶:

 

可以看到shopClientPage快取將會被清空,同時增加一個shopClient快取的物件,即同時進行了多個快取池操作:


 

問題解答:

前面說到的兩個問題:

1.多執行緒問題,可配合DB事務機制,進行快取延時雙刪,每次DB更新前,先刪除快取中物件,更新後,再去刪除一次快取中物件,

2.快取方法位置問題,按照前端到後端的“倒金字塔模型”,越靠近前端,快取資料物件被其他業務邏輯更新的可能性越大,靠近DB,能儘量保證每次DB的更新都能被快取邏輯感知。

全文完!


我的其他文章:

1 SOFARPC模式下的Consul註冊中心

2 八種控制執行緒順序的方法

3 移動應用APP購物車(店鋪系列二)

4 H5開發移動應用APP(店鋪系列一)

5 阿里雲平臺OSS物件儲存

 

只寫原創,敬請關注 

&n