Spring+Redis实现分布式锁

961 阅读6分钟

在分布式系统中,使用 Redis 实现分布式锁是一种常见的做法。Redis 提供了多种方式来实现分布式锁,最常见的方法是使用 SETNX 命令。为了确保锁的安全性和可靠性,通常需要考虑锁的自动过期、锁的释放、锁的重入等问题。

以下是使用 Spring 和 Redis 实现分布式锁的详细步骤和知识点:

1. 引入依赖

首先,在 pom.xml 中添加必要的依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.7.0</version>
</dependency>

2. 配置 Redis

application.propertiesapplication.yml 文件中配置 Redis 连接:

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=yourpassword

3. 实现分布式锁

接下来,使用 Spring Data Redis 和 Jedis 实现分布式锁。

3.1 Redis 配置类

创建一个配置类来初始化 RedisTemplate 和 JedisConnectionFactory:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.JedisPoolConfig;

@Configuration
public class RedisConfig {

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(10);
        poolConfig.setMaxIdle(5);
        poolConfig.setMinIdle(1);
        return new JedisConnectionFactory(poolConfig);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        return template;
    }
}

3.2 分布式锁实现类

创建一个分布式锁实现类:

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

import java.util.concurrent.TimeUnit;

@Component
public class RedisLock {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 尝试获取分布式锁
     * @param key 锁的键
     * @param value 锁的值,通常是唯一标识,如 UUID
     * @param expireTime 锁的过期时间,单位是秒
     * @return 是否成功获取锁
     */
    public boolean tryLock(String key, String value, long expireTime) {
        Boolean success = redisTemplate.opsForValue().setIfAbsent(key, value, expireTime, TimeUnit.SECONDS);
        return success != null && success;
    }

    /**
     * 释放分布式锁
     * @param key 锁的键
     * @param value 锁的值,只有当值匹配时才释放锁,防止误释放
     */
    public void unlock(String key, String value) {
        String currentValue = (String) redisTemplate.opsForValue().get(key);
        if (value.equals(currentValue)) {
            redisTemplate.delete(key);
        }
    }
}

3.3 使用分布式锁

在需要使用分布式锁的地方注入 RedisLock 并使用:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

@RestController
public class LockController {

    @Autowired
    private RedisLock redisLock;

    @GetMapping("/lock")
    public String lock() {
        String key = "myLock";
        String value = UUID.randomUUID().toString();
        long expireTime = 10; // 锁的过期时间,单位是秒

        boolean locked = redisLock.tryLock(key, value, expireTime);
        if (locked) {
            try {
                // 执行业务逻辑
                return "Locked and executed";
            } finally {
                redisLock.unlock(key, value);
            }
        } else {
            return "Unable to acquire lock";
        }
    }
}

4. 注意事项

  1. 锁的重入:上面的简单实现不支持锁的重入。如果需要支持重入锁,需要在锁的值中记录重入计数。
  2. 锁的过期时间:确保锁的过期时间足够长,以防止在业务逻辑执行过程中锁过期。
  3. 锁的自动续期:对于执行时间较长的任务,可以考虑实现锁的自动续期机制。
  4. Redis 集群:在 Redis 集群环境中,需要确保锁的实现能够正确处理 Redis 的分片和主从复制。

那么如何满足上述事项来实现基于Redis的分布式锁?

可重入锁

可重入锁允许同一个线程多次获取同一个锁,而不会导致死锁。这在需要嵌套锁操作的场景中非常有用。实现可重入锁的关键在于记录锁的持有者和重入次数。

实现步骤:

  1. 记录锁持有者:使用唯一标识(如线程ID)来标识锁的持有者。
  2. 记录重入次数:使用计数器记录同一线程重入锁的次数。
  3. 判断锁持有者:在获取锁时,检查当前线程是否已经持有锁,如果是则增加重入计数。
  4. 释放锁:在释放锁时,减少重入计数,当重入计数为0时才真正释放锁。

代码示例:

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

import java.util.concurrent.TimeUnit;

@Component
public class ReentrantRedisLock {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private static final String LOCK_PREFIX = "lock:";
    private static final ThreadLocal<String> LOCK_VALUE = new ThreadLocal<>();
    private static final ThreadLocal<Integer> LOCK_COUNT = new ThreadLocal<>();

    public boolean tryLock(String key, long expireTime) {
        String lockKey = LOCK_PREFIX + key;
        String lockValue = LOCK_VALUE.get();
        if (lockValue != null) {
            // 当前线程已持有锁,增加重入计数
            LOCK_COUNT.set(LOCK_COUNT.get() + 1);
            return true;
        }

        lockValue = Thread.currentThread().getId() + ":" + System.nanoTime();
        Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.SECONDS);
        if (success != null && success) {
            // 成功获取锁
            LOCK_VALUE.set(lockValue);
            LOCK_COUNT.set(1);
            return true;
        }
        return false;
    }

    public void unlock(String key) {
        String lockKey = LOCK_PREFIX + key;
        String lockValue = LOCK_VALUE.get();
        if (lockValue == null) {
            throw new IllegalStateException("This thread does not hold the lock");
        }

        int count = LOCK_COUNT.get();
        if (count > 1) {
            // 减少重入计数
            LOCK_COUNT.set(count - 1);
        } else {
            // 释放锁
            String currentValue = (String) redisTemplate.opsForValue().get(lockKey);
            if (lockValue.equals(currentValue)) {
                redisTemplate.delete(lockKey);
            }
            LOCK_VALUE.remove();
            LOCK_COUNT.remove();
        }
    }
}

锁的过期续期

锁的过期续期用于防止长时间执行的任务在锁过期后被其他线程获取。通过一个后台线程定期更新锁的过期时间来实现。

实现步骤:

  1. 启动续期任务:在获取锁成功后,启动一个定时任务,定期检查并更新锁的过期时间。
  2. 确保锁仍然持有:在续期时,确保当前线程仍然持有锁。
  3. 停止续期任务:在释放锁时,停止续期任务。

代码示例:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
public class RenewableRedisLock {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private static final String LOCK_PREFIX = "lock:";
    private static final ThreadLocal<String> LOCK_VALUE = new ThreadLocal<>();
    private static final ThreadLocal<Integer> LOCK_COUNT = new ThreadLocal<>();
    private static final long RENEW_INTERVAL = 5000L; // 续期间隔,单位毫秒

    public boolean tryLock(String key, long expireTime) {
        String lockKey = LOCK_PREFIX + key;
        String lockValue = LOCK_VALUE.get();
        if (lockValue != null) {
            LOCK_COUNT.set(LOCK_COUNT.get() + 1);
            return true;
        }

        lockValue = Thread.currentThread().getId() + ":" + System.nanoTime();
        Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.SECONDS);
        if (success != null && success) {
            LOCK_VALUE.set(lockValue);
            LOCK_COUNT.set(1);
            startRenewTask(lockKey, lockValue, expireTime);
            return true;
        }
        return false;
    }

    public void unlock(String key) {
        String lockKey = LOCK_PREFIX + key;
        String lockValue = LOCK_VALUE.get();
        if (lockValue == null) {
            throw new IllegalStateException("This thread does not hold the lock");
        }

        int count = LOCK_COUNT.get();
        if (count > 1) {
            LOCK_COUNT.set(count - 1);
        } else {
            String currentValue = (String) redisTemplate.opsForValue().get(lockKey);
            if (lockValue.equals(currentValue)) {
                redisTemplate.delete(lockKey);
            }
            LOCK_VALUE.remove();
            LOCK_COUNT.remove();
            stopRenewTask();
        }
    }

    private void startRenewTask(String lockKey, String lockValue, long expireTime) {
        // 启动定时任务,定期续期
        Runnable renewTask = () -> {
            while (LOCK_VALUE.get() != null && LOCK_VALUE.get().equals(lockValue)) {
                redisTemplate.expire(lockKey, expireTime, TimeUnit.SECONDS);
                try {
                    Thread.sleep(RENEW_INTERVAL);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        };
        new Thread(renewTask).start();
    }

    private void stopRenewTask() {
        // 停止定时任务
        // 这里可以使用更复杂的机制来管理续期任务的停止
    }
}

Redis 集群环境下的处理

在 Redis 集群环境中,锁的实现需要考虑分片和主从复制的问题。Redis 官方推荐使用 Redlock 算法来实现分布式锁。Redlock 算法在多个独立的 Redis 实例上实现锁,确保高可用性和一致性。

Redlock 算法的基本步骤:

  1. 获取当前时间:记录当前时间。
  2. 依次尝试在 N 个 Redis 实例上获取锁:每个实例设置相同的锁键和过期时间。
  3. 计算获取锁的总时间:如果总时间小于锁的过期时间的一半,则认为获取锁成功。
  4. 释放锁:当业务逻辑执行完毕后,依次在所有实例上释放锁。

代码示例:


import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.Arrays;
import java.util.List;
import java.util.UUID;

public class Redlock {

    private final List<JedisPool> jedisPools;
    private static final long DEFAULT_EXPIRE_TIME = 10000L; // 锁的默认过期时间,单位毫秒
    private static final long DEFAULT_RETRY_DELAY = 200L; // 重试延迟,单位毫秒
    private static final int DEFAULT_RETRY_COUNT = 3; // 重试次数

    public Redlock(JedisPool... jedisPools) {
        this.jedisPools = Arrays.asList(jedisPools);
    }

    public String lock(String key) {
        String value = UUID.randomUUID().toString();
        long expireTime = DEFAULT_EXPIRE_TIME;
        long startTime = System.currentTimeMillis();

        for (int i = 0; i < DEFAULT_RETRY_COUNT; i++) {
            int lockedCount = 0;
            for (JedisPool jedisPool : jedisPools) {
                try (Jedis jedis = jedisPool.getResource()) {
                    String result = jedis.set(key, value, "NX", "PX", expireTime);
                    if ("OK".equals(result)) {
                        lockedCount++;
                    }
                }
            }

            long elapsedTime = System.currentTimeMillis() - startTime;
            if (lockedCount >= (jedisPools.size() / 2 + 1) && elapsedTime < expireTime / 2) {
                return value;
            }

            for (JedisPool jedisPool : jedisPools) {
                try (Jedis jedis = jedisPool.getResource()) {
                    if (value.equals(jedis.get(key))) {
                        jedis.del(key);
                    }
                }
            }

            try {
                Thread.sleep(DEFAULT_RETRY_DELAY);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        return null;
    }

    public void unlock(String key, String value) {
        for (JedisPool jedisPool : jedisPools) {
            try (Jedis jedis = jedisPool.getResource()) {
                if (value.equals(jedis.get(key))) {
                    jedis.del(key);
                }
            }
        }
    }
}

总结

  1. 可重入锁:通过记录线程标识和重入计数实现,使得同一线程可以多次获取锁。
  2. 锁的过期续期:通过启动定时任务定期更新锁的过期时间,确保长时间任务不会因为锁过期而被其他线程获取。
  3. Redis 集群环境:使用 Redlock 算法在多个 Redis 实例上实现分布式锁,确保高可用性和一致性。