Java实现Redis分布式锁

前言

为了让同一时刻资源只能一个线程访问,也就是互斥访问共享资源,在单机环境中,我们通常会使用JVM本地锁、volatile、concurrent并发包等方式实现。但随着系统规模的扩大,系统升级成了分布式系统,一个服务会部署到不同服务器上,不同机器不同进程,就需要在多进程下保证线程的安全性了。因此,分布式锁应运而生。

原理

分布式锁的基本原理:

  1. 请求锁:当一个线程实例需要访问共享资源时,他会向分布式锁系统发送一个请求,试图获取锁。
  2. 锁定资源:分布式锁系统判断当前锁是否被其他实例占用,如果没有被占用,则当前实例获取成功,锁定资源;如果已经被占用,当前实例获取失败,继续等待或直接放弃。
  3. 访问资源:当前实例获取成功后,则可以放心安全使用资源,直到业务执行完毕。
  4. 释放资源:使用完毕后,通知分布式锁系统释放资源,放给其他实例使用。

Redis实现

  • SETNX key value SETNX的全称是SET IF NOT EXIST(如果不存在则设置key),当设置成功后返回1,设置失败后返回0。
  • EXPIRE key seconds 设置key的过期时间
  • DEL key 删除key 所以使用redis加锁的流程为
less 复制代码
// 加锁 
SETNX lock_key 1 
// 1分钟后过期
EXPIRE lock_key 60
// 业务逻辑 
DO THINGS 
// 释放锁 
DEL lock_key

但这个操作并不是原子性的,如果加锁过后,Redis宕机,锁就一直释放不了,就会造成死锁。所以最好把这段命令使用Lua脚本,或者使用Redis给我们提供的SETNX和EXPIRE合并的命令SET key value EX seconds NXSET lock_key 1 EX 60 NX

不足:

  • 不可重入:Redis分布式锁默认是不可重入的,如果需要可重入,需要额外的逻辑来实现。
  • 非阻塞:Redis分布式锁是非阻塞的,如果获取锁失败,需要自己进行重试。

Redission实现

Redis虽然可以完成分布式锁的实现,但是还有有不可重入、非阻塞等缺点,使用Redission就给我们大大降低了使用成本,Redission使用大量的Lua脚本和Netty,解决了Redis锁不可重入、非阻塞等缺点。

引入Redission

java 复制代码
<!-- redisson 分布式锁 -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.27.2</version>
</dependency>

使用Redission

Redisson支持单点模式、主从模式、哨兵模式、集群模式,这里以单点模式为例,模拟5个线程同时抢1个红包:

java 复制代码
public void test() throws InterruptedException {
    String key = "red_paper_id";
    long waitTimeout = 100;  //100毫秒
    long leaseTime = 2000; //2000毫秒
    for (int i = 1; i <= 5; i++) {
        new Thread(() -> {
            String threadName = Thread.currentThread().getName();
            RLock lock = redissonClient.getLock(key);
            try {
                /* waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
                 * leaseTime   锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)*/
                boolean res = lock.tryLock(waitTimeout, leaseTime, TimeUnit.MILLISECONDS);
                if(res){
                    System.out.println(threadName + "获取到锁");
                    Thread.sleep(1000); //模拟业务处理
                    boolean res2 = lock.tryLock(waitTimeout, leaseTime, TimeUnit.MILLISECONDS);
                    if(res2){
                        System.out.println(threadName + "重新获取锁成功");
                    } else {
                        System.out.println(threadName + "重新获取锁失败");
                    }
                    //释放锁
                    lock.unlock();
                    System.out.println(threadName + "释放锁");
                } else {
                    System.out.println(threadName + "没有获取到锁");
                }
            } catch (Exception e) {
                System.out.println(threadName + "获取锁失败");
            }
        }, "线程" + i).start();
    }
    //等待所有线程执行完
    Thread.sleep(3000);
}

执行结果:

复制代码
线程1获取到锁
线程2没有获取到锁
线程4没有获取到锁
线程3没有获取到锁
线程5没有获取到锁
线程1重新获取锁成功
线程1释放锁

可以看到Redission将原子性的操作都封装起来了,我们只需要调用tryLock尝试获取锁并设置过期时间,并且再次tryLock时能够重入获取到锁;其他线程实例也能在给定时间内重试获取锁。

相关推荐
程序员爱钓鱼9 小时前
Rust 开发环境安装(Windows|macOS|Linux)
后端·rust
爱勇宝17 小时前
第 1 章:别把“需求文档”当成真正的需求
前端·后端·程序员
IT_陈寒1 天前
闭包陷阱让我加了两天班,JavaScript你真行
前端·人工智能·后端
易协同低代码1 天前
通达OA核心类库TD类深度解析
后端
Gopher_HBo1 天前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志1 天前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..1 天前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects1 天前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰1 天前
一些基础和问题解决
后端
陈随易1 天前
前端项目部署只要30秒
前端·后端·程序员