springboot中使用Redis实现分布式锁

springboot中实现分布式锁有多种方法,一般采用Redis来实现分布式锁的比较多,它基于Redis的高性能和高可用特性,能够有效地协调多节点间的并发访问,防止竞争条件。在Spring Boot中使用Redis实现分布式锁,可以通过直接使用Jedis客户端或者集成第三方库如Redisson来简化操作。

原理如下:

基于Redis的分布式锁实现依赖于Redis的原子操作能力,核心是使用SET命令的NX和PX选项来实现锁的获取与自动过期。

当多个线程想要访问受保护的资源时,首先会循环尝试获取锁,这里可以设置获取时间,也可以设置获取次数,依据实际项目来定。(我们这里使用设置获取时间的方式)

获取锁之后它会尝试在Redis中设置一个带有唯一标识的键。如果设置成功,服务获得锁并可以继续执行;如果失败,则重试或回退。设置时间的意义在于如果因为其他因素导致redis宕机,也不会影响后续线程。

为确保锁不会被误删,释放锁时会检查键的值与设置时的标识是否一致。此外,通过引入锁续期机制和RedLock算法,可以进一步增强锁的稳定性和安全性。

1、基于Jedis客户端手动实现(不推荐)

        1.添加依赖:首先确保你的pom.xml中添加了Jedis客户端的依赖。

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.7.0</version> <!-- 使用最新的稳定版本 -->
</dependency>

        2.分布式锁实现

     下面的Java代码展示了如何实现一个基本的Redis分布式锁,包括获取锁、执行业务逻辑和释放锁的过程。此示例中,我们将使用Redis的SETNX(Set if Not Exists)命令尝试获取锁,并且利用EX参数设置锁的过期时间来防止死锁。
import redis.clients.jedis.Jedis;
import java.util.UUID;

public class DistributedLockWithJedis {

    private Jedis jedis;
    private static final String LOCK_SUCCESS = "OK";
    private static final long LOCK_TIMEOUT = 5; // 锁超时时间,单位:秒

    public DistributedLockWithJedis(Jedis jedis) {
        this.jedis = jedis;
    }

    /**
     * 尝试获取锁,允许设置获取锁的超时时间
     * @param lockKey 锁的键名
     * @return 成功时返回锁的唯一标识,失败则返回null
     */
    public String tryLock(String lockKey) {
        final String identifier = UUID.randomUUID().toString(); // 生成一个全局唯一的标识符
        final long endTime = System.currentTimeMillis() + LOCK_TIMEOUT * 1000; // 计算尝试获取锁的结束时间点
        
        // 在指定超时时间内循环尝试获取锁
        while (System.currentTimeMillis() < endTime) {
            // 使用SETNX命令尝试设置锁,同时设置锁的自动过期时间
            String result = jedis.set(lockKey, identifier, "NX", "EX", LOCK_TIMEOUT);
            if (LOCK_SUCCESS.equals(result)) {
                System.out.println("锁已获得,标识符为: " + identifier);
                return identifier; // 成功获取锁,返回标识符
            }
            try {
                Thread.sleep(100); // 如果没有获取到锁,则短暂休眠后重试
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException(e);
            }
        }
        return null; // 超时未获取到锁,返回null
    }

    /**
     * 释放锁
     * @param lockKey 锁的键名
     * @param identifier 锁的唯一标识
     * @return 是否成功释放了锁
     */
    public boolean releaseLock(String lockKey, String identifier) {
        // 使用Lua脚本确保删除操作的原子性,避免由于并发问题导致误删他人锁
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, 1, lockKey, identifier);
        if ("1".equals(result.toString())) {
            System.out.println("锁已释放,标识符为: " + identifier);
            return true;
        }
        return false;
    }

    // 示例使用方法
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost");
        DistributedLockWithJedis locker = new DistributedLockWithJedis(jedis);

        String lockKey = "myDistributedLock";
        String lockIdentifier = locker.tryLock(lockKey);
        if (lockIdentifier != null) {
            try {
                // 在此执行受保护的业务逻辑
                System.out.println("正在执行关键部分...");
            } finally {
                locker.releaseLock(lockKey, lockIdentifier); // 使用正确的标识符释放锁
            }
        } else {
            System.out.println("未能获取到锁。");
        }
        jedis.close(); // 关闭Jedis连接
    }
}

2、使用Redisson实现

使用Redisson在Spring Boot中实现分布式锁是一个既高效又简便的方法。上边我们说过的实现原理在Redisson中有更简洁的体现,我们不需要再自己创建唯一键,删除键时也不用额外写逻辑去验证是否误删,Redisson默认在内部处理锁的标识验证,当你使用tryLock方法获取锁时,它会在Redis中存储一个带有锁标识(内部由Redisson管理)的键值对,解锁时会校验这个标识。因此,直接使用Redisson的API,你不需要显式地添加额外的标识验证代码。也不用关心锁的过期时间,Redisson都在内部为我们实现了。
如果设置获取时间的话redissonClient中有方法可以直接设置:

boolean isLocked = lock.tryLock(10, 5, TimeUnit.SECONDS); // 尝试获取锁,等待10秒,锁的最大持有时间为5秒

这意味着,如果服务在持有锁的5秒内没有主动释放锁(比如因为处理逻辑未完成或服务崩溃),Redisson会自动在5秒后让锁失效,其他等待的线程就有机会获取锁并继续执行。

Redisson的锁实现了自动续期机制,只要客户端持有锁且会话有效,锁就会自动续期,避免因处理时间过长导致锁意外过期。你可以在创建RedissonClient时设置锁的默认自动续期时间,或者在获取锁时指定续期参数。例如,如果你希望锁自动续期,可以这样设置:

// 配置RedissonClient时设置默认锁自动续期时间
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379")
                 .setLockWatchdogTimeout(30, TimeUnit.SECONDS); // 设置锁自动续期时间窗口为30秒

RedissonClient redisson = Redisson.create(config);

当然我们也可以使用DI注入的方式将上述配置写入配置类中:

@Configuration
public class RedissonConfig {

    @Value("${spring.redis.host}")
    private String redisHost;

    @Value("${spring.redis.port}")
    private int redisPort;

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
              .setAddress("redis://" + redisHost + ":" + redisPort)
              .setDatabase(0)
              .setLockWatchdogTimeout(30, TimeUnit.SECONDS);

        return Redisson.create(config);
    }
}

这里的配置从Spring Boot的默认配置中读取Redis服务器的地址和端口,确保这些属性已经在你的application.propertiesapplication.yml文件中正确配置。我们在下文也使用配置类的方式,这样可以使我们编写更灵活、可测试的代码。

下面是详细的步骤指南,帮助你了解如何在Spring Boot应用中集成和使用Redisson来实现分布式锁:

        1.在你的Spring Boot项目的pom.xml文件中,添加Redisson的Spring Boot Starter依赖。确保使用与你的Spring Boot版本兼容的Redisson版本。

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>{latest-version}</version> <!-- 替换为最新稳定版本 -->
</dependency>

        2.配置Redisson,在application.propertiesapplication.yml中配置Redis服务器的连接信息。

spring:
  redis:
    host: your-redis-host
    port: 6379
    password: your-redis-password # 如果有密码的话

        3.Spring Boot会自动配置RedissonClient,你可以在需要使用分布式锁的服务类中直接注入RedissonClient。

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MyService {

    private final RedissonClient redissonClient;

    @Autowired
    public BusinessService(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    public void criticalSectionOperation(String businessId) {
        RLock lock = redissonClient.getLock("myLock" + businessId)); // 锁名称可以加上业务ID来使我们更容易区分
        
        try {
            // 尝试获取锁,如果无法立即获取则等待直到获得锁或者达到最大等待时间
            boolean isLocked = lock.tryLock(10, 5, TimeUnit.SECONDS); // 尝试获取锁,等待10秒,锁的最大持有时间为5秒
            
            if (isLocked) {
                // 执行临界区代码
                System.out.println("已获取锁,正在执行关键部分...");
                
                // 你的业务逻辑代码
                
            } else {
                System.out.println("在指定时间内未能获取到锁。");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.out.println("线程在尝试获取锁时被中断。);
        } finally {
            // 无论是否执行成功,都要确保解锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
                System.out.println("Lock released.");
            }
        }
    }
}
通过引入锁续期机制和RedLock算法,可以进一步增强锁的稳定性和安全性。

需要注意的是:

  • 确保Redis服务器的稳定性,避免因Redis故障导致的锁管理问题。
  • 正确处理锁的超时和释放,避免死锁。
  • 考虑使用lua脚本进行原子操作,以减少分布式环境下的竞态条件。
  • 26
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值