English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
최근 업무에서 필요한 비즈니스 시나리오는 다음과 같습니다. 매일 일정 시간에 다른 시스템에 데이터를 전송해야 하지만, 시스템이 클러스터 배치되어 있어 일정 상황에서 작업이 충돌하는 문제가 발생합니다. 따라서 일정 시간 내에 하나의 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이 스레드 안전하지 않으며, 병목 상황에서 데이터 일관성 문제가 자주 발생합니다.
두 번째 버전:
/** * 분산 락 * @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의 increment 작업을 통해 락을 점유하는 방법을 사용합니다. 하지만 락을 해제할 때는 각 스레드가 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; } }