Redis分布式锁学习总结

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

⭐️ 前言

想必大家都有过并发编程的经验,在一个单体应用中,可以通过java提供的各种锁机制来控制多线程对于单体应用中同一资源的并发访问;那么在分布式场景下,想要控制多个应用对于同一外部资源的并发访问,就要用到分布式锁。分布式锁不但要保证单个应用程序内部不会产生并发问题,同时也要保证多个应用程序之间不能产生并发问题。分布式锁有很多实现方式,比如使用redis、zookeeper或关系型数据库的唯一索引,也有现成的分布式锁架构,比如redisson、curator等。本文利用spring-data-redis手动实现一个简易的redis分布式锁,剖析redis分布式锁的原理。

⭐️ redis分布式锁实现原理浅析

实现redis分布式锁最简单的想法就是各个应用利用setnx命令向redis中争抢设置key的机会,但我们还应该考虑得更加周全。

死锁

如果成功加锁的应用程序在未释放锁之前就异常终止了,那么这个锁永远无法释放,其他应用程序则永远也无法获取到锁,为了解决这个问题,需要给代表锁的redis的key加上过期时间。

原子性

很多时候我们需要保证多个操作具有原子性,

例如,加锁和设置过期时间

若它们无法保证原子性,则应用程序在刚刚成功加锁后就异常终止了,则仍然会出现上面的死锁问题。

原子性可以通过让redis执行Lua脚本来保证,eval命令可以原子性的执行Lua脚本(Lua的多个步骤会被原子性的执行),在redis内置的Lua脚本中有一个redis对象,可以通过redis.call()方法执行各种redis命令。

防误删

根据上面的讨论,我们需要给redis锁加上过期时间,当业务执行完毕之前锁就过期了,这种情况下,其他线程就会成功加锁,那么之前的程序运行到解锁逻辑时,就会造成对后面线程获得锁的误删。

误删可以通过给每一个线程设置一个id,我们可以叫它 线程标识码

可重入性

另外,在应用程序中免不了方法的彼此调用,若锁无法重入,则业务根本无法执行,比如A方法需要加锁,它在执行过程中会调用B方法,B方法也需要加锁,若锁不具有可重入性,则程序根本无法运行。

可重入性可以通过hash数据结构来实现

key: 代表锁的key

field:线程的唯一标识id,即上文说的 线程标识码

value:重入次数

自动续期

若应用程序执行需要的时间大于锁的过期时间,则锁过期后,应用程序便不再受锁保护,这样就会导致并发问题。所以分布式锁还要具有自动续期的功能,即只要应用程序业务没有执行完毕,则锁需要不断的自动延长过期时间。

自动续期可以通过Timer定时任务配合Lua脚本来实现。

本文参考了上硅谷课程《【尚硅谷】分布式锁全家桶丨一套搞定Redis/Zookeeper/MySQL实现分布式锁》,B站上就有,更详细的内容读者可以去看这门课程。

这里贴出关键代码,完整代码我已经上传到了gitee。欢迎围观啊!!点这里哟

⭐️ 加锁主要代码

/**
     * 加锁
     * @param time
     * @param unit
     * @return
     * @throws InterruptedException
     */
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        if (time != -1){
            this.expire = unit.toSeconds(time);
        }
        // redis加锁的lua脚本
        String lockStr="if redis.call('exists', KEYS[1])==0 or redis.call('hexists', KEYS[1], ARGV[1])==1 " +
                "then " +
                "redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
                "redis.call('expire', KEYS[1], ARGV[2]) " +
                "return 1 " +
                "end " +
                "return 0";
        // 加锁
        while (!this.redisTemplate.execute(new DefaultRedisScript<>(lockStr, Boolean.class), Arrays.asList(this.lockName), this.uuid, String.valueOf(this.expire))){
            Thread.sleep(50);
        }
        // 自动续期
        this.autoExpire();
        return true;
    }

⭐️ 解锁主要代码

/**
     * 解锁
     */
    @Override
    public void unlock() {
        // 解锁lua脚本
        String unlockStr = "if redis.call('hexists', KEYS[1], ARGV[1])==0 " +
                "then " +
                "return nil " +
                "end " +
                "if redis.call('hincrby', KEYS[1], ARGV[1], -1)==0 " +
                "then " +
                "return redis.call('del', KEYS[1]) " +
                "end " +
                "return 0";
        // 解锁
        Long del = this.redisTemplate.execute(new DefaultRedisScript<>(unlockStr, Long.class), Arrays.asList(this.lockName), this.uuid);
        if (del == null){
            throw new IllegalMonitorStateException("lock wrong");
        }
        if (del == 1L){
            System.out.println("lock deleted");
        }
    }

⭐️ 自动续期主要代码

/**
     * 自动续期
     */
    private void autoExpire(){
        String expireStr="if redis.call('hexists', KEYS[1], ARGV[1])==1 " +
                         "then return redis.call('expire', KEYS[1], ARGV[2]) " +
                         "end " +
                         "return 0";
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                if (redisTemplate.execute(new DefaultRedisScript<>(expireStr, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire))){
                    autoExpire();
                }
            }
        }, this.expire * 1000 /3);
    }

⭐️ 运行架构

运行架构比较简单,可以启动两个应用实例,利用nginx做负载均衡。

⭐️ 压力测试

压力测试可以使用jmeter,其设置如下图所示

笔者水平有限,若有不对的地方欢迎评论指正!

相关实践学习
基于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
目录
打赏
0
0
0
0
21
分享
相关文章
分布式爬虫框架Scrapy-Redis实战指南
本文介绍如何使用Scrapy-Redis构建分布式爬虫系统,采集携程平台上热门城市的酒店价格与评价信息。通过代理IP、Cookie和User-Agent设置规避反爬策略,实现高效数据抓取。结合价格动态趋势分析,助力酒店业优化市场策略、提升服务质量。技术架构涵盖Scrapy-Redis核心调度、代理中间件及数据解析存储,提供完整的技术路线图与代码示例。
分布式爬虫框架Scrapy-Redis实战指南
【📕分布式锁通关指南 02】基于Redis实现的分布式锁
本文介绍了从单机锁到分布式锁的演变,重点探讨了使用Redis实现分布式锁的方法。分布式锁用于控制分布式系统中多个实例对共享资源的同步访问,需满足互斥性、可重入性、锁超时防死锁和锁释放正确防误删等特性。文章通过具体示例展示了如何利用Redis的`setnx`命令实现加锁,并分析了简化版分布式锁存在的问题,如锁超时和误删。为了解决这些问题,文中提出了设置锁过期时间和在解锁前验证持有锁的线程身份的优化方案。最后指出,尽管当前设计已解决部分问题,但仍存在进一步优化的空间,将在后续章节继续探讨。
490 131
【📕分布式锁通关指南 02】基于Redis实现的分布式锁
|
2月前
|
Springboot使用Redis实现分布式锁
通过这些步骤和示例,您可以系统地了解如何在Spring Boot中使用Redis实现分布式锁,并在实际项目中应用。希望这些内容对您的学习和工作有所帮助。
194 83
Redis分布式锁如何实现 ?
Redis分布式锁主要依靠一个SETNX指令实现的 , 这条命令的含义就是“SET if Not Exists”,即不存在的时候才会设置值。 只有在key不存在的情况下,将键key的值设置为value。如果key已经存在,则SETNX命令不做任何操作。 这个命令的返回值如下。 ● 命令在设置成功时返回1。 ● 命令在设置失败时返回0。 假设此时有线程A和线程B同时访问临界区代码,假设线程A首先执行了SETNX命令,并返回结果1,继续向下执行。而此时线程B再次执行SETNX命令时,返回的结果为0,则线程B不能继续向下执行。只有当线程A执行DELETE命令将设置的锁状态删除时,线程B才会成功执行S
【📕分布式锁通关指南 03】通过Lua脚本保证redis操作的原子性
本文介绍了如何通过Lua脚本在Redis中实现分布式锁的原子性操作,避免并发问题。首先讲解了Lua脚本的基本概念及其在Redis中的使用方法,包括通过`eval`指令执行Lua脚本和通过`script load`指令缓存脚本。接着详细展示了如何用Lua脚本实现加锁、解锁及可重入锁的功能,确保同一线程可以多次获取锁而不发生死锁。最后,通过代码示例演示了如何在实际业务中调用这些Lua脚本,确保锁操作的原子性和安全性。
94 6
【📕分布式锁通关指南 03】通过Lua脚本保证redis操作的原子性
|
2月前
|
【📕分布式锁通关指南 04】redis分布式锁的细节问题以及RedLock算法原理
本文深入探讨了基于Redis实现分布式锁时遇到的细节问题及解决方案。首先,针对锁续期问题,提出了通过独立服务、获取锁进程自己续期和异步线程三种方式,并详细介绍了如何利用Lua脚本和守护线程实现自动续期。接着,解决了锁阻塞问题,引入了带超时时间的`tryLock`机制,确保在高并发场景下不会无限等待锁。最后,作为知识扩展,讲解了RedLock算法原理及其在实际业务中的局限性。文章强调,在并发量不高的场景中手写分布式锁可行,但推荐使用更成熟的Redisson框架来实现分布式锁,以保证系统的稳定性和可靠性。
52 0
【📕分布式锁通关指南 04】redis分布式锁的细节问题以及RedLock算法原理
Redis学习一(基础入门).
一、前言     Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、key-Value 的数据库、并提供多种语言的API。     通常,Redis 将数据存储于内存中,或被配置为使用虚拟内存。
2532 0
|
12天前
|
Redis--缓存击穿、缓存穿透、缓存雪崩
缓存击穿、缓存穿透和缓存雪崩是Redis使用过程中可能遇到的常见问题。理解这些问题的成因并采取相应的解决措施,可以有效提升系统的稳定性和性能。在实际应用中,应根据具体场景,选择合适的解决方案,并持续监控和优化缓存策略,以应对不断变化的业务需求。
65 29
Redis应用—8.相关的缓存框架
本文介绍了Ehcache和Guava Cache两个缓存框架及其使用方法,以及如何自定义缓存。主要内容包括:Ehcache缓存框架、Guava Cache缓存框架、自定义缓存。总结:Ehcache适合用作本地缓存或与Redis结合使用,Guava Cache则提供了更灵活的缓存管理和更高的并发性能。自定义缓存可以根据具体需求选择不同的数据结构和引用类型来实现特定的缓存策略。
Redis应用—8.相关的缓存框架
Redis缓存设计与性能优化
Redis缓存设计与性能优化涵盖缓存穿透、击穿、雪崩及热点key重建等问题。针对缓存穿透,可采用缓存空对象或布隆过滤器;缓存击穿通过随机设置过期时间避免集中失效;缓存雪崩需确保高可用性并使用限流熔断组件;热点key重建利用互斥锁防止大量线程同时操作。此外,开发规范强调键值设计、命令使用和客户端配置优化,如避免bigkey、合理使用批量操作和连接池管理。系统内核参数如vm.swappiness、vm.overcommit_memory及文件句柄数的优化也至关重要。慢查询日志帮助监控性能瓶颈。
51 9

热门文章

最新文章