分散式鎖結合SpringCache

阿波羅的手發表於2020-10-17

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

 

相關文章