1. ホーム
  2. データベース
  3. レディス

SpringBootがRedisの分散ロックを利用して並行処理の問題を解決することについて

2022-01-23 17:21:32

問題の背景

今日のアプリケーション・アーキテクチャでは、サービスの安定性を確保するために、多くのサービスが複数のコピーで実行されています。あるサービスインスタンスがハングアップしても、他のサービスはリクエストを受信することができます。例えば、あるインターフェースの処理ロジックは次のようなものです。リクエストを受信したら、まずDBに問い合わせて関連データがあるかどうかを確認し、なければデータを挿入し、あればデータを更新します。このとき、バックエンドのサービスインスタンスに同じN個のリクエストが同時に送られると、データの挿入が重複してしまう。

解決方法

上記の問題に対する一般的な解決策は、分散ロックを使用することである。しかし、サービスが複数のインスタンスに展開されている場合、サービスは分散され、各プロセスは独立している。この場合、グローバルロックの場所を設定し、各プロセスは何らかの方法でグローバルロックを取得し、ロックを取得した後にビジネスロジックのコードを実行し、ロックを取得しない場合は実行をスキップすることができるようにすることができる。このグローバル・ロックが分散ロックと呼ばれるものです。分散ロックは一般に3つの方法で実装される。1.データベースの楽観的ロック、2.Redisベースの分散ロック、3.ZooKeeperベースの分散ロック。

ここでは、Redisベースの分散ロックが分散並行処理問題を解決するためにどのように使用できるかを説明する。Redisはグローバルロックを取得する場所として機能し、各インスタンスはリクエストを受けるとまずRedisからロックを取得し、ロックを取得するとビジネスロジックのコードを実行し、ロックを争奪しない場合は実行を中断します。


主な実装方針。

Redisロックは、主にRedis setnxコマンドを使用して実装されます。

ロックコマンドです。SETNX key value:キーを設定し、キーが存在しない場合は成功を、それ以外の場合は失敗を返します。KEYはロックの一意な識別子で、一般に業務に応じて命名される。valueはロックが誤解されないようにUUIDで識別されるのが一般的である。

ロック解除のコマンドです。DEL keyは、キーと値のペアを削除してロックを解放し、他のスレッドがSETNXコマンドでロックを取得できるようにします。

ロックのタイムアウト EXPIRE key timeout: キーのタイムアウトを設定し、明示的にロックを解除しなくても一定時間後に自動的にロックが解除されるようにし、リソースが永遠にロックされることを防ぐ。

信頼性

分散ロックを確実に利用するためには、ロック実装が少なくとも4つの条件を同時に満たすようにする必要があります。

  • 相互排他性。あるマシンの1つのスレッドだけが、任意の瞬間にロックを保持できることを保証するもの。
  • デッドロックが発生しない。あるクライアントがロックを保持中にクラッシュし、積極的にロックを解除しない場合でも、後続の他のクライアントがロックを追加できることを保証します。
  • ノンブロッキング。ロックが取得されないとすぐにロック失敗を返します。
  • ロックとアンロックは同じクライアントが行う必要があります。クライアント自身は、他の人が追加したロックをアンロックすることはできません。

Redisの分散ロックを利用したSpringBootの統合

ビジネスロジックの実行前にロックし、ビジネスロジックの実行後にアンロックするためのRedisLockユーティリティクラスを書きました。SpringBootのバージョンが2.xの場合、コメントにあるコードでロック有効期限を設定しながらロックを追加することができます。SpringBootのバージョンが2.x以下であれば、Luaスクリプトを使用して、操作のアトミック性を確保することをお勧めします。簡単のため、以下のように記述します。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util;
import java.util.concurrent.TimeUnit;

/**
 * @description: Redis distributed lock implementation tool class
 * @author: qianghaohao
 * @time: 2021/7 *@Component
public class RedisLock {
    @Autowired
    StringRedisTemplate redisTemplate;

    /**
     * Get the lock
     *
     * @param lockKey lock
     * @param identity identity (ensures that the lock will not be released by anyone else)
     * @param expireTime The expiration time of the lock (in seconds)
     * @return
     *    public boolean lock(String lockKey, String identity, long expireTime) {
        // Since we have a low version of springboot, 1.5.9, we don't support the following way of writing
        // return redisTemplate.opsForValue().setIfAbsent(lockKey, identity, expireTime, TimeUnit.SECONDS);
        if (redisTemplate.opsForValue().setIfAbsent(lockKey, identity)) {
            redisTemplate.expire(lockKey, expireTime, TimeUnit.SECONDS);
            return true;
        }
        return false;
    }

    /**
     * Release the lock
     *
     * @param lockKey lock
     * @param identity identity (ensures that the lock will not be released by anyone else)
     * @return
     *    public boolean releaseLock(String lockKey, String identity) {
        String luaScript = "if " +
                " redis.call('get', KEYS[1]) == ARGV[1] " +
                "then " +
                " return redis.call('del', KEYS[1]) " +
                "else " +
                " return 0 " +
                "end";
        DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(Boolean.class);
        redisScript.setScriptText(luaScript);
        List<String> keys = new ArrayList<>();
        keys.add(lockKey);
        Object result = redisTemplate.execute(redisScript, keys, identity);
        return (boolean) result;
    }
}


使用例

ここにはキーコードのみが掲載されていますが、ロックのキーはビジネスロジックに従って命名され、同じリクエストを一意に識別できることに注意してください。 valueにはUUIDを設定し、ロックを解放するときに正しく解放されるようにします(追加したロックのみが解放されます)。

@Autowired
private RedisLock redisLock; // redis Distributed lock


        String redisLockKey = String.format("%s:docker-image:%s", REDIS_LOCK_PREFIX, imageVo.getImageRepository());
        String redisLockValue = UUID.randomUUUID().toString();
        try {
            if (!redisLock.lock(redisLockKey, redisLockValue, REDIS_LOCK_TIMEOUT)) {
                logger.info("redisLockKey [" + redisLockKey + "] already exists, not performing mirror insertion and update");
                result.setMessage("New mirror frequently, retry later, lock occupied");
                return result;
            }
            ... // Execute the business logic
       catch (Execpion e) {
            ... // Exception handling
       } finally { // Release the lock
            if (!redisLock.releaseLock(redisLockKey, redisLockValue)) {
                logger.error("Free redis lock [" + redisLockKey + "] failed);
            } else {
                logger.error("Free redis lock [" + redisLockKey + "] success");
            }
        }


参考資料

https://www.jianshu.com/p <未定義
https://xiaomi-info.github.io/2019/12/17/redis-distributed-lock

今回はSpringBootのRedis分散ロックによる並行処理問題の解決についてご紹介します。