【Redis】相互排他ロックと論理的有効期限の2つの側面からキャッシュ破綻の問題を解決

序文

大変な時こそ、万一の備えを

1.キャッシュブレイクダウンとは

率直に言えば、非常に頻繁に使用されるキーが突然失敗し、リクエストがキャッシュを逃すと、無数のリクエストがデータベースに落ち、データベースを一瞬で引きずります。そして、そのようなキーはホットキーとも呼ばれます!

ここに画像の説明を挿入
キャッシュの内訳を解決したい場合、特定の期間に非常に多くのリクエストのスレッドがデータベースにアクセスすることを許可してはならないことが直感的にわかります。
これに基づいて、データベースへのアクセスを制限するための 2 つのソリューションがあります。

2.ミューテックスに基づいてキャッシュの内訳を解決する

アクセス頻度の高いidクエリインターフェースでは、キャッシュの破綻が発生する可能性があります. 以下は、ミューテックスを使用して問題を解決します.
ここに画像の説明を挿入
従来、idクエリ情報インターフェースは、クエリされた情報をキャッシュに書き込むのが一般的でした. 対応する処理を行います. 同時実行の場合、ホット キーが失敗すると、多数のリクエストがデータベースに直接ヒットし、キャッシュを再構築しようとします。これにより、データベースが停止し、サービスが中断される可能性があります。このような場合、キャッシュがミスした場合、業務上キャッシュがヒットするかどうか、つまり「余分な」リクエストがデータベースにアクセスするかどうかを判断した後のステップが最適な処理ポイントです。
他のスレッドからの要求はデータベースにアクセスできますか? データベースにはいつアクセスできますか?
他のスレッドはデータベースにアクセスできますか? ——
ロック、データベースにアクセスするためのロックしか持てない場合は? ——メイン スレッドがロックを解放するのを待ち
ます。ロックを取得できない場合、他のスレッドはどうすればよいですか。——寝て、後で戻ってくる

複数のスレッドが並列している場合に 1 つのスレッドのみがロックを取得できることを実現するために、Redis
ここに画像の説明を挿入
に付属の setnx を使用して、キーが存在しないときに書き込み操作を実行できるようにし、書き込み操作を実行できないようにすることができます。これは、ロックを取得した最初のスレッドのみが並行条件下で書き込み可能であることを完全に保証し、ロックを解放せずに書き込みを終了すると、他のスレッドは書き込みできなくなります。
入手方法は?Key-Value を書き込んで、
それを解放する方法は? デルロックからキーを削除します(通常は有効期間を設定して、長時間解放しないという状況を回避します)

このようにして、この条件に基づいて 2 つのメソッドをカプセル化できます。1 つはキーを書き込んでロックを取得しようとし、もう 1 つはキーを削除してロックを解放します。このような:

/**
 * 尝试获取锁
 *
 * @param key
 * @return
 */
private boolean tryLock(String key) {
    
    
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
}

/**
 * 释放锁
 *
 * @param key
 */
private void unlock(String key) {
    
    
    stringRedisTemplate.delete(key);
}

並行して、他のスレッドがロックを取得したいときはいつでも、キャッシュにアクセスするために独自のキーを tryLock() メソッドに書き込む必要があります. setIfAbsent() が false を返す場合、スレッドがキャッシュ データを更新中であり、ロックが保持されていることを意味しますリリースされていません。true が返された場合、現在のスレッドがロックを取得し、キャッシュにアクセスしたり、キャッシュを操作したりできることを意味します。
次の一般的なクエリ シナリオでは、コードを使用してミューテックスを実装し、キャッシュの内訳を解決します。
ここに画像の説明を挿入

    /**
     * 解决缓存击穿的互斥锁
     * @param id
     * @return
     */
    public Shop queryWithMutex(Long id) {
    
    
        String key = CACHE_SHOP_KEY + id;
        //1.从Redis查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);  //JSON格式
        //2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
    
     //不为空就返回 此工具类API会判断""为false
            //存在则直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            //return Result.ok(shop);
            return shop;
        }
        //3.判断是否为空值
        if (shopJson != null) {
    
    
            //返回一个空值
            return null;
        }
        //4.缓存重建
        //4.1获得互斥锁
        String lockKey = "lock:shop"+id;
        Shop shopById=null;
        try {
    
    
            boolean isLock = tryLock(lockKey);
            //4.2判断是否获取成功
            if (!isLock){
    
    
                //4.3失败,则休眠并重试
                Thread.sleep(50);
               return queryWithMutex(id);
            }
            //4.4成功,根据id查询数据库
            shopById = getById(id);
            //5.不存在则返回错误
            if (shopById == null) {
    
    
                //将空值写入Redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                //return Result.fail("暂无该商铺信息");
                return null;
            }
            //6.存在,写入Redis
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shopById), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
    
    
            throw new RuntimeException(e);
        } finally {
    
    
            //7.释放互斥锁
            unlock(lockKey);
        }

        return shopById;
    }

3. 論理的な有効期限に基づいてキャッシュの内訳を解決する

論理的有効期限は実際の有効期限ではありません. 対応するキーに TTL を設定する必要はありませんが、ビジネスロジックを使用して「有効期限」と同様の効果を実現します. その本質は、データベースに落ちるリクエストの数を制限することです! ただし、可用性を確保するために一貫性を犠牲にしたり、以前のビジネスのインターフェイスを犠牲にしたり、論理的な有効期限を使用してキャッシュの故障を解決したりすることが前提です。
ここに画像の説明を挿入
このように、キャッシュには基本的にヒットします。キャッシュ、および for キーセットは事前にすべて選択されています. ミスがある場合は、基本的に彼が選択に含まれていないと判断できるので、直接エラーメッセージを返すことができます. ヒットした場合は、まず論理時間が経過したかどうかを判断し、その結果に基づいてキャッシュを再構築するかどうかを決定する必要があります。ここでの論理的な時間は、データベースに分類される「ゲートウェイ」への多数のリクエストを減らすことです

上記の段落を読んだ後、誰もがまだ混乱していると思います。有効期限が設定されていないのに、なぜ論理的な有効期限を判断する必要があるのでしょうか? 有効期限が切れているかどうかの問題が依然として存在するのはなぜですか?
実際、ここでのいわゆる論理有効期限はクラスの属性フィールドにすぎません. Redis にはまったく上がっていませんが、クエリオブジェクトの判断を支援するために使用されるキャッシュレベルに上がっています. つまり. 、いわゆる有効期限はキャッシュされたデータから分離されています. はい、キャッシュの有効期限の問題はまったくなく、当然データベースは圧迫されません.

コードステージ:

可能な限りオープンとクローズの原則に準拠するために、元のエンティティの属性は継承ではなく、組み合わせによって拡張されます。

@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;  //这里用Object是因为以后可能还要缓存别的数据
}

更新ロジックの有効期限とキャッシュされたデータをシミュレートするメソッドをカプセル化して、テスト クラスで実行し、データと熱の効果を実現します。

/**
 * 添加逻辑过期时间
 *
 * @param id
 * @param expireTime
 */
public void saveShopRedis(Long id, Long expireTime) {
    
    
    //查询店铺信息
    Shop shop = getById(id);
    //封装逻辑过期时间
    RedisData redisData = new RedisData();
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));
    //将封装过期时间和商铺数据的对象写入Redis
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

クエリ インターフェイス:

/**
 * 逻辑过期解决缓存击穿
 *
 * @param id
 * @return
 */
public Shop queryWithLogicalExpire(Long id) throws InterruptedException {
    
    
    String key = CACHE_SHOP_KEY + id;
    Thread.sleep(200);
    //1.从Redis查询缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);  //JSON格式
    //2.判断是否存在
    if (StrUtil.isBlank(shopJson)) {
    
    
        //不存在则直接返回
        return null;
    }
    //3.判断是否为空值
    if (shopJson != null) {
    
    
        //返回一个空值
        //return Result.fail("店铺不存在!");
        return null;
    }
    //4.命中
    //4.1将JSON反序列化为对象
    RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
    Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
    LocalDateTime expireTime = redisData.getExpireTime();
    //4.2判断是否过期
    if (expireTime.isAfter(LocalDateTime.now())) {
    
    
        //5.未过期则返回店铺信息
        return shop;
    }
    //6.过期则缓存重建
    //6.1获取互斥锁
    String LockKey = LOCK_SHOP_KEY + id;
    boolean isLock = tryLock(LockKey);
    //6.2判断是否成功获得锁
    if (isLock) {
    
    
        //6.3成功,开启独立线程,实现缓存重建
        CACHE_REBUILD_EXECUTOR.submit(() -> {
    
    
            try {
    
    
                //重建缓存
                this.saveShop2Redis(id, 20L);

            } catch (Exception e) {
    
    
                throw new RuntimeException(e);
            } finally {
    
    
                //释放锁
                unlock(LockKey);
            }
        });
    }
    //6.4返回商铺信息
    return shop;
}

4. インターフェース試験

インターフェイス テストは、APIfox を介して同時シナリオをシミュレートすることによって実行され、平均所要時間は依然として非常に短く、コンソール ログはデータベースに頻繁にアクセスしていないことが
ここに画像の説明を挿入
わかります。スレッド、jmeter を使用して 1550 スレッドでテストしました。しばらくすると、インターフェイスはまだ実行できます。
ここに画像の説明を挿入
インターフェイスのパフォーマンスは同時シナリオで悪くないようで、QPS もかなり理想的です

5.両者の比較

ミューテックス メソッドのコード レベルはより単純であり、ロックを操作するためにカプセル化する必要があるのは 2 つの単純なメソッドのみであることがわかります。論理的な期限切れメソッドはより複雑であり、追加のエンティティ クラスを追加する必要があります。メソッドをカプセル化した後、テスト クラスでデータのウォームアップをシミュレートする必要があります。
対照的に、前者は追加のメモリを消費せず (新しいスレッドを開かず)、強力なデータ整合性を備えていますが、スレッドは待機する必要があり、パフォーマンスが低下する可能性があり、デッドロックのリスクがあります。後者は、追加のメモリ消費で新しいスレッドを開き、一貫性を犠牲にして可用性を確保しますが、パフォーマンスは待機なしで向上します。

おすすめ

転載: blog.csdn.net/weixin_57535055/article/details/128572301