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 が中国に進出

推薦する

革新的なSEO最適化戦術がウェブサイト開発に貢献

SEO最適化には、受注-最適化-完了の3つのステップがあります。熟練した操作に少し戸惑いを感じました...

#米国 VPS# archhosting - 年額 15 ドル / メモリ 512 MB / ハードディスク 10 GB / トラフィック 250 GB

archhosting は 2017 年に設立された新しいホスティング プロバイダーです。主な事業は...

モバイル インターネットの 7 つの黄金律: トラフィックが王様、ユーザー第一

1. トラフィックこそが王様、ユーザーが至高モバイル インターネットの時代では、トラフィックは力であ...

パブリック クラウド セキュリティ: 4 つの誤解と現実

新しいテクノロジーが登場すると、必ず何らかの誇大宣伝が行われ、さまざまな用語、誤解、神話が生まれます...

推奨: 予算ノード - 12 ドル/年/50g 保護/256m メモリ/40g ハードディスク/ロサンゼルス

budgetnode VPS の驚きは、同じ価格でハードディスクが 2 台になり、各ユーザーが購入で...

Alibaba Cloud で効率的で安定した安全な Echsop モールの Web サイトを展開する方法

2018年最もホットなプロジェクト:テレマーケティングロボットがあなたの参加を待っていますEcsho...

短縮URLはSEOには適していません

短縮 URL については、Weibo の始まりの頃から存在していたため、私にとっては馴染みのないもの...

ステーションBには1つの大きな矛盾がある

資本市場はビリビリの前四半期の財務報告に失望した。ユーザー規模(MAUとDAU)は前月比で増加しなか...

ワイルドカードドメイン名でウェブサイトが解決される問題を解決する代替方法

編集者は昨年 11 月 25 日に「ウェブサイトがワイルドカード ドメイン名で悪意を持って解決された...

ウェブサイトに 404 ページが必要なのはなぜですか?

2018年最もホットなプロジェクト:テレマーケティングロボットがあなたの参加を待っています404 ペ...

ウェブサイトの内部リンクの構築と機能分析

内部リンクとは、同じ Web サイトのドメイン名の下にあるコンテンツ ページ間のリンクを指します。た...

COVID-19パンデミック下での現地生産においてエッジコンピューティングが重要な役割を果たす

[[359008]] COVID-19パンデミックにより、製造業者が現地生産と流通へとシフトするにつ...

クラウド コンピューティング サービスの利点、欠点、種類

クラウド コンピューティングは、現代のビジネスに柔軟性、効率性、拡張性、セキュリティ、コラボレーショ...

クラウド アプリケーションの左右の腕: 仮想化とセキュリティ

仮想化とは、オペレーティング システム、コンピュータ システム、ストレージ デバイス、ネットワーク ...

自動車ブランドマーケティングについて簡単に解説します!

この記事では、自動車マーケティングに関する私の見解を述べます。私は広告会社で働いており、当初は日用消...