聊聊分布式锁——Redis和Redisson的方式

本文探讨了分布式锁的概念,强调了其在分布式系统中的重要性。介绍了基于Redis和Redisson实现分布式锁的方式,包括Redis的原子性操作和Redisson的封装特性。文章提到了Redis实现分布式锁的步骤,如设置过期时间、原子性加锁和解锁,以及Redisson如何简化这一过程。还讨论了Redisson提供的看门狗机制和RedLock算法以增强锁的可靠性。
摘要由CSDN通过智能技术生成

聊聊分布式锁——Redis和Redisson的方式

一、什么是分布式锁
分布式~~锁,要这么念,首先得是『分布式』,然后才是『锁』
分布式:这里的分布式指的是分布式系统,涉及到好多技术和理论,包括CAP 理论、分布式存储、分布式事务、分布式锁…

分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。

分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据。

锁:对对,就是你想的那个,Javer 学的第一个锁应该就是 synchronized
Java 初级面试问题,来拼写下 赛克瑞纳挨日的

从锁的使用场景有来看下边这 3 种锁:

线程锁:synchronized 是用在方法或代码块中的,我们把它叫『线程锁』,线程锁的实现其实是靠线程之间共享内存实现的,说白了就是内存中的一个整型数,有空闲、上锁这类状态,比如 synchronized 是在对象头中的 Mark Word 有个锁状态标志,Lock 的实现类大部分都有个叫 volatile int state 的共享变量来做状态标志。

进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过 synchronized 等线程锁实现进程锁。比如说,我们的同一个 linux 服务器,部署了好几个 Java 项目,有可能同时访问或操作服务器上的相同数据,这就需要进程锁,一般可以用『文件锁』来达到进程互斥。

分布式锁:随着用户越来越多,我们上了好多服务器,原本有个定时给客户发邮件的任务,如果不加以控制的话,到点后每台机器跑一次任务,客户就会收到 N 条邮件,这就需要通过分布式锁来互斥了。

书面解释:分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥来防止彼此干扰来保证一致性。

知道了什么是分布式锁,接下来就到了技术选型环节

二、分布式锁要怎么搞
要实现一个分布式锁,我们一般选择集群机器都可以操作的外部系统,然后各个机器都去这个外部系统申请锁。

这个外部系统一般需要满足如下要求才能胜任:

互斥:在任意时刻,只能有一个客户端能持有锁。
防止死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。所以锁一般要有一个过期时间。
独占性:解铃还须系铃人,加锁和解锁必须是同一个客户端,一把锁只能有一把钥匙,客户端自己的锁不能被别人给解开,当然也不能去开别人的锁。
容错:外部系统不能太“脆弱”,要保证外部系统的正常运行,客户端才可以加锁和解锁。
我觉得可以这么类比:

好多商贩要租用某个仓库,同一时刻,只能给一个商贩租用,且只能有一把钥匙,还得有固定的“租期”,到期后要回收的,当然最重要的是仓库门不能坏了,要不锁都锁不住。这不就是分布式锁吗?

感慨自己真是个爱技术爱生活的程序猿~~

其实锁,本质上就是用来进行防重操作的(数据一致性),像查询这种幂等操作,就不需要费这劲

直接上结论:

分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于 Redis 的分布式锁;3. 基于 ZooKeeper 的分布式锁。

但为了追求更好的性能,我们通常会选择使用 Redis 或 Zookeeper 来做。

想必也有喜欢问为什么的同学,那数据库客观锁怎么就性能不好了?

使用数据库乐观锁,包括主键防重,版本号控制。但是这两种方法各有利弊。

使用主键冲突的策略进行防重,在并发量非常高的情况下对数据库性能会有影响,尤其是应用数据表和主键冲突表在一个库的时候,表现更加明显。还有就是在 MySQL 数据库中采用主键冲突防重,在大并发情况下有可能会造成锁表现象,比较好的办法是在程序中生产主键进行防重。

使用版本号策略

这个策略源于 MySQL 的 MVCC 机制,使用这个策略其实本身没有什么问题,唯一的问题就是对数据表侵入较大,我们要为每个表设计一个版本号字段,然后写一条判断 SQL 每次进行判断。

第三趴,编码

三、基于 Redis 的分布式锁
其实 Redis 官网已经给出了实现:https://redis.io/topics/distlock,说各种书籍和博客用了各种手段去用 Redis 实现分布式锁,建议用 Redlock 实现,这样更规范、更安全。我们循序渐进来看

我们默认指定大家用的是 Redis 2.6.12 及更高的版本,就不再去讲 setnx、expire 这种了,直接 set 命令加锁

set key value[expiration EX seconds|PX milliseconds] [NX|XX] 

eg:

SET resource_name my_random_value NX PX 30000 

SET 命令的行为可以通过一系列参数来修改

EX second :设置键的过期时间为 second 秒。SET key value EX second 效果等同于 SETEX key second value 。
PX millisecond :设置键的过期时间为 millisecond 毫秒。SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
NX :只在键不存在时,才对键进行设置操作。SET key value NX 效果等同于 SETNX key value 。
XX :只在键已经存在时,才对键进行设置操作。
这条指令的意思:当 key——resource_name 不存在时创建这样的key,设值为 my_random_value,并设置过期时间 30000 毫秒。

别看这干了两件事,因为 Redis 是单线程的,这一条指令不会被打断,所以是原子性的操作。

Redis 实现分布式锁的主要步骤:

指定一个 key 作为锁标记,存入 Redis 中,指定一个 唯一的标识 作为 value。
当 key 不存在时才能设置值,确保同一时间只有一个客户端进程获得锁,满足 互斥性 特性。
设置一个过期时间,防止因系统异常导致没能删除这个 key,满足 防死锁 特性。
当处理完业务之后需要清除这个 key 来释放锁,清除 key 时需要校验 value 值,需要满足 解铃还须系铃人 。
设置一个随机值的意思是在解锁时候判断 key 的值和我们存储的随机数是不是一样,一样的话,才是自己的锁,直接 del 解锁就行。

当然这个两个操作要保证原子性,所以 Redis 给出了一段 lua 脚本(Redis 服务器会单线程原子性执行 lua 脚本,保证 lua 脚本在处理的过程中不会被任意其它请求打断。):

if redis.call("get",KEYS[1]) == ARGV[1] then 
    return redis.call("del",KEYS[1]) 
else 
    return 0 
end 

问题:
我们先抛出两个问题思考:

获取锁时,过期时间要设置多少合适呢?

预估一个合适的时间,其实没那么容易,比如操作资源的时间最慢可能要 10 s,而我们只设置了 5 s 就过期,那就存在锁提前过期的风险。这个问题先记下,我们先看下 Javaer 要怎么在代码中用 Redis 锁。

容错性如何保证呢?

Redis 挂了怎么办,你可能会说上主从、上集群,但也会出现这样的极端情况,当我们上锁后,主节点就挂了,这个时候还没来的急同步到从节点,主从切换后锁还是丢了

带着这两个问题,我们接着看

Redisson 实现代码
redisson 是 Redis 官方的分布式锁组件。GitHub 地址:https://github.com/redisson/redisson

Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的 Java 常用对象,还实现了可重入锁(Reentrant Lock)、公平锁(Fair Lock、联锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)等,还提供了许多分布式服务。Redisson 提供了使用 Redis 的最简单和最便捷的方法。Redisson 的宗旨是促进使用者对 Redis 的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

redisson 现在已经很强大了,github 的 wiki 也很详细,分布式锁的介绍直接戳 Distributed locks and synchronizers

Redisson 支持单点模式、主从模式、哨兵模式、集群模式,只是配置的不同,我们以单点模式来看下怎么使用,代码很简单,都已经为我们封装好了,直接拿来用就好,详细的demo,我放在了 github: starfish-learn-redisson 上,这里就不一步步来了

RLock lock = redisson.getLock("myLock"); 

RLock 提供了各种锁方法,我们来解读下这个接口方法,

注:代码为 3.16.2 版本,可以看到继承自 JDK 的 Lock 接口,和 Reddsion 的异步锁接口 RLockAsync(这个我们先不研究)

RLock
在这里插入图片描述

public interface RLock extends Lock, RLockAsync {
    
 
    /** 
     * 获取锁的名字 
     */ 
    String getName(); 
     
    /** 
     * 这个叫终端锁操作,表示该锁可以被中断 假如A和B同时调这个方法,A获取锁,B为获取锁,那么B线程可以通过 
     * Thread.currentThread().interrupt(); 方法真正中断该线程 
     */ 
    void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException; 
 
    /** 
     * 这个应该是最常用的,尝试获取锁 
     * waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败 
     * leaseTime   锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完) 
     */ 
    boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException; 
 
    /** 
     * 锁的有效期设置为 leaseTime,过期后自动失效 
     * 如果 leaseTime 设置为 -1, 表示不主动过期 
     */ 
    void lock(long leaseTime, TimeUnit unit); 
 
    /** 
     * Unlocks the lock independently of its state 
     */ 
    boolean forceUnlock(); 
 
    /** 
     * 检查是否被另一个线程锁住 
     */ 
    boolean isLocked(); 
 
    /** 
     * 检查当前线线程是否持有该锁 
     */ 
    boolean isHeldByCurrentThread(); 
   
     /** 
     *  这个就明了了,检查指定线程是否持有锁 
     */ 
    boolean isHeldByThread(long threadId); 
 
    /** 
     * 返回当前线程持有锁的次数 
     */ 
    int getHoldCount(); 
 
    /** 
     * 返回锁的剩余时间 
     * @return time in milliseconds 
     *          -2 if the lock does not exist. 
     *          -1 if the lock exists but has no associated expire. 
     */ 
    long remainTimeToLive(); 
     
} 

Demo
就是这么简单,Redisson 已经做好了封装,使用起来 so easy,如果使用主从、哨兵、集群这种也只是配置不同。

原理
看源码小 tips,最好是 fork 到自己的仓库,然后拉到本地,边看边注释,然后提交到自己的仓库,也方便之后再看,不想这么麻烦的,也可以直接看我的 Jstarfish/redisson

先看下 RLock 的类关系
在这里插入图片描述
跟着源码,可以发现 RedissonLock 是 RLock 的直接实现,也是我们加锁、解锁操作的核心类

加锁
主要的加锁方法就下边这两个,区别也很简单,一个有等待时间,一个没有,所以我们挑个复杂的看(源码包含了另一个的绝大部分)

boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException; 
void lock(long leaseTime, TimeUnit unit); 

RedissonLock.tryLock

@Override 
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
    
    // 获取等锁的最长时间 
    long time = unit.toMillis(waitTime); 
    long current = System.currentTimeMillis(); 
    //取得当前线程id(判断是否可重入锁的关键) 
    long threadId = Thread.currentThread().getId(); 
    // 【核心点1】尝试获取锁,若返回值为null,则表示已获取到锁,返回的ttl就是key的剩余存活时间 
    Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId); 
    if (ttl == null) {
    
        return true; 
    } 
    // 还可以容忍的等待时长 = 获取锁能容忍的最大等待时长 - 执行完上述操作流程的时间 
    time -= System.currentTimeMillis() - current; 
    if (time <= 0) {
    
        //等不到了,直接返回失败 
        acquireFailed(waitTime, unit, threadId); 
        return false; 
    } 
 
    current = System.currentTimeMillis(); 
    /** 
     * 【核心点2】 
     * 订阅解锁消息 redisson_lock__channel:{$KEY},并通过await方法阻塞等待锁释放,解决了无效的锁申请浪费资源的问题: 
     * 基于信息量,当锁被其它资源占用时,当前线程通过 Redis 的 channel 订阅锁的释放事件,一旦锁释放会发消息通知待等待的线程进行竞争 
     * 当 this.await返回false,说明等待时间已经超出获取锁最大等待时间,取消订阅并返回获取锁失败 
     * 当 this.await返回true,进入循环尝试获取锁 
     */ 
    RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId); 
    //await 方法内部是用CountDownLatch来实现阻塞,获取subscribe异步执行的结果(应用了Netty 的 Future) 
    if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
    
        if (!subscribeFuture.cancel(false)) {
    
            subscribeFuture.onComplete((res, e) -> {
    
                if (e == null) {
    
                    unsubscribe(subscribeFuture, threadId)
  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值