【分布式】Redis分布式锁

发布于:2024-11-29 ⋅ 阅读:(22) ⋅ 点赞:(0)

一、什么是Redis分布式锁

  • Redis分布式锁是一种基于Redis的机制,用于实现在分布式环境中对共享资源的互斥访问。它通过利用Redis的原子操作和过期时间特性来实现锁的获取和释放。
  • 如果没有合适的机制来保证资源的互斥访问,就可能导致数据不一致或者竞态条件的发生。而分布式锁就是一种解决这种并发访问问题的机制。

二、为什么选择用Redis分布式锁

  1. 高性能:Redis是一个高性能的内存数据库,具有快速的读写操作和高并发处理能力,因此适合用于实现分布式锁。

  2. 原子操作:Redis提供了一些原子操作,如SET命令可以在一个请求内完成锁的设置,保证锁的获取是原子的。

  3. 过期时间:Redis支持设置键的过期时间,可以为锁设置一个合理的过期时间,避免因为某个节点崩溃或者锁被长时间占用而导致死锁。

  4. 主从复制和哨兵模式:Redis支持主从复制和哨兵模式,可以提供高可用性和故障恢复能力,确保分布式锁的可靠性和稳定性。

三、Redis分布式锁的实现原理

Redis分布式锁的实现原理是通过利用Redis的单线程特性原子性操作来实现。它基于以下几个关键组件和步骤:

  1. 加锁:客户端通过执行SET命令,在Redis中创建一个特定的key作为锁,并设置一个具有过期时间的值,当key不存在时创建锁

  2. 保证原子性:在设置锁时,使用了Redis的SETNX命令或SET命令的NX选项,确保只有一个客户端能够成功获取到锁。这样可以保证加锁的原子性操作。

  3. 设置过期时间:通过给锁设置一个过期时间,即使在某种情况下锁没有被正常释放,也能确保锁会自动过期并释放资源,避免死锁的发生。

  4. 解锁:客户端在完成任务后,通过执行DEL命令来删除锁,或者通过执行Lua脚本来判断并删除锁。只有持有锁的客户端才能成功释放锁

四、注意在使用Redis分布式锁时,需要考虑以下几个问题

  1. 锁的过期时间需谨慎设置,过短可能导致锁提前释放,过长可能导致锁长时间占用资源。
  2. 加锁和释放锁的操作需要原子性,否则可能会导致多个客户端同时拥有锁或释放锁的问题。
  3. 客户端获取到锁后,需要保证执行共享资源的操作尽量快速,避免锁的长时间占用。
  4. 需要考虑锁过期但是没有执行完的情况,进行锁续期
    • 在获取到锁后,使用定时任务或线程来定期刷新锁的过期时间,可以通过调用RedisTemplate的expire方法来实现。
    • 续期的时间间隔可以根据实际情况设置,可以是固定的时间间隔,也可以根据任务执行时间动态调整。
    • 如果任务执行时间过长,可能会导致锁一直被续期,从而阻塞其他客户端获取锁的操作,因此续期的时间间隔需要谨慎设置。

五、Redis分布式锁的Java实现示例

import redis.clients.jedis.Jedis;

public class RedisLock {
    private Jedis jedis;
    private String lockKey;
    private int expireTime;

    public RedisLock(Jedis jedis, String lockKey, int expireTime) {
        this.jedis = jedis;
        this.lockKey = lockKey;
        this.expireTime = expireTime;
    }

    public boolean acquire() {
        long startTime = System.currentTimeMillis();
        try {
            while (true) {
                // 尝试获取锁
                String result = jedis.set(lockKey, "locked", "NX", "PX", expireTime);
                if ("OK".equals(result)) {
                    return true;
                }

                // 检查锁是否过期
                if (jedis.ttl(lockKey) == -1) {
                    jedis.expire(lockKey, expireTime);
                }

                // 避免无限循环造成的CPU资源浪费
                if (System.currentTimeMillis() - startTime > expireTime) {
                    return false;
                }

                Thread.sleep(100);
            }
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    public void release() {
        jedis.del(lockKey);
    }
}

在上述代码中,我们使用Jedis库来连接Redis服务器。RedisLock类封装了获取锁和释放锁的逻辑。

在获取锁时,通过使用set命令并设置NX(只在键不存在时设置)和PX(设置过期时间)参数来尝试将一个特定的键(lockKey)作为锁写入Redis服务器。如果成功获取到锁,则返回true。如果其他节点已经持有了锁,则需要等待一段时间后再次尝试获取锁。

在释放锁时,使用del命令将锁从Redis服务器中删除。

使用示例:

public class Main {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost");
        RedisLock lock = new RedisLock(jedis, "mylock", 10000);
        if (lock.acquire()) {
            try {
                // 执行共享资源的操作
                System.out.println("Critical section");
            } finally {
                lock.release();
            }
        }
    }
}

在该示例中,我们创建了一个Jedis实例,并使用它来实例化RedisLock对象。然后,我们在获取锁后执行共享资源的操作,并在操作完成后释放锁。

请确保在使用完成后及时释放锁,以免造成死锁或长时间占用资源。

六、Redis分布式锁的SpringBoot实现示例

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;

    public boolean acquireLock(String lockKey, String requestId, long expireTime) {
        Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expireTime, TimeUnit.MILLISECONDS);
        return locked != null && locked;
    }

    public void releaseLock(String lockKey, String requestId) {
        String storedRequestId = (String) redisTemplate.opsForValue().get(lockKey);
        if (requestId.equals(storedRequestId)) {
            redisTemplate.delete(lockKey);
        }
    }
}

在上述代码中,我们使用Spring Data Redis来连接Redis服务器。RedisLock类封装了获取锁和释放锁的逻辑。

在获取锁时,我们使用了Redis的setIfAbsent方法,该方法在键不存在时才会设置键值对。并设置了过期时间,以确保锁的自动释放。

在释放锁时,我们首先获取存储在Redis中的请求标识,如果和当前请求的标识一致,则删除锁。

使用示例:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application implements CommandLineRunner {
    @Autowired
    private RedisLock redisLock;

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        String lockKey = "mylock";
        String requestId = "123456";
        long expireTime = 10000;

        if (redisLock.acquireLock(lockKey, requestId, expireTime)) {
            try {
                // 执行共享资源的操作
                System.out.println("Critical section");
            } finally {
                redisLock.releaseLock(lockKey, requestId);
            }
        }
    }
}

在这个示例中,我们使用了Spring Boot的注解驱动,将RedisLock类注入到Application类中。在run方法中,我们使用acquireLock方法获取锁,并在获取到锁之后执行共享资源的操作。在操作完成后,我们使用releaseLock方法释放锁。

请确保在使用完成后及时释放锁,以免造成死锁或长时间占用资源。