「小明,多系統的session共享,怎麼處理?」「Redis緩存啊!」 「小明,我想實現一個簡單的消息隊列?」「Redis緩存啊!」前端
「小明,分佈式鎖這玩意有什麼方案?」「Redis緩存啊!」 「小明,公司系統響應如蝸牛,咋整?」「Redis緩存啊!」vue
本着研究的精神,咱們來分析下小明的第四個問題。java
準備:
Idea2019.03/Gradle6.0.1/Maven3.6.3/JDK11.0.4/Lombok0.28/SpringBoot2.2.4RELEASE/mybatisPlus3.3.0/Soul2.1.2/git
Dubbo2.7.5/Druid1.2.21/Zookeeper3.5.5/Mysql8.0.11/Vue2.5/Redis3.2github
難度:新手-- 戰士 --老兵--大師redis
目標:
Spring優雅整合Redis作數據庫緩存
步驟:
爲了碰見各類問題,同時保持時效性,我儘可能使用最新的軟件版本。源碼地址: https://github.com/xiexiaobiao/vehicle-shop-admin
1 先說結論
Redis緩存不是金彈,若系統DB毫無壓力,系統性能瓶頸不在DB上,不建議強加緩存層!spring
增長業務複雜度:同一緩存必須被所有相關方法所覆蓋,如訂單緩存,只要涉及到訂單數據更新的方法都要進行緩存邏輯處理。
同時,KV存儲時,因各方法返回的類型不一樣,這樣就須要多個緩存池,但各方法後臺的數據又存在關聯,每每致使一個方法需sql
要處理關聯的多個緩存,從而造成網狀處理邏輯。數據庫
致緩存與DB不一致;或者A線程B線程都進行DB更新,但寫入緩存的順序發生顛倒,也會致使緩存與DB不一致,請看官君想一想如何解決;json
3.內存消耗:小數據量可直接所有進內存,但海量數據不可能所有直接進入Redis,機器吃不消!可考慮只緩存DB數據索引,而後配合
「布隆過濾器」攔截無效請求,有效請求再去DB查詢;
適用場景 :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
@Configurationbr/>@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.Servicebr/>@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); }
}
以上代碼解析:
用於構造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的更新都能被緩存邏輯感知。
全文完!