优秀的编程知识分享平台

网站首页 > 技术文章 正文

实现分布式锁,Redisson是把利剑(实现分布式锁的几种方式)

nanyue 2024-09-01 00:07:03 技术文章 5 ℃

前言:

在分布式系统中,当多个线程(或进程)同时操作同一个资源时,为了保证数据一致性问题,所以就需要一种机制来确保在同一时间只有一个线程(或进程)能够对资源进行修改,这就是分布式锁的作用。


分布式锁实现方案

基于数据库、Redis、ZooKeeper 等中间件来实现,它们通常依赖于这些中间件提供的事务特性,或者命令语义来达到分布式环境下的锁效果。例如,Redis 通过 SETNX 命令配合过期时间可实现一个简单的分布式锁方案。

SETNX 的问题及实现

setnx能实现分布式锁,但是同样也存在一些问题:

  1. 死锁问题:SETNX 如未设置过期时间,锁忘记删了或加锁线程宕机都会导致死锁,也就是分布式锁一直被占用的情况。
  2. 不可重入问题:也就是说同一线程在已经获取了某个锁的情况下,如果再次请求获取该锁,则请求会失败(因为只有在第一次能加锁成功)。也就是说,一个线程不能对自己已持有的锁进行重复锁定。
  3. 锁误删问题:SETNX 设置了超时时间,但因为执行时间太长,所以在超时时间之内锁已经被自动释放了,但线程不知道,因此在线程执行结束之后,会把其他线程的锁误删的问题。
  4. 无法自动续期:线程在持有锁期间,任务未能执行完成,锁可能会因为超时而自动释放。SETNX 无法自动根据任务的执行情况,设置新的超时实现,以延长锁的时间。

使用 set key value [EX seconds][PX milliseconds][NX|XX] 命令

SET key value[EX seconds][PX milliseconds][NX|XX]命令说明:

  • EX seconds:设定过期时间,单位为秒
  • PX milliseconds:设置过期时间,单位为毫秒
  • NX:仅当key不存在时,设置值
  • XX:仅当key存在时设置值
ublic String tryLock(String name, long expire){
    name = name +"_lock";
    String token = UUID.randomUUID().toString();
    //获取redis连接
    RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
    RedisConnection conn = factory.getConnection();
    //redis的set命令
    //conn.set(name.getBytes(),token.getBytes());
    try {
        Boolean result = conn.set(name.getBytes(),//将字符串name转换为字节数组,作为Redis中键的值
                token.getBytes(),//将字符串token转换为字节数组,作为Redis中值的值
                Expiration.from(expire, TimeUnit.SECONDS),//设置键的过期时间,expire表示过期时间的长度,TimeUnit.SECONDS表示时间单位为秒
                RedisStringCommands.SetOption.SET_IF_ABSENT //设置只在键不存在时才进行设置操作(即"NX"参数)
        );
        if(result && result != null){
            return token;//返回值
        }

    } catch (Exception e) {
        throw new RuntimeException(e);
    }finally {
        RedisConnectionUtils.releaseConnection(conn,factory,false);
    }

    return null;
}

value必须要具有唯一性,我们可以用UUID来做,设置随机字符串保证唯一性,至于为什么要保证唯一性?假如value不是随机字符串,而是一个固定值,那么就可能存在下面的问题:

1.客户端1获取锁成功

2.客户端1在某个操作上阻塞了太长时间

3.设置的key过期了,锁自动释放了

4.客户端2获取到了对应同一个资源的锁

5.客户端1从阻塞中恢复过来,因为value值一样,所以执行释放锁操作时就会释放掉客户端2持有的锁,这样就会造成问题。

Redisson是什么?

Redisson 是一个开源的用于操作 Redis 的 Java 框架。与 Jedis 和 Lettuce 等轻量级的 Redis 框架不同,它提供了更高级且功能丰富的 Redis 客户端。它提供了许多简化 Redis 操作的高级 API,并支持分布式对象、分布式锁、分布式集合等特性。

Redisson 特性说明

  1. Redisson 可以设置分布式锁的过期时间,从而避免锁一直被占用而导致的死锁问题。
  2. Redisson 在为每个锁关联一个线程 ID 和重入次数(递增计数器)作为分布锁 value 的一部分存储在 Redis 中,这样就避免了锁误删和不可重入的问题。
  3. Redisson 还提供了自动续期的功能,通过定时任务(看门狗)定期延长锁的有效期,确保在业务未完成前,锁不会被其他线程获取。

Redisson 分布式锁

1.添加 Redisson 框架

Spring Boot 项目,直接添加 Redisson 为 Spring Boot 写的如下依赖:

<!-- Redisson -->
<!-- https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter -->
<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson-spring-boot-starter</artifactId>
  <version>3.25.2</version> <!-- 请根据实际情况使用最新版本 -->
</dependency>

2.配置 RedissonClient 对象

将 RedissonClient 重写,存放到 IoC 容器,并且配置连接的 Redis 服务器信息。

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        // 也可以将 redis 配置信息保存到配置文件
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        return Redisson.create(config);
    }
}

3.创建分布式锁

Redisson 分布式锁的操作和 Java 中的 ReentrantLock(可重入锁)的操作很像,都是先使用 tryLock 尝试获取(非公平)锁,最后再通过 unlock 释放锁,具体实现如下:

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
@RestController
public class LockController {
    @Autowired
    private RedissonClient redissonClient;
    @GetMapping("/lock")
    public String lockResource() throws InterruptedException {
        String lockKey = "myLock";
        // 获取 RLock 对象
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 尝试获取锁(尝试加锁)(锁超时时间是 30 秒)
            boolean isLocked = lock.tryLock(30, TimeUnit.SECONDS);
            if (isLocked) {
                // 成功获取到锁
                try {
                    // 模拟业务处理
                    TimeUnit.SECONDS.sleep(5);
                    return "成功获取锁,并执行业务代码";
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放锁
                    lock.unlock();
                }
            } else {
                // 获取锁失败
                return "获取锁失败";
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "获取锁成功";
    }
}

3.1实现公平锁

Redisson 默认创建的分布式锁是非公平锁(出于性能的考虑),想要把它变成公平锁可使用以下代码实现:

RLock lock = redissonClient.getFairLock(lockKey);  // 获取公平锁

3.2实现读写锁

Redisson 还可以创建读写锁,如下代码所示:

RReadWriteLock lock = redissonClient.getReadWriteLock(lockKey); // 获取读写锁
lock.readLock();  // 读锁
lock.writeLock(); // 写锁

读写锁的特点就是并发性能高,它是允许多个线程同时获取读锁进行读操作的,也就是说在没有写锁的情况下,读取操作可以并发执行,提高了系统的并行度。但写锁则是独占式的,同一时间只有一个线程可以获得写锁,无论是读还是写都无法与写锁并存,这样就确保了数据修改时的数据一致性。

3.3实现联锁

Redisson 也支持联锁,也叫分布式多锁 MultiLock,它允许客户端一次性获取多个独立资源(RLock)上的锁,这些资源可能是不同的键或同一键的不同锁。当所有指定的锁都被成功获取后,才会认为整个操作成功锁定。这样能够确保在分布式环境下进行跨资源的并发控制。 联锁的实现示例如下:

// 获取需要加锁的资源
RLock lock1 = redisson.getLock("lock1");
RLock lock2 = redisson.getLock("lock2");
// 联锁
RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2);
try {
    // 一次性尝试获取所有锁
    if (multiLock.tryLock()) {
        // 获取锁成功...
    }
} finally {
    // 释放所有锁
    multiLock.unlock();
}

分布式锁重点在于互斥性,在任意一个时刻,只有一个客户端获取了锁。在实际的生产环境中,分布式锁的实现可能会更复杂,而文章讲述主要针对的是单机环境下的基于Redis的分布式锁实现,那么我们思考下Redis集群环境下怎样实现分布式锁呢?

Tags:

最近发表
标签列表