English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
最近の業務で以下のようなビジネスシーンに直面しました。毎日一定時に別のシステムにデータを一括して送信する必要がありますが、システムがクラスタデプロイメントされているため、タスクの競合が発生する可能性があります。したがって、一定時間範囲内で1つのJobが定時タスクを完了するように分布式ロックを追加する必要があります。当初の候補として、ZooKeeper分布式タスクやQuartz分布式タスクスケジューラが検討されましたが、ZooKeeperには追加コンポーネントが必要で、Quartzにはテーブルが必要であり、プロジェクトには既にRedisコンポーネントがあるため、Redis分布式ロックを使用して分布式タスクの抢占機能を実現する方法を検討しました。
記録一下通過した曲り道.
第一版本:
@Override public <T> Long set(String key,T value, Long cacheSeconds) { if (value instanceof HashMap) { BoundHashOperations valueOperations = redisTemplate.boundHashOps(key); valueOperations.putAll((Map) value); valueOperations.expire(cacheSeconds, TimeUnit.SECONDS); } else{ //使用map存储 BoundHashOperations valueOperations = redisTemplate.boundHashOps(key); valueOperations.put(key, value); //秒 valueOperations.expire(cacheSeconds, TimeUnit.SECONDS); } return null; } @Override public void del(String key) { redisTemplate.delete(key); }
ロックの占用と解放にはsetおよびdelを使用し、後にテストによりsetがスレッドセーフでないことが判明し、並行処理ではデータの不一致が発生することが多いとされました。
第2バージョン:
/** * 分散ロック * @param range ロックの長さ、リソースを奪取できる請求の数 * @param key * @return */ public boolean getLock(int range, String key) { ValueOperations<String, Integer> valueOper1 = template.opsForValue(); return valueOper1.increment(key, 1) <= range; } /** * ロックの初期化、0に設定 * @param key * @param expireSeconds * @return */ public void initLock(String key, Long expireSeconds) { ValueOperations<String, Integer> operations = template.opsForValue(); template.setKeySerializer(new GenericJackson2JsonRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); operations.set(key, 0, expireSeconds * 1000); } /** * ロックの解放 * @param key */ public void releaseLock(String key) { ValueOperations<String, Integer> operations = template.opsForValue(); template.setKeySerializer(new GenericJackson2JsonRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.delete(key); }
Redisのincreament操作を使用してロックの奪取を完了します。しかし、ロックを解放する際には、各スレッドがRedisからkey値を削除することができます。また、initLockは前回の操作をオーバーライドするため、このメソッドも廃止しました。
最終版:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.jedis.JedisConnection; import org.springframework.stereotype.Service; import org.springframework.util.ReflectionUtils; import redis.clients.jedis.Jedis; import java.lang.reflect.Field; import java.util.Collections; @Service public class RedisLock { private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; private static final Long RELEASE_SUCCESS = 1L; @Autowired private RedisConnectionFactory connectionFactory; /** * 分布式ロックの取得を試みます * @param lockKey ロック * @param requestId 请求識別子 * @param expireTime 有効期限 * @return 成功与否 */ public boolean lock(String lockKey, String requestId, int expireTime) { Field jedisField = ReflectionUtils.findField(JedisConnection.class, "jedis"); ReflectionUtils.makeAccessible(jedisField); Jedis jedis = (Jedis) ReflectionUtils.getField(jedisField, connectionFactory.getConnection()); String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } /** * 分布式ロックの解放 * @param lockKey ロック * @param requestId 请求識別子 * @return 释放成功かどうか */ public boolean releaseLock(String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('del', KEYS[1]) else return 0 end"; Object result = getJedis().eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; } public Jedis getJedis() { Field jedisField = ReflectionUtils.findField(JedisConnection.class, "jedis"); ReflectionUtils.makeAccessible(jedisField); Jedis jedis = (Jedis) ReflectionUtils.getField(jedisField, connectionFactory.getConnection()); return jedis; } }