Redis分散トランザクション・ロックの実装

12488 ワード

Redisトランザクション・ロック
分散ロックは、異なるプロセスが共有リソースに反発的にアクセスする必要がある場合に非常に有用な技術的手段である.ここではSpring Data Redisを用いてRedisの分散トランザクションロックを実装する.
Redisは単一プロセスの単一スレッド・モードであり、キュー・モードを使用すると同時アクセスがシリアル・アクセスになり、マルチクライアントのRedisへの接続に競合はありません.
SETNXコマンド(SET if Not eXists)構文:
SETNX key value
与えられたkeyが既に存在する場合、SETNXは何もせず、0を返す.
セキュリティ:反発を保証し、いつでも1つのクライアントだけがロックを保持できます.現在ロックを保持しているクライアントがクラッシュしたり、クラスタから分離されたりしても、他のクライアントは最終的に常にロックを取得できます.フォールトトレランス:Redisノードのほとんどがオンラインであれば、クライアントはロックを取得および解放できます.
Spring redisTemplateによる実装
redisTemplate実装を使用するにはredisのeval実装に合わせる必要があり、Spring Data Redisの公式ドキュメントではRedis Scriptingの一節に関連する説明があります.
Spring Redisドキュメントでevalがどのように使用されているかを見てみましょう.
@Bean
public RedisScript<Boolean> script() {
  DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<Boolean>();
  redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("META-INF/scripts/checkandset.lua")));
  redisScript.setResultType(Boolean.class);
}
public class Example {
  @Autowired
  RedisScript script;
  public boolean checkAndSet(String expectedValue, String newValue) {
    return redisTemplate.execute(script, Collections.singletonList("key"), expectedValue, newValue);
  }
}
 -- checkandset.lua local
 current = redis.call('GET', KEYS[1])
 if current == ARGV[1]
   then redis.call('SET', KEYS[1], ARGV[2])
   return true
 end
 return false

eval関数およびLuaスクリプトについては、ここでは説明しません.redisTemplateを使用してトランザクション・ロックを実装する方法について説明します.
トランザクション・ロックのBeanを定義します.
public class RedisLock {
    private String key;
    private final UUID uuid;
    private long lockTimeout;

    private long startLockTimeMillis;
    private long getLockTimeMillis;
    private int tryCount;

    public RedisLock(String key, UUID uuid, long lockTimeout, long startLockTimeMillis, long getLockTimeMillis, int tryCount) {
        this.key = key;
        this.uuid = uuid;
        this.lockTimeout = lockTimeout;
        this.startLockTimeMillis = startLockTimeMillis;
        this.getLockTimeMillis = getLockTimeMillis;
        this.tryCount = tryCount;
    }

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public UUID getUuid() {
        return uuid;
    }

    public long getLockTimeout() {
        return lockTimeout;
    }

    public void setLockTimeout(long lockTimeout) {
        this.lockTimeout = lockTimeout;
    }

    public long getGetLockTimeMillis() {
        return getLockTimeMillis;
    }

    public void setGetLockTimeMillis(long getLockTimeMillis) {
        this.getLockTimeMillis = getLockTimeMillis;
    }

    public long getStartLockTimeMillis() {
        return startLockTimeMillis;
    }

    public void setStartLockTimeMillis(long startLockTimeMillis) {
        this.startLockTimeMillis = startLockTimeMillis;
    }

    public int getTryCount() {
        return tryCount;
    }

    public void setTryCount(int tryCount) {
        this.tryCount = tryCount;
    }
}

取得ロック操作を作成するには、次の手順に従います.
//       ,    
private static final long DEFAULT_LOCK_TIME_OUT = 3000;   //         ,    ,0      (       )
private static final long DEFAULT_TRY_LOCK_TIME_OUT = 0;  
//   EVAL  
private static final String LUA_SCRIPT_LOCK = "return redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) ";
//    EVAL  
private static RedisScript scriptLock = new DefaultRedisScript(LUA_SCRIPT_LOCK, String.class);

ロックの取得方法:
public static RedisLock lock(int dbIndex, String key, long lockTimeout, long tryLockTimeout) {
    long timestamp = System.currentTimeMillis();
    try {
        //    
        key = key + ".lock";

        UUID uuid = UUID.randomUUID();

        int tryCount = 0;

        //     ,      
        while (tryLockTimeout == 0 || (System.currentTimeMillis() - timestamp) < tryLockTimeout) {

//       ,    ,            scriptLock          ,KEYS[1],ARGV[1],ARGV[2],     key,key   value,  key      (    )

String result = redisTemplate.execute(scriptLock, redisTemplate.getStringSerializer(), redisTemplate.getStringSerializer(), Collections.singletonList(key), uuid.toString(),
                        String.valueOf(lockTimeout));
    tryCount++;
    //  “OK”     
    if (result != null && result.equals("OK")) {
        return new RedisLock(key, uuid, lockTimeout, timestamp, System.currentTimeMillis(), tryCount);
    } else {
        try {
            //    ,  50      (   )
            Thread.sleep(50);
        } catch (InterruptedException e) {
                        e.printStackTrace();
        }
    }
}
logger.error("Fail to get lock key");
}
return null;
}

上記のコードはredisTemplateによって実現されたredisの分散ロックであり、Beanの作成に成功すればロックを取得することを説明し、そうでなければロックを取得することに失敗し、コアはRedisのeval関数を採用し、CASのような操作を用いてロックを行い、ロックを取得に成功すれば「OK」に戻り、失敗した場合、休眠してからロックを取得し続け、タイムアウトするまで試行する.
ロック解除操作:
private static final String LUA_SCRIPT_UNLOCK = 
            "if (redis.call('GET', KEYS[1]) == ARGV[1]) then "
            + "return redis.call('DEL',KEYS[1]) " 
            + "else " + "return 0 " + "end";


private static RedisScript<String> scriptUnlock = 
        new DefaultRedisScript<String>(LUA_SCRIPT_UNLOCK,
            String.class);
public static void unLock(int dbIndex, RedisLock lock) {                                                                 

    redisTemplate.execute(scriptUnlock,
                  redisTemplate.getStringSerializer(),
                  redisTemplate.getStringSerializer(),     
                  Collections.singletonList(lock.getKey()),     
                  lock.getUuid().toString());
}

以上、Redisを使用して分散ロックを実現する方法であり、その方法はRedis StringのSETを使用して実現され、SETコマンドの動作は一連のパラメータによって修正することができる.
  • EX second:キーの有効期限をsecond秒に設定します.SET key value EX second効果はSETEX key second valueと同等です.
  • PX millisecond:設定キーの有効期限はmillisecondミリ秒です.SET key value PX millisecond効果はPSETEX key millisecond valueと同等です.
  • NX:キーが存在しない場合のみ、キーの設定操作を行います.SET key value NX効果はSETNX key valueに等しい.
  • XX:キーが既に存在する場合のみ、キーを設定します.

  • 詳細については、Redisドキュメントを参照してください.
    http://redisdoc.com/string/set.html