1、高併發快取失效問題:
快取穿透:
指查詢一個一定不存在的資料,由於快取不命中導致去查詢資料庫,但資料庫也無此記錄,我們沒有將此次查詢的null寫入快取,導致這個不存在的資料每次請求都要到儲存層進行查詢,失去了快取的意義;
風險:利用不存在的資料進行攻擊讓資料庫壓力增大最終崩潰;
解決:對不存在的資料進行快取並加入短暫的過期時間;
快取雪崩:
快取雪崩是指我們在設定快取時key採用相同的過期時間,導致快取在某一個時刻同時失效,請求全部轉發到DB,DB瞬間壓力過重雪崩;
解決:原有的失效時間基礎上增加一個隨機值;
快取擊穿:
對於一些設定過期時間的key,如果這些key會在某個時間被高併發地訪問,是一種非常“熱點”的資料;如果這個key在大量請求同時進來前正好失效,那麼所有對這個key的資料查詢都落在db,我們稱之為快取擊穿
解決:加鎖。大量併發情況下只讓一個人去查,其他人等到,查到資料後釋放鎖,其他人獲取到鎖後先查快取,這樣就不會出現大量訪問DB的情況。
2、加鎖解決擊穿問題
2.1、加本地鎖
“確認快取”與“查詢資料庫”完成後才釋放鎖,圖示:
改進後(將“確認快取”、“查資料庫”、“結果放入資料庫“都放入鎖中):
程式碼部分:
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithLocalLock() { //加鎖,只要是同一把鎖就鎖柱所有執行緒,例如:100w個請求用同意一把鎖 synchronized (this) { //得到鎖後再去緩衝中檢視,如果快取沒有再去檢視,有則直接取快取。但是使用“this”只能鎖住本地服務,所以要使用分散式鎖 return getDataFromDb(); } }
private Map<String, List<Catalog2Vo>> getDataFromDb() { //得到鎖後再去緩衝中檢視,如果快取沒有再去檢視,有則直接取快取。但是使用“this”只能鎖住本地服務,所以要使用分散式鎖 String catalogJSON = redisTemplate.opsForValue().get("catalogJSON"); if (!StringUtils.isEmpty(catalogJSON)) { //如果快取不為空直接返回 Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() { }); return result; } System.out.println("查了資料庫"); /* Map<String, List<Catalog2Vo>> catelogJson = (Map<String, List<Catalog2Vo>>) cache.get("catelogJson"); if (cache.get("catelogJson") == null){ }*/ List<CategoryEntity> selectList = baseMapper.selectList(null); //1、先查出所有分類 /** * 一級結構: * id:[ * {二級內容} * {二級內容} * ] */ //List<CategoryEntity> level1Categorys = getLevel1Categorys(); List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L); //2、封裝資料,二級結構 /** * "catalog1Id": * "catalog3List":[三級內容] * "id": "",(二級id) * "name": ""(二級名字) * @return */ Map<String, List<Catalog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> { //1、查到2級分類 //List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId())); List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId()); List<Catalog2Vo> catalog2Vos = null; if (categoryEntities != null) { catalog2Vos = categoryEntities.stream().map(l2 -> { Catalog2Vo catalog2Vo = new Catalog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName()); /** * 三級內容: * { * "catalog2Id": "",(二級id) * "id": "",(三級id) * "name": "商務休閒鞋"(三級名字) * }, */ //List<CategoryEntity> level3Catalog = getParent_cid(l2); List<CategoryEntity> level3Catalog = getParent_cid(selectList, l2.getCatId()); if (level3Catalog != null) { List<Catalog2Vo.Catalog3Vo> collect = level3Catalog.stream().map(l3 -> { Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName()); return catalog3Vo; }).collect(Collectors.toList()); catalog2Vo.setCatalog3List(collect); } return catalog2Vo; }).collect(Collectors.toList()); } return catalog2Vos; })); //3、放入快取中 String s = JSON.toJSONString(parent_cid); //1, TimeUnit.DAYS 空結果快取:解決快取穿透;設計過期時間:解決雪崩 redisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS); return parent_cid; }
3、分散式鎖原理與使用
由於本地鎖只能管理本地服務的事務,所以在分散式微服務中引進了分散式鎖
1、分散式鎖演進:
1.1、分散式鎖階段一:
1.2、分散式鎖階段二:
1.3、 分散式鎖階段三:
1.4、分散式鎖階段四:
1.5、演進程式碼:
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() { //1、佔分布式鎖,去redis佔坑同時加鎖成功(原子性),設定過期時間(30秒)。假設過程出問題直接過期不會發生死鎖,改進第階段1和階段2,使用setIfAbsent原子命令將獲取鎖和過期時間同時設定 String uuid = UUID.randomUUID().toString(); Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS); if (lock) { System.out.println("獲取分散式鎖成功"); Map<String, List<Catalog2Vo>> dataFromDb; try { //執行業務 dataFromDb = getDataFromDb(); } finally { //刪除鎖.redisTemplate.opsForValue().get("lock")獲取需要時間釋放鎖,存在“不可重複讀”,所以獲取後刪除過程也要要求原子性; /* String lockValue = redisTemplate.opsForValue().get("lock"); if (uuid.equals(lockValue)){ redisTemplate.delete("lock"); }*/ //使用指令碼,改進階段4 String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"; // public <T> T execute(RedisScript<T> script, List<K> keys, Object... args)
// 採用隨機的uuid值,改進階段3 Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid); } return getDataFromDb(); } else { System.out.println("獲取分散式鎖失敗"); try { Thread.sleep(200); } catch (Exception e) { } //加鎖失敗,設定時間進行重試 return getCatalogJsonFromDbWithRedisLock();//自旋方式 } }
4、Redisson實踐:
1、Redisson官方文件:
https://github.com/redisson/redisson/wiki/Table-of-Content
2、Redisson可重用鎖:
@ResponseBody @GetMapping("/hello") public String hello(){ RLock lock= redisson.getLock("my-lock"); lock.lock(10, TimeUnit.SECONDS);//這種方法不會自動續期
//Redisson功能: //1、可以鎖的自動續期,如果業務超長,因此不用擔心業務時間過長自動刪鎖 //2、枷鎖業務只要執行完成,就不會給當業務續期。預設是30s後自動刪鎖 try{ //模擬業務超長執行 System.out.println("加鎖成功"+Thread.currentThread().getId()); Thread.sleep(30000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } return "hello"; }
3、快取資料與資料庫保持一致性:
/** * @return 快取一致性 * 1、雙寫模式:資料庫修改時修改快取資料,高併發情況下對資料庫進行修改容易出現髒資料 * 2、失效模式:資料庫更新時刪除快取資料,等待下次查詢進行更新快取 */ public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedissonLock() { //1、佔分布式鎖,去redis佔坑,鎖的粒度越大,越細越快,鎖的名字一樣 RLock lock = redisson.getLock("CatalogJson-lock"); lock.lock(); Map<String, List<Catalog2Vo>> dataFromDb; try { //執行程式碼 dataFromDb = getDataFromDb(); } finally { lock.unlock(); } return getDataFromDb(); }
3.1、雙寫模式:
3.2、失效模式(採用):
5、SpringCache的使用
1、主啟動類以及基本註解詳解:
/** * * @Cacheable:觸發儲存快取。 * @CacheEvict:觸發刪除快取。 * @CachePut:更新快取,而不會干擾方法的執行。 * @Caching:重新組合多個快取操作。 * @CacheConfig:在類級別共享一些與快取相關的常見設定。 * * 開啟快取@EnableCaching * * 原理:CacheAutoConfiguration->RedisCacheConfiguration->自動配置了RedisCacheManager->初始化所有快取->每個快取決定用什麼配置 * ->進行判斷,如果有自定義配置則按自定義配置,如果沒有則按預設->想改快取配置,只需要給容器中放一個RedisCacheCpnfiguration */ @EnableRedisHttpSession @EnableCaching @EnableFeignClients(basePackages = "com.atguigu.gulimall.product.feign") @EnableDiscoveryClient @MapperScan(basePackages = "com.atguigu.gulimall.product.dao") @SpringBootApplication public class GulimallProductApplication { public static void main(String[] args) { SpringApplication.run(GulimallProductApplication.class, args); } }
2、註解應用:
/** * 級聯更新所有關聯的資料 * @CacheEvict:失效模式 * 1、同時進行多種快取操作 * 2、指定刪除某個分割槽下所有資料:@CacheEvict(value = "category", allEntries = true) * 3、儲存同一型別資料、都可以指定同一個分割槽 * */ /*方法一:@Caching(evict = { @CacheEvict(value = "category", key = "'level1Categorys'"), @CacheEvict(value = "category", key = "'getCatalogJson'") })*/ //@CacheEvict一旦更改資料就清除快取 @CacheEvict(value = "category", allEntries = true)//方法二 @Transactional @Override public void updateCascade(CategoryEntity category) { this.updateById(category); categoryBrandRelationService.updateCategory(category.getCatId(), category.getName()); } /** * (1指定生成的快取使用的key:key屬性指定 * (2指定快取的資料存活時間: * (3將資料儲存為json */ //每一個快取的資料都要制定要放到那個名字的快取【快取分割槽】 //快取過期之後,如果多個執行緒同時請求對某個資料的訪問,會同時去到資料庫,導致資料庫瞬間負荷增高。 //屬性 sync 可以指示底層將快取鎖住,使只有一個執行緒可以進入計算,而其他執行緒堵塞,直到返回結果更新到快取中。 @Cacheable(cacheNames = "category", key = "'level1Categorys'", sync = true) //代表當前方法結果需要快取,如果快取中結果,有方法就不呼叫,如果沒有則呼叫方法並返回快取結果 @Override public List<CategoryEntity> getLevel1Categorys() { System.out.println("----getLevel1Categorys----"); List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0)); return categoryEntities; }
3、將redis快取資料資料儲存為json格式
//可以讀取Properties檔案 @EnableConfigurationProperties(CacheProperties.class) @EnableCaching @Configuration public class MyCacheConfig { @Bean RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); //Key和Value JSON序列化方式 config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())); config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer())); //使用預設才會讀出properties檔案中的配置,自定義時roperties檔案中的配置不生效 CacheProperties.Redis redisProperties = cacheProperties.getRedis(); if (redisProperties.getTimeToLive() != null) { config = config.entryTtl(redisProperties.getTimeToLive()); } if (redisProperties.getKeyPrefix() != null) { config = config.prefixCacheNameWith(redisProperties.getKeyPrefix()); } if (!redisProperties.isCacheNullValues()) { config = config.disableCachingNullValues(); } if (!redisProperties.isUseKeyPrefix()) { config = config.disableKeyPrefix(); } return config; } }
4、SpringCache能解決的事以及不足:
(1)讀模式
快取擊穿:查詢一個不存在的資料。解決:快取空資料( ache-null-values=true)
快取雪崩:大量key剛好過期。解決:加隨機時間和過期時間(spring.cache.redis.time-to-live=3600000)
快取擊穿:大量併發同時查詢一個正好過期的資料。解決:加鎖(@Cacheable(cacheNames = "category", key = "'level1Categorys'", sync = true))
(2)寫模式
SpringCache並未做太多處理
總結:讀多寫少,即時性、、一致性要求不搞的資料使用SpringCache