百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

高并发必备,使用Redis分布式锁必须注意的10个细节

mhr18 2024-12-04 14:01 17 浏览 0 评论

在分布式系统中,实现分布式锁是一项常见的需求。为了追求性能,通常使用Redis使用分布式锁,但是想要实现高性能并且数据安全的分布式锁,并非易事,先看一下分布式锁要满足哪些特性。

分布式锁需要满足以下特性:

  1. 互斥,一个线程获取到锁,其他线程只能等待。
  2. 高性能,比如MySQL基于磁盘操作实现,性能较差。
  3. 加锁操作是原子的
  4. 释放锁操作是原子的
  5. 避免释放锁失败
  6. 避免提前释放锁,比如释放锁操作加在事务里面,就会出现事务提交前,已经释放锁。
  7. 加锁操作支持阻塞,避免其他线程不断轮询,浪费CPU。
  8. 支持设置锁过期时间,防止释放锁失败,作为兜底策略。
  9. 支持断开客户端连接后,自动释放锁(例如zookeeper临时节点)。
  10. 释放锁的时候判断是否属于当前线程,避免释放了其他线程的锁。
  11. 支持锁可重入,避免当前线程多次加锁的时候,出现死锁。
  12. 支持锁自动续期,如果已经给锁设置了过期时间,可能会出现业务还没执行完成,锁已经过期。

使用Redis实现分布式锁有以下10种方式,每种方式都有各自的优缺点,一起分析一下。

使用 setnx 命令

可以使用 Redis 提供的 setnx 命令实现分布式锁,setnx 命令的作用是:

  1. 如果键 key 不存在,则将键 key 的值设置为 value,同时返回 1;
  2. 如果键 key 已经存在,则不做任何操作,返回 0。

伪代码实现如下:

// 判断加锁是否成功
if (jedis.setnx(lock_key, lock_value) == 1) {
    // 执行业务代码
    doBusiness();
    // 释放锁
    jedis.del(lock_key);
}

上面代码中有个问题,如果执行业务出现异常了,岂不是永远无法释放锁了,所以业务代码要用 try/finally 包裹起来。

防止释放锁失败

// 判断加锁是否成功
if (jedis.setnx(lock_key, lock_value) == 1) {
    try {
        // 执行业务代码
        doBusiness();
    } finally {
        // 释放锁
        jedis.del(lock_key);
    }
}

这样还是有问题的,如果执行业务的时候服务器宕机了怎么办?还是无法释放锁,所以要给锁加上过期时间,到期后可以自动释放锁。

加过期时间

// 判断加锁是否成功
if (jedis.setnx(lock_key, lock_value) == 1) {
    try {
        // 设置过期时间
        jedis.expire(lock_key, timeout);
        // 执行业务代码
        doBusiness();
    } finally {
        // 释放锁
        jedis.del(lock_key);
    }
}

由于加锁和设置过期时间,两个操作不是原子的。可能出现刚加锁成功,还没来得及设置过期时间,服务器就宕机了,造成永远无法释放锁。 Redis 2.6.12 版本 提供的 setnx 命令,同时可以设置过期时间,是原子操作。 SET key value [EX seconds] [PX milliseconds] [NX|XX]

  • 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 :只在键已经存在时,才对键进行设置操作。

原子加锁

// 设置 nx 和 ex 参数
SetParams setParams = new SetParams();
setParams.nx();
setParams.ex(timeout);

// 判断加锁是否成功
if (jedis.set(lock_key, lock_value, setParams) == 1) {
    try {
        // 执行业务代码
        doBusiness();
    } finally {
        // 释放锁
        jedis.del(lock_key);
    }
}

释放锁流程有问题,释放锁的时候,没有判断是否是当前线程的 lock_value,可能会释放了其他线程的锁。 例如下面的情况:

  1. A线程加锁成功
  2. A线程执行业务代码耗时过长,锁过期,自动释放锁。
  3. B线程抢占锁
  4. A线程执行结束,释放锁(由于 lock_key 相同,没有判断 lock_value,释放了B线程的锁)

释放了其他线程的锁

所以释放锁的时候,要判断 lock_value 是否属于当前线程。

// 设置 nx 和 ex 参数
SetParams setParams = new SetParams();
setParams.nx();
setParams.ex(timeout);
// 判断加锁是否成功
if (jedis.set(lock_key, lock_value, setParams) == 1) {
    try {
        // 设置过期时间
        jedis.set()
        jedis.expire(lock_key, timeout);
        // 执行业务代码
        doBusiness();
    } finally {
        // 释放锁,判断 lock_value 是否属于当前线程
        if ("lock_value".equals(jedis.get(lock_key))) {
            jedis.del(lock_key);
        }
    }
}

又有个很明显的问题,释放锁流程不是原子操作,可能存在刚判断是否相等,已经被其他线程抢占锁了,导致又释放了其他线程的锁,可以使用 Lua 脚本实现原子操作。

释放锁原子操作

final String releaseLockScript = "if redis.call('get', KEYS[1]) == ARGV[1] then\n" +
        "   return redis.call('del', KEYS[1])\n" +
        "else\n" +
        "   return 0\n" +
        "end";

// 设置 nx 和 ex 参数
SetParams setParams = new SetParams();
setParams.nx();
setParams.ex(timeout);
// 判断加锁是否成功
if (jedis.set(lock_key, lock_value, setParams) == 1) {
    try {
        // 设置过期时间
        jedis.set()
        jedis.expire(lock_key, timeout);
        // 执行业务代码
        doBusiness();
    } finally {
        // 释放锁,原子操作
        jedis.eval(releaseLockScript, Collections.singletonList(lock_key), Collections.singletonList(lock_value))
    }
}

上面的操作流程也不是完美的,刚才分析过,如果执行业务代码耗时过长,锁过期了,就会出现其他线程抢占锁,所以就需要锁自动续期。

锁自动续期

可以使用下面方式实现锁自动续期:

  1. 加锁成功后,启动一个定时任务,每个一段时间检测是否执行完成业务代码,依据是锁是否还存在。
  2. 这个定时任务每隔三分之一时间检查一次,比如锁过期时间是30秒,就每隔10秒检查一次。
  3. 如果锁还存在,就把锁过期时间继续设置成30秒。

Redisson框架已经实现了这个流程,名叫看门狗机制(Watch Dog),底层使用 Lua 脚本实现。

// 获取 Redisson 锁
RLock lock = redissonClient.getLock(lock_key);
try {
    // 加锁,并设置3秒后过期
    lock.lock(3, TimeUnit.SECONDS);
    // 执行业务代码
    doBusiness();
} catch (Exception e) {
    System.out.println("加锁超时");
} finally {
    // 释放锁
    lock.unlock();
}

事务提交前,释放锁

@Transactional
public void updateDB(String lockKey) {
    // 加锁
    boolean lockFlag = redisLock.lock(lockKey);
    // 判断是否加锁成功
    if (lockFlag) {
        // 执行业务代码
        doBusiness();
        // 释放锁
        redisLock.unlock(lockKey);
    }
}

如果使用注解事务,并把加锁和释放锁的逻辑放在事务里面,就可能会出现事务提交前,锁已经释放,导致互斥锁失效。正确的做法是,在开启事务前加锁,在提交事务后释放锁。

public void lock(String lockKey) {
    // 加锁
    boolean lockFlag = redisLock.lock(lockKey);
    // 判断是否加锁成功
    if (lockFlag) {
        // 事务方法单独执行
        updateDB(lockKey);
        // 释放锁
        redisLock.unlock(lockKey);
    }
}

@Transactional
public void updateDB(String lockKey) {
    // 执行业务代码
    doBusiness();
}

注意不要调用本类的事务方法,会导致事务失效,上面为了演示加锁、释放锁要跟事务分开。

可重入锁

可重入锁是允许同一个线程多次获取同一把锁。 如果锁不支持可重入的话,在递归调用的场景下,会出现死锁情况。 实现可重入锁,也比较简单,只需要增加一个计数器,记录当前线程加锁次数。每加一次锁,计数器加一,释放锁,计数器减一,计数器为零的时候,释放锁。

集群加锁

在对Redis集群加锁的时候,由于故障转移的原因可能会导致互斥锁失效。比如下面的情况:

  1. 线程A对集群master节点加锁成功
  2. 锁数据还没有来得及同步到slave节点,master节点崩溃了。
  3. 集群自动故障转移,slave节点被选举成新的master节点,对外提供服务。
  4. 线程B对集群master节点加锁成功,互斥锁失效。

Redlock(Redis Distributed Lock,Redis分布式锁,简称红锁)实现对集群加锁的功能。 实现原理如下:

  1. 使用多个master节点组成Redis集群(假设有5个master节点)
  2. 客户端加锁的时候,按照顺序对5个master节点请求加锁,要求所有请求锁超时时间之和要小于锁过期时间。假设锁过期时间是5秒,则对每个master节点请求锁超时时间最长为一秒,超过一秒还没有加锁成功,就放弃加锁,继续请求下一个master节点。
  3. 如果对超过半数的master节点加锁成功,视为获取锁成功。这里是需要对3个master节点加锁成功(N/2+1)。如果没有获取锁成功,则需要释放已经加锁的master节点,避免影响其他客户端获取锁。
  4. 锁的实际过期时间等于锁的初始过期时间减去获取锁的时间,比如锁初始过期时间是5秒,获取锁用了2秒,锁实际过期时间是3秒。

相关推荐

SpringBoot 各种分页查询方式详解(全网最全)

一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...

《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略

《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...

LoadRunner(loadrunner录制不到脚本)

一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...

Redis数据类型介绍(redis 数据类型)

介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...

RMAN备份监控及优化总结(rman备份原理)

今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...

备份软件调用rman接口备份报错RMAN-06820 ORA-17629 ORA-17627

一、报错描述:备份归档报错无法连接主库进行归档,监听问题12541RMAN-06820:WARNING:failedtoarchivecurrentlogatprimarydatab...

增量备份修复物理备库gap(增量备份恢复数据库步骤)

适用场景:主备不同步,主库归档日志已删除且无备份.解决方案:主库增量备份修复dg备库中的gap.具体步骤:1、停止同步>alterdatabaserecovermanagedstand...

一分钟看懂,如何白嫖sql工具(白嫖数据库)

如何白嫖sql工具?1分钟看懂。今天分享一个免费的sql工具,毕竟现在比较火的NavicatDbeaverDatagrip都需要付费才能使用完整功能。幸亏今天有了这款SQLynx,它不仅支持国内外...

「开源资讯」数据管理与可视化分析平台,DataGear 1.6.1 发布

前言数据齿轮(DataGear)是一款数据库管理系统,使用Java语言开发,采用浏览器/服务器架构,以数据管理为核心功能,支持多种数据库。它的数据模型并不是原始的数据库表,而是融合了数据库表及表间关系...

您还在手工打造增删改查代码么,该神器带你脱离苦海

作为Java开发程序,日常开发中,都会使用Spring框架,完成日常的功能开发;在相关业务系统中,难免存在各种增删改查的接口需求开发。通常来说,实现增删改查有如下几个方式:纯手工打造,编写各种Cont...

Linux基础知识(linux基础知识点及答案)

系统目录结构/bin:命令和应用程序。/boot:这里存放的是启动Linux时使用的一些核心文件,包括一些连接文件以及镜像文件。/dev:dev是Device(设备)的缩写,该目录...

PL/SQL 杂谈(二)(pl/sql developer使用)

承接(一)部分。我们从结构和功能这两个方面展示PL/SQL的关键要素。可以看看PL/SQL的优雅的代码。写出一个好的代码,就和文科生写出一篇优秀的作文一样,那么赏心悦目。1、与SQL的集成PL/S...

电商ERP系统哪个好用?(电商erp哪个好一点)

电商ERP系统哪个好用?做电商的,谁还没被ERP折腾过?有老板说:“我们早就上了ERP,订单、库存、财务全搞定,系统用得飞起。”也有运营吐槽:“系统是上了,可库存老不准,订单漏单错单天天有,财务对账还...

汽车检测线系统实例,看集中控制与PLC分布控制

PLC可编程控制器,上个世纪70年代初,为取代早期继电器控制线路,开始采取存储指令方式,完成顺序控制而设计的。开始仅有逻辑运算、计时、计数等简单功能。随着微处理的发展,PLC可编程能力日益提高,已经能...

苹果五件套成公司年会奖品主角,几大小技巧教你玩转苹果新品

钱江晚报·小时新闻记者张云山随着春节的临近,各家大公司的年会又将陆续上演。上周,各大游戏公司的年会大奖,苹果五件套又成了标配。在上海的游戏公司中,莉莉丝奖品列表拉得相当长,从特等奖到九等奖还包含了特...

取消回复欢迎 发表评论: