天天看點

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

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

目标:

Spring優雅整合Redis做資料庫緩存

步驟:

為了遇見各種問題,同時保持時效性,我盡量使用最新的軟體版本。源碼位址:

https://github.com/xiexiaobiao/vehicle-shop-admin

1 先說結論

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

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

同時,KV存儲時,因各方法傳回的類型不同,這樣就需要多個緩存池,但各方法背景的資料又存在關聯,往往導緻一個方法需

要處理關聯的多個緩存,進而形成網狀處理邏輯。

  1. 存在并發問題:緩存沒有鎖機制,B線程進行DB更新,同時A線程請求資料,緩存中存在即傳回,但B線程還未更新到緩存,導

緻緩存與DB不一緻;或者A線程B線程都進行DB更新,但寫入緩存的順序發生颠倒,也會導緻緩存與DB不一緻,請看官君想想如何解決;

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

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

  1. 緩存位置:緩存注解的方法,執行時序上應盡量靠近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 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的更新都能被緩存邏輯感覺。

全文完!

原文位址

https://www.cnblogs.com/xxbiao/p/12593525.html