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

推薦する

クラウドネイティブデスクトップ: 仮想デスクトップの解体と再定義

デスクトップクラウドの進化と、世代から世代へと受け継がれてきたさまざまなデスクトップ管理技術は、「デ...

ウェブサイト運営ルール:ロングテールで利益が出やすい

私の友人の中には、ロングテール理論を聞いたことがある人がかなりいると思います。この理論はアメリカ人が...

Hosthatch: 年間 18 ドル、512 MB メモリ/1 コア/250 g ハード ドライブ/3 T トラフィック、ロサンゼルスを含む 5 つのデータ センター

Hosthatchは、主に欧米のデータセンター(米国ロサンゼルスとニューヨーク、スウェーデンのストッ...

iSoftStoneとTongli InterconnectがSaaSアプリケーションパートナーのHuawei Cloudへの移行を支援

【2017年10月31日、中国北京】10月26日、華為技術有限公司、艾軟石信息技術(集団)有限公司、...

安定したランキングを獲得する方法

ウェブサイトのランキングが不安定です。どのウェブマスターもいくつかの理由を教えてくれますが、多くのウ...

退役軍人が米国のサーバーレンタルとホスティングの違いを詳しく説明

海外のIDCが中国市場に参入して以来、大多数のユーザーから熱烈な歓迎を受けています。国内IDC製品の...

分散MemCacheの詳細な解釈

MemCacheとはMemCache は、動的な Web アプリケーションがデータベースの負荷を軽減...

vinahost: カンボジア VPS/カンボジア クラウド サーバー、月額 38 ドルから、トラフィック無制限

2008 年に設立されたベトナムのホスティング会社である vinahost は、ベトナム、タイ (機...

ソフト記事マーケティングにウェブマスターネットワークを選択する3つの理由を紹介します

ご存知のとおり、ソフトテキストマーケティングは、SEO最適化プロセスにおいて非常に優れたマーケティン...

信頼性の高い IPXCORE - 256MB メモリ KVM/SSD キャッシュ/月額 3 ドル/ニューヨーク

ipxcore がついに KVM を正式にリリースしました。最初のデータセンターはニューヨーク州バッ...

クラウドから移行する前に考慮すべきこと

ほとんどの IT リーダーは、より優れた、より高速な、またはより安価なコンピューティング サービスと...

Bilibiliは独自のビジネス哲学を理解したのでしょうか?

サークルを抜け出した後、ビリビリでもう一つの大きな出来事が起こりました。ビリビリが香港株式市場に上場...

losangelesvps: 年間 11 ドル、ロサンゼルスの無制限トラフィック VPS、1Gbps 帯域幅、768M メモリ/1 コア/10g SSD

中国の旧正月が近づいており、losangelesvps もこの楽しみに参加し、中国の旧正月向けの特別...

奇妙な沼地: 医療 SEO に関する外部の視点

Baidu による医療 SEO の継続的な取り締まりにより、多くの医療 SEO 担当者は最近ますます...

raksmart: クラウドサーバー 129 元/年、香港\日本\サンノゼ\ロサンゼルス、2G メモリ\1 コア\40g ハードディスク\

raksmart は現在、香港、日本、サンノゼ、ロサンゼルスのデータセンターのクラウド サーバー (...