Redis 分散ロックで発生するシリアル化の問題

Redis 分散ロックで発生するシリアル化の問題

[[389269]]

シナリオの説明

最近、Redis を使用しているときに、分散ロックに似たシナリオに遭遇しました。 Redis の分散ロックの実装と同様に、ロックの解放に失敗しました。つまり、キャッシュを削除できなかったことになります。私はまた別のRedisの穴に足を踏み入れました...

これはどのような状況ですか? また、どのようにトラブルシューティングすればよいですか?

この記事では主にこれについてレビューします。

トラブルシューティング

ロックの解除に問題があるので、まずはロックを解除するコードを見てみましょう。

ロックを解除

ロックを解除するには Lua スクリプトを使用します。コードロジックと Lua スクリプトは次のとおりです。

ロック解除サンプルコード

  1. パブリックオブジェクトリリース(文字列キー、文字列値) {
  2. オブジェクトexistedValue = stringRedisTemplate.opsForValue().get( key );
  3. log.info( "キー:{}、値:{}、redis の古い値: {}" キー、値、存在する値);
  4.    
  5. DefaultRedisScript<Long> redisScript = 新しい DefaultRedisScript<>(COMPARE_AND_DELETE, Long.class);
  6. stringRedisTemplate.execute ( redisScript 、 Collections.singletonList(キー)、 値 )を返します
  7. }

ロックを解除するために使用されるLuaスクリプト

  1. redis.call( 'get' ,KEYS[1]) == ARGV[1]の場合
  2. それから 
  3. redis.call( 'del' ,KEYS[1])を返す
  4. それ以外 
  5. 0を返す
  6. 終わり;

削除スクリプトでは、まず Redis キーの古い値が取得され、入力パラメータ値と比較されます。両者が等しい場合にのみ削除されます。

解放が成功した場合、つまり Redis キャッシュが正常に削除された場合、戻り値は 1 になります。それ以外の場合は失敗し、戻り値は 0 になります。

一見すると、コードは問題ないように見えます。テストしてみましょう。

ただし、ロックを解除する必要があるため、その前にロックする必要があります。まずロックのロジックを見てみましょう。

ロック

ロックのロジックに関しては、コードに実装する方法が 2 つあります。

サンプルコード1

  1. パブリックオブジェクト lock01(文字列キー、文字列値) {
  2. log.info( "lock01, キー={}, 値={}" ,キー, 値);
  3. redisTemplate.opsForValue().setIfAbsent(キー、値、LOCKED_TIME、TimeUnit.SECONDS )を返します
  4. }

サンプルコード2

  1. パブリックオブジェクト lock02(文字列キー、文字列値) {
  2. log.info( "lock02, キー={}, 値={}" ,キー, 値);
  3. stringRedisTemplate.opsForValue().setIfAbsent(キー、値、LOCKED_TIME、TimeUnit.SECONDS )を返します
  4. }

実際、それらの違いは、前者は RedisTemplate を使用し、後者は StringRedisTemplate を使用することです。

  • Q: ちょっと待ってください…なぜテンプレートが 2 つあるのですか?
  • A: 実を言うと、穴を掘ったのは私です。 RedisTemplate を追加しました... 今考えてみると、なぜそうしたのかまだわかりません。たぶん頭が固まってしまっただけでしょう。

まずこの 2 つの方法をテストしてみてはいかがでしょうか?

試してみる

それぞれロックするには 2 つの方法を使用します。lock01 は k1 と v1、lock02 は k2 と v2 です。

k1 と k2 の値をそれぞれ見てみましょう (ツール: RDM、Redis Desktop Manager を使用)。


v1 には二重引用符がありますが、v2 には二重引用符がないことがわかります。

シリアル化の問題だと思います。 Redis の設定を見てみましょう。

Redisテンプレートの設定

ロックを見るとわかるように、k1 は RedisTemplate を使用し、k2 は StringRedisTemplate を使用しています。それぞれの構成の違いは何ですか?

RedisTemplate の構成は次のようにカスタマイズされます。

  1. @構成
  2. @AutoConfigureAfter(RedisAutoConfiguration.クラス)
  3. パブリッククラスRedisConfig {
  4. @ビーン
  5. パブリックRedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
  6. RedisTemplate<String, Object> redisTemplate = 新しい RedisTemplate<>();
  7. redisTemplate.setConnectionFactory(redisConnectionFactory);
  8.  
  9. // Jackson2JsonRedisSerializeを使用してデフォルトのシリアル化を置き換えます
  10. Jackson2JsonRedisSerializer<オブジェクト> jackson2JsonRedisSerializer
  11. = 新しい Jackson2JsonRedisSerializer<>(Object.class);
  12.  
  13. オブジェクトマッパー objectMapper = 新しいオブジェクトマッパー();
  14. objectMapper.setVisibility(PropertyAccessor. ALL 、 JsonAutoDetect.Visibility. ANY );
  15. オブジェクトマッパーのデフォルトタイピングを有効にします(ObjectMapper.DefaultTyping.NON_FINAL);
  16. objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false );
  17.  
  18. jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
  19.  
  20. //キーと値(特に値)のシリアル化ルールを設定します
  21. redisTemplate.setKeySerializer(新しい StringRedisSerializer());
  22. redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
  23. redisTemplate.afterPropertiesSet();
  24.  
  25. redisTemplateを返します
  26. }
  27. }

StringRedisTemplate の構成は SpringBoot のデフォルトの構成です。

  1. @構成
  2. @ConditionalOnClass({RedisOperations.class})
  3. @EnableConfigurationProperties({RedisProperties.class})
  4. @Import({LettuceConnectionConfiguration.class、JedisConnectionConfiguration.class}) をインポートします。
  5. パブリッククラスRedisAutoConfiguration {
  6. パブリックRedisAutoConfiguration() {
  7. }
  8.  
  9. @ビーン
  10. @条件付きで欠落しているBean
  11. パブリックStringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) はUnknownHostExceptionをスローします {
  12. StringRedisTemplate テンプレート = 新しい StringRedisTemplate();
  13. テンプレート。setConnectionFactory(redisConnectionFactory);
  14. 戻りテンプレート;
  15. }
  16. }

PS: SpringBootのバージョンは2.1.13.RELEASEです

StringRedisTemplate をクリックして確認してみましょう。

  1. パブリッククラスStringRedisTemplateはRedisTemplate<String, String>を拡張します。
  2.      
  3. パブリックStringRedisテンプレート() {
  4. // ここでシリアル化の設定に注意してください
  5. キーシリアライザーを設定します(RedisSerializer.string());
  6. RedisSerializer を setValueSerializer(RedisSerializer.string());
  7. RedisSerializer を setHashKeySerializer(RedisSerializer.string());
  8. RedisSerializer を setHashValueSerializer(RedisSerializer.string());
  9. }
  10. // ...
  11. }

シリアル化設定に注意し、方法が何であるかを確認するためにフォローアップを続けます。

  1. パブリックインターフェースRedisSerializer<T> {
  2. 静的RedisSerializer<String>文字列() {
  3. StringRedisSerializer.UTF_8を返します
  4. }
  5. }

  1. パブリッククラスStringRedisSerializerはRedisSerializer<String>を実装します。
  2. 公共 静的最終 StringRedisSerializer UTF_8 = 新しい StringRedisSerializer(StandardCharsets.UTF_8);
  3. // ...
  4. }

ご覧のとおり、StringRedisTemplate のキーと値は、デフォルトで StringRedisSerializer(StandardCharsets.UTF_8) によってシリアル化されます。

RedisTemplate のキーは StringRedisSerializer を使用し、値は Jackson2JsonRedisSerializer を使用してシリアル化されています (なぜこれを使用するかについては、ここでは書きません)。

この時点で、基本的に問題を特定できます。RedisTemplate の値のシリアル化が StringRedisTemplate と一致していません。

一貫性を保つために変更しても大丈夫でしょうか?確認してみてください。

推論を検証する

RedisTemplate の値のシリアル化メソッドを StringRedisSerializer に変更します。

  1. @構成
  2. @AutoConfigureAfter(RedisAutoConfiguration.クラス)
  3. パブリッククラスRedisConfig {
  4. @ビーン
  5. パブリックRedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
  6. RedisTemplate<String, Object> redisTemplate = 新しい RedisTemplate<>();
  7.        
  8. // ...
  9.  
  10. redisTemplate.setKeySerializer(新しい StringRedisSerializer());
  11. redisTemplate.setValueSerializer(新しい StringRedisSerializer());
  12.  
  13. // ...
  14. redisTemplateを返します
  15. }
  16. }

さらに 2 つのロック ロジックを呼び出して、k1 と k2 の値を確認します。


ご覧の通り、v1 の二重引用符はなくなり、ロックを解除するサービスは正常に削除できます。

まあ、それがここでの問題です。

2 つのシリアル化のソース コードについては、興味のある友人が引き続き学習できるので、ここでは詳しく説明しません。

まとめ

この記事で発生した問題は、主に、ロックのロックと解除に異なる Redis テンプレートが使用され、これら 2 つのテンプレートが異なるシリアル化方法を使用しているために発生し、最終的にはシリアル化によって問題が発生します。

当時は本当に不注意だったので、しばらくは理解できませんでした...

実稼働環境に関しては、まるで奈落の底に立っているか、薄氷の上を歩いているかのように、依然として細心の注意を払う必要があります。

<<:  Grafana Tempo による分散トレース

>>:  ハイパーコンバージェンス「ハードからソフトへ」: ハイブリッド クラウド向けに設計された Azure Stack HCI が中国に進出

推薦する

ウェブサイト構築の共有: ドメイン名をどこで購入したか忘れてしまった場合はどうすればいいですか?

月収10万元の起業の夢を実現するミニプログラム起業支援プランドメイン名をどこで購入したか忘れましたか...

Ali Bixuan: 開発者エコシステムは、クラウドの将来における成功と失敗の鍵となるでしょうか?

過去 1 年間、開発者エコシステムをめぐるクラウド ベンダー間の競争はますます激化しています。なぜこ...

24 の節気のドメイン名を 1 つにまとめました: 3 つの接尾辞と 72 のピンイン名

春は耕作、秋は収穫、農業はずっと中国の発展における主要産業です。農業は古代から多くの注目を集めており...

「ダブル12イベント」キーワードランキング/順位ASOソース、低価格ソース!

Apple App Store キーワードランキング リストの最適化は常に激動の時代です。リスト内の...

モバイルインターネット時代の電子書籍は恥ずかしい状況に直面

今日、Sutu.com で電子書籍リーダーに関するレポートを見ましたが、今年の電子書籍の売上は昨年の...

Baidu の外部リンク ツールが利用可能になりました。SEO 革命が到来します。

Yahoo の外部リンク クエリ ツールが閉鎖されて以来、SEO 業界は外部リンクの数を測定するため...

hostkvm 香港 CN2 VPS (Kwai Wan Data Center、HK、KW) の簡単なレビューで、hostkvm がどのように機能するかを説明します。

Hostkvm の香港 VPS には 2 つのデータセンターがあり、1 つは葵湾、もう 1 つは大埔...

ウェブサイト運営の失敗の原因を理解し、ネットワーク起業のリスクを回避する

あっという間に、2012年の旧正月が過ぎました。新年の喜びが完全に薄れる前に、多くの草の根ウェブマス...

製品に個性を与える: 感情的なデザインの要素と実例

[編集者注] この記事は、@C7210 によって Smashing Magazine から翻訳され、...

Baidu の入札キーワード品質最適化によりマーケティング パフォーマンスを向上

検索エンジンマーケティングでは、企業が自社の商品やサービスに関連するキーワードを購入し、入札すること...

Googleの検索品質は、顧客よりも広告に重点を置いているため低下している。

テンセントテクノロジーニュース(劉学同)北京時間9月6日のニュース、海外メディアの報道によると、テク...

ウェブサイトのオンラインカスタマーサービスシステムに関する個人的な意見(パート2)

「ウェブサイトオンラインカスタマーサービスシステムに関する個人的な見解(パート1)」では、ウェブサイ...

情報ストリーム広告チャネルの引用と掲載に関する 7 つの実践ガイド (2016 年更新)

この記事の内容:情報フロー広告とは?情報フロー広告入門表示フォームチャネル引用申請プロセス舞台裏の運...

業界ウェブサイト開発の限界の観点から、新しい業界サイトの発展の道筋を議論する

数年前に業界のウェブサイトを運営し、今日まで続けていれば、ある程度の成功を収めていたでしょう。現在、...

SEO最適化を行うには「勝つ」必要がある

中国文化は奥深く、長い歴史を持っています。漢字は深い文化的遺産を持ち、多くの意味を表しています。オン...