【Redis】Redis 分布式锁

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 【Redis】Redis 分布式锁

一、分布式锁概念

随着业务发展的需要,原单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的 Java API 并不能提供分布式锁的能力。为了解决这个问题就需要一种跨 JVM 的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

说得通俗些,集群中上了锁后,无论当前操作在哪台机器,所有的机器都会识别并且等待,锁释放后其他操作才能进行,这就是分布式锁,对所有集群里都有效

分布式锁主流的实现方案:

  1. 基于数据库实现分布式锁
  2. 基于缓存(Redis 等)
  3. 基于 Zookeeper

每一种分布式锁解决方案都有各自的优缺点,其中redis性能最高zookeeper可靠性最高

二、使用setnx实现锁


set stu:1:info “OK” nx px 10000
  • 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 :只在键已经存在时,才对键进行设置操作

image.png

  • 多个客户端同时获取锁(setnx)
  • 获取成功,执行业务逻辑(从 db 获取数据,放入缓存),执行完成释放锁(del)
  • 获取失败的客户端则等待重试

image.png

用setnx和del添加以及释放锁

image.png

一般地,我们需要给锁设置过期时间防止锁被长期占用

image.png

这里有个问题:加锁和设置过期时间是两个操作,而不是同时进行操作的,如果上锁后发生异常情况,就无法设置过期时间了。我们可以上锁的同时设置过期时间

image.png

三、编写代码测试分布式锁

1. 使用Java代码测试分布式锁

首先在redis中设置num的值为0,编写Java代码进行测试

下方代码做的就是:获取到锁则num++,并释放锁;没获取到则0.1秒后重新获取

image.png

重启,服务集群,通过网关压力测试:ab -n 5000 -c 100 http://192.168.140.1:8080/test/testLock

image.png

查看 redis 中 num 的值

image.png

问题:  setnx 刚好获取到锁,业务逻辑出现异常,导致锁无法释放

解决:  设置过期时间,自动释放锁

2. 优化之设置锁的过期时间

设置过期时间有两种方式:

  • 首先想到通过 expire 设置过期时间(缺乏原子性:如果在 setnx 和 expire 之 间出现异常,锁也无法释放)
  • 在 set 的同时指定过期时间(推荐)

image.png

代码中设置过期时间:

image.png

问题:  可能会释放其他服务器的锁

如果业务逻辑的执行时间是 7s,执行流程如下:

  • index1 业务逻辑没执行完,3 秒后锁被自动释放
  • index2 获取到锁,执行业务逻辑,3 秒后锁被自动释放
  • index3 获取到锁,执行业务逻辑
  • index1 业务逻辑执行完成,开始调用 del 释放锁,这时释放的是 index3 的锁, 导致 index3 的业务只执行 1s 就被别人释放。
    最终等于没锁的情况

image.png

a在操作时卡顿了,导致锁超时后自动释放;释放后,b抢到锁进行操作;此时a操作完成,手动释放锁,这就把b的锁给释放了,b再释放锁则会报错

解决:  setnx 获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这 个值,判断是否自己的锁

四、优化之给lock设置UUID防误删

image.png

image.png

五、使用LUA脚本保证删除的原子性

使用lock的uuid可以一定程度上缓解线程释放其他锁,但并不能完全解决这种情况。因为比较uuid和删除lock并不是原子性的

image.png

问题:  a比较uuid通过后,锁到期了自动释放,b重新加锁,a此时会手动释放b的锁,这还是出现问题

解决:  使用LUA 脚本保证删除的原子性

LUA脚本:

  • 将复杂的或者多步的 redis 操作,写为一个脚本,一次提交给 redis 执行,减少反复连接 redis 的次数,提升性能
  • LUA 脚本是类似 redis 事务,有一定的原子性,不会被其他命令插队,可以完成一些redis 事务性的


@GetMapping("testLockLua")
public void testLockLua() {
    //1 声明一个 uuid ,将做为一个 value 放入我们的 key 所对应的值中
    String uuid = UUID.randomUUID().toString();
    //2 定义一个锁:lua 脚本可以使用同一把锁,来实现删除!
    String skuId = "25"; // 访问 skuId 为 25 号的商品 100008348542
    String locKey = "lock:" + skuId; // 锁住的是每个商品的数据
    // 3 获取锁
    Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS);
    // 第一种: lock 与过期时间中间不写任何的代码。
    // redisTemplate.expire("lock",10, TimeUnit.SECONDS);//设置过期时间
    // 如果 true
    if (lock) {
        // 执行的业务逻辑开始
        // 获取缓存中的 num 数据
        Object value = redisTemplate.opsForValue().get("num");
        // 如果是空直接返回
        if (StringUtils.isEmpty(value)) {
            return;
        }
        // 不是空 如果说在这出现了异常! 那么 delete 就删除失败! 也就是说锁永远存在!
        int num = Integer.parseInt(value + "");
        // 使 num 每次+1 放入缓存
        redisTemplate.opsForValue().set("num", String.valueOf(++num));
        /*使用 lua 脚本来锁*/
        // 定义 lua 脚本:将判断和删除操作同时进行
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        // 使用 redis 执行 lua 执行
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(script);
        // 设置一下返回值类型 为 Long
        // 因为删除判断的时候,返回的 0,给其封装为数据类型。如果不封装那么默认返回 String 类型,
        // 那么返回字符串与 0 会有发生错误。
        redisScript.setResultType(Long.class);
        // 第一个是执行的 script 脚本 ,第二个需要判断的 key,第三个就是 key 所对应的值。
        redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);
        } else {
        // 其他线程等待
        try {
            // 睡眠
            Thread.sleep(1000);
            // 睡醒了之后,调用方法。
            testLockLua();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

image.png

image.png

为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

  • 互斥性;在任意时刻,只有一个客户端能持有锁
  • 不会发生死锁;即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁(设置lock的过期时间)
  • 解铃还须系铃人;加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了(使用LUA脚本和uuid)
  • 加锁和解锁必须具有原子性(使用LUA脚本)



相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
7天前
|
NoSQL Java 中间件
【📕分布式锁通关指南 02】基于Redis实现的分布式锁
本文介绍了从单机锁到分布式锁的演变,重点探讨了使用Redis实现分布式锁的方法。分布式锁用于控制分布式系统中多个实例对共享资源的同步访问,需满足互斥性、可重入性、锁超时防死锁和锁释放正确防误删等特性。文章通过具体示例展示了如何利用Redis的`setnx`命令实现加锁,并分析了简化版分布式锁存在的问题,如锁超时和误删。为了解决这些问题,文中提出了设置锁过期时间和在解锁前验证持有锁的线程身份的优化方案。最后指出,尽管当前设计已解决部分问题,但仍存在进一步优化的空间,将在后续章节继续探讨。
416 131
【📕分布式锁通关指南 02】基于Redis实现的分布式锁
|
10天前
|
NoSQL Java Redis
Springboot使用Redis实现分布式锁
通过这些步骤和示例,您可以系统地了解如何在Spring Boot中使用Redis实现分布式锁,并在实际项目中应用。希望这些内容对您的学习和工作有所帮助。
134 83
|
5天前
|
缓存 NoSQL 搜索推荐
【📕分布式锁通关指南 03】通过Lua脚本保证redis操作的原子性
本文介绍了如何通过Lua脚本在Redis中实现分布式锁的原子性操作,避免并发问题。首先讲解了Lua脚本的基本概念及其在Redis中的使用方法,包括通过`eval`指令执行Lua脚本和通过`script load`指令缓存脚本。接着详细展示了如何用Lua脚本实现加锁、解锁及可重入锁的功能,确保同一线程可以多次获取锁而不发生死锁。最后,通过代码示例演示了如何在实际业务中调用这些Lua脚本,确保锁操作的原子性和安全性。
32 6
【📕分布式锁通关指南 03】通过Lua脚本保证redis操作的原子性
|
18天前
|
缓存 NoSQL 中间件
Redis,分布式缓存演化之路
本文介绍了基于Redis的分布式缓存演化,探讨了分布式锁和缓存一致性问题及其解决方案。首先分析了本地缓存和分布式缓存的区别与优劣,接着深入讲解了分布式远程缓存带来的并发、缓存失效(穿透、雪崩、击穿)等问题及应对策略。文章还详细描述了如何使用Redis实现分布式锁,确保高并发场景下的数据一致性和系统稳定性。最后,通过双写模式和失效模式讨论了缓存一致性问题,并提出了多种解决方案,如引入Canal中间件等。希望这些内容能为读者在设计分布式缓存系统时提供有价值的参考。感谢您的阅读!
109 6
Redis,分布式缓存演化之路
|
2月前
|
存储 NoSQL Java
使用lock4j-redis-template-spring-boot-starter实现redis分布式锁
通过使用 `lock4j-redis-template-spring-boot-starter`,我们可以轻松实现 Redis 分布式锁,从而解决分布式系统中多个实例并发访问共享资源的问题。合理配置和使用分布式锁,可以有效提高系统的稳定性和数据的一致性。希望本文对你在实际项目中使用 Redis 分布式锁有所帮助。
229 5
|
3月前
|
NoSQL Java 数据处理
基于Redis海量数据场景分布式ID架构实践
【11月更文挑战第30天】在现代分布式系统中,生成全局唯一的ID是一个常见且重要的需求。在微服务架构中,各个服务可能需要生成唯一标识符,如用户ID、订单ID等。传统的自增ID已经无法满足在集群环境下保持唯一性的要求,而分布式ID解决方案能够确保即使在多个实例间也能生成全局唯一的标识符。本文将深入探讨如何利用Redis实现分布式ID生成,并通过Java语言展示多个示例,同时分析每个实践方案的优缺点。
106 8
|
3月前
|
NoSQL Redis
Redis分布式锁如何实现 ?
Redis分布式锁通过SETNX指令实现,确保仅在键不存在时设置值。此机制用于控制多个线程对共享资源的访问,避免并发冲突。然而,实际应用中需解决死锁、锁超时、归一化、可重入及阻塞等问题,以确保系统的稳定性和可靠性。解决方案包括设置锁超时、引入Watch Dog机制、使用ThreadLocal绑定加解锁操作、实现计数器支持可重入锁以及采用自旋锁思想处理阻塞请求。
91 16
|
3月前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
72 5
|
2月前
|
存储 缓存 NoSQL
解决Redis缓存数据类型丢失问题
解决Redis缓存数据类型丢失问题
193 85
|
1月前
|
存储 缓存 NoSQL
云端问道21期方案教学-应对高并发,利用云数据库 Tair(兼容 Redis®*)缓存实现极速响应
云端问道21期方案教学-应对高并发,利用云数据库 Tair(兼容 Redis®*)缓存实现极速响应