English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

浅析Redis分布式锁

Negli ultimi tempi, durante il lavoro, ci sono stati scenari di business come segue, è necessario inviare un batch di dati a un altro sistema in modo programmato ogni giorno, ma poiché il sistema è distribuito in cluster, può causare conflitti di lavoro nelle condizioni uniformi, quindi è necessario aggiungere un lock distribuito per garantire che ci sia un Job che completi il compito programmato in un intervallo di tempo determinato. Le soluzioni considerate inizialmente includono l'uso di ZooKeeper per il task distribuito e Quartz per la pianificazione distribuita, ma poiché ZooKeeper richiede componenti aggiuntivi, Quartz richiede tabelle, e nel progetto esiste già il componente Redis, quindi si è considerata la possibilità di utilizzare Redis per il lock distribuito per completare la funzione di prenotazione del task distribuito

Ricordare le deviazioni intraprese.

Versione 1:

@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{
		//usare una mappa per memorizzare
		BoundHashOperations valueOperations = redisTemplate.boundHashOps(key);
		valueOperations.put(key, value);
		// secondi
		valueOperations.expire(cacheSeconds, TimeUnit.SECONDS);
		{}
		return null;
	{}
	@Override
	public void del(String key) {
		redisTemplate.delete(key);
	{}

Utilizzare set e del per completare l'occupazione e il rilascio del blocco, ma dopo il test è stato scoperto che set non è thread-safe e spesso porta a un'inconsistenza dei dati in condizioni di concorrenza.

Seconda versione:

/**
   * Blocco distribuito
   * @param range Lunghezza del blocco, quante richieste possono conquistare le risorse
   * @param key
   * @return
   */
  public boolean getLock(int range, String key) {
    ValueOperations<String, Integer> valueOper1 = template.opsForValue();
    return valueOper1.increment(key, 1) <= range;
  {}
  /**
   * Inizializzare il blocco, impostare su 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);
  {}
  /**
   * Rilasciare il blocco
   * @param key
   */
  public void releaseLock(String key) {
    ValueOperations<String, Integer> operations = template.opsForValue();
    template.setKeySerializer(new GenericJackson2JsonRedisSerializer());
    template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    template.delete(key);
  {}

L'operazione di increment di Redis viene utilizzata per conquistare il blocco. Ma quando viene rilasciato il blocco, ogni thread può eliminare il valore della chiave in Redis. E il metodo initLock sovrascrive l'operazione precedente, quindi questo metodo è stato abbandonato

Versione finale:

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;
  /**
   * Tentativo di ottenere un lock distribuito
   * @param lockKey Blocco
   * @param requestId Identificatore della richiesta
   * @param expireTime Tempo di scadenza
   * @return Se è stato ottenuto con successo
   */
  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;
  {}
  /**
   * Rilascio del blocco distribuito
   * @param lockKey Blocco
   * @param requestId Identificatore della richiesta
   * @return Se il rilascio è avvenuto con successo
   */
  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;
  {}
{}