搜索
首页数据库Redis使用Redis实现一个安全可靠的分布式锁

本篇文章给大家介绍一下如何使用Redis实现一个安全可靠的分布式锁,说明分布式锁实现的主要要素,常见误区。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。

使用Redis实现一个安全可靠的分布式锁

并发场景下多个进程或线程共享资源的读写,需要保证对资源的访问互斥。在单机系统中,我们可以使用Java并发包中的API、synchronized关键字等方式来解决;但是在分布式系统下,这些方式不再适用,我们需要自己实现分布式锁。

常见的分布式锁的实现方案有:基于数据库、基于Redis、基于Zookeeper等。作为Redis专题的一部分,本文将基于Redis聊一聊分布式锁的实现方案。【相关推荐:Redis视频教程

分析与实现


问题分析

分布式锁与JVM内置的锁有着共同的目的:让应用程序以预期的顺序访问或操作共享的资源,防止多个线程同时对同一资源操作,导致系统运行紊乱、不可控。常常用于商品库存扣减、优惠券扣减等场景。

理论上来讲,为了保证锁的安全性和有效性,分布式锁至少需要满足以下条件:

  • 互斥性:在同一时间内,仅有一个线程能够获得锁;
  • 无死锁:线程获取锁后,必须保证能够释放,即使线程获取锁后应用程序宕机,也能在限定时间内释放;
  • 加锁和解锁必须是同一个线程;

在实现方式上,分布式锁大体分为三个步骤:

  • a-获取资源的操作权;
  • b-对资源执行操作;
  • c-释放资源的操作权;

无论是Java内置的锁,还是分布式锁,也无论使用哪种分布式实现方案,都是围绕a、c两个步骤展开。Redis对于实现分布式锁天然友好,原因如下:

  • 命令处理阶段Redis使用单线程处理,同一个key同时只有一个线程能够处理,没有多线程竞态问题。
  • SET key value NX PX milliseconds命令在不存在key的情况下添加具有过期时间的key,为安全加锁提供支持。
  • Lua脚本和DEL命令为安全解锁提供可靠支撑。

代码实现

  • Maven依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
  	<version>${your-spring-boot-version}</version>
</dependency>
  • 配置文件

在application.properties增加以下内容,单机版Redis实例。

spring.redis.database=0
spring.redis.host=localhost
spring.redis.port=6379
  • RedisConfig
@Configuration
public class RedisConfig {

    // 自己定义了一个 RedisTemplate
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory)
        throws UnknownHostException {
        // 我们为了自己开发方便,一般直接使用 <String, Object>
        RedisTemplate<String, Object> template = new RedisTemplate<String,
            Object>();
        template.setConnectionFactory(factory);
        // Json序列化配置
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // String 的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}
  • RedisLock
@Service
public class RedisLock {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 加锁,最多等待maxWait毫秒
     *
     * @param lockKey   锁定key
     * @param lockValue 锁定value
     * @param timeout   锁定时长(毫秒)
     * @param maxWait   加锁等待时间(毫秒)
     * @return true-成功,false-失败
     */
    public boolean tryAcquire(String lockKey, String lockValue, int timeout, long maxWait) {
        long start = System.currentTimeMillis();

        while (true) {
            // 尝试加锁
            Boolean ret = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, timeout, TimeUnit.MILLISECONDS);
            if (!ObjectUtils.isEmpty(ret) && ret) {
                return true;
            }

            // 计算已经等待的时间
            long now = System.currentTimeMillis();
            if (now - start > maxWait) {
                return false;
            }

            try {
                Thread.sleep(200);
            } catch (Exception ex) {
                return false;
            }
        }
    }

    /**
     * 释放锁
     *
     * @param lockKey   锁定key
     * @param lockValue 锁定value
     * @return true-成功,false-失败
     */
    public boolean releaseLock(String lockKey, String lockValue) {
        // lua脚本
        String script = "if redis.call(&#39;get&#39;,KEYS[1]) == ARGV[1] then return redis.call(&#39;del&#39;,KEYS[1]) else return 0 end";

        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
        Long result = redisTemplate.opsForValue().getOperations().execute(redisScript, Collections.singletonList(lockKey), lockValue);
        return result != null && result > 0L;
    }
}
  • 测试用例
@SpringBootTest
class RedisDistLockDemoApplicationTests {

    @Resource
    private RedisLock redisLock;

    @Test
    public void testLock() {
        redisLock.tryAcquire("abcd", "abcd", 5 * 60 * 1000, 5 * 1000);
        redisLock.releaseLock("abcd", "abcd");
    }
}

安全隐患

可能很多同学(也包括我)在日常工作中都是使用上面的实现方式,看似是稳妥的:

  • 使用set命令NXPX选项进行加锁,保证了加锁互斥,避免了死锁;
  • 使用lua脚本解锁,防止解除其他线程的锁;
  • 加锁、解锁命令都是原子操作;

其实以上实现的稳妥有个前提条件:单机版Redis、开启AOF持久化方式并设置appendfsync=always

但是在哨兵模式和集群模式下可能存在问题,为什么呢?

哨兵模式和集群模式基于主从架构,主从之间通过命令传播实现数据同步,而命令传播是异步的。

所以就存在主节点数据写入成功,在还未通知从节点情况下,主节点就宕机的可能。

当从节点通过故障转移提升为新的主节点后,其他线程就有机会重新加锁成功,导致不满足分布式锁的互斥条件。

官方RedLock


集群模式下,若集群所有节点稳定运行,不出现故障转移的情况下,安全性是有保障的。但是,没有什么系统能够保证100%稳定,基于Redis的分布式锁必须考虑容错。

由于主从同步基于异步复制原理,所以哨兵模式和集群模式天生无法满足此条件。为此,Redis作者专门提出了一种解决方案——RedLock(Redis Distribute Lock)。

设计思路

根据官方文档的说明,把RedLock的设计思路进行介绍。

先说环境要求,需要N(N>=3)个独立部署的Redis实例,相互之间不需要主从复制、故障转移等技术。

为了获取锁,客户端将按照以下流程进行操作:

  • 获取当前时间(毫秒)作为开始时间start;
  • 使用相同的key和随机value,按顺序向所有N个节点发起获取锁的请求。当向每个实例设置锁时,客户端会使用一个过期时间(小于锁的自动释放时间)。比如锁的自动释放时间是10秒,这个超时时间应该是5-50毫秒。这是为了防止客户端在一个已经宕机的实例浪费太多时间:如果Redis实例宕机,客户端尽快处理下一个实例。
  • 客户端计算加锁消耗的时间cost(cost=start-now)。只有客户端在半数以上实例加锁成功,并且整个耗时小于整个有效时间(ttl),才能认为当前客户端加锁成功。
  • 如果客户端加锁成功,那么整个锁的真正有效时间应该是:validTime=ttl-cost。
  • 如果客户端加锁失败(可能是获取锁成功实例数未过半,也可能是耗时超过ttl),那么客户端应该向所有实例尝试解锁(即使刚刚客户端认为加锁失败)。

RedLock的设计思路延续了Redis内部多种场景的投票方案,通过多个实例分别加锁解决竞态问题,虽然加锁消耗了时间,但是消除了主从机制下的安全问题。

代码实现

官方推荐Java实现为Redisson,它具备可重入特性,按照RedLock进行实现,支持独立实例模式、集群模式、主从模式、哨兵模式等;API比较简单,上手容易。示例如下(直接通过测试用例):

    @Test
    public void testRedLock() throws InterruptedException {

        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        final RedissonClient client = Redisson.create(config);

        // 获取锁实例
        final RLock lock = client.getLock("test-lock");

        // 加锁
        lock.lock(60 * 1000, TimeUnit.MILLISECONDS);
        try {
            // 假装做些什么事情
            Thread.sleep(50 * 1000);
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            //解锁
            lock.unlock();
        }
    }

Redisson封装的非常好,我们可以像使用Java内置的锁一样去使用,代码简洁的不能再少了。关于Redisson源码的分析,网上有很多文章大家可以找找看。

全文总结


分布式锁是我们研发过程中常用的的一种解决并发问题的方式,Redis是只是一种实现方式。

关键的是要弄清楚加锁、解锁背后的原理,以及实现分布式锁需要解决的核心问题,同时考虑我们所采用的中间件有什么特性可以支撑。了解这些后,实现起来就不是什么问题了。

学习了RedLock的思想,我们是不是也可以在自己的应用程序内实现了分布式锁呢?欢迎沟通!

更多编程相关知识,请访问:编程入门!!

以上是使用Redis实现一个安全可靠的分布式锁的详细内容。更多信息请关注PHP中文网其他相关文章!

声明
本文转载于:掘金社区。如有侵权,请联系admin@php.cn删除
REDIS:它如何充当数据存储和服务REDIS:它如何充当数据存储和服务Apr 24, 2025 am 12:08 AM

REDISACTSASBOTHADATASTOREANDASERVICE.1)ASADATASTORE,ITUSESIN-MEMORYSTOOGATOFORFOFFASTESITION,支持VariousDatharptructuresLikeKey-valuepairsandsortedsetsetsetsetsetsetsets.2)asaservice,ItprovidespunctionslikeItionitionslikepunikeLikePublikePublikePlikePlikePlikeAndluikeAndluAascriptingiationsmpleplepleclexplectiations

REDIS与其他数据库:比较分析REDIS与其他数据库:比较分析Apr 23, 2025 am 12:16 AM

Redis与其他数据库相比,具有以下独特优势:1)速度极快,读写操作通常在微秒级别;2)支持丰富的数据结构和操作;3)灵活的使用场景,如缓存、计数器和发布订阅。选择Redis还是其他数据库需根据具体需求和场景,Redis在高性能、低延迟应用中表现出色。

REDIS的角色:探索数据存储和管理功能REDIS的角色:探索数据存储和管理功能Apr 22, 2025 am 12:10 AM

Redis在数据存储和管理中扮演着关键角色,通过其多种数据结构和持久化机制成为现代应用的核心。1)Redis支持字符串、列表、集合、有序集合和哈希表等数据结构,适用于缓存和复杂业务逻辑。2)通过RDB和AOF两种持久化方式,Redis确保数据的可靠存储和快速恢复。

REDIS:了解NOSQL概念REDIS:了解NOSQL概念Apr 21, 2025 am 12:04 AM

Redis是一种NoSQL数据库,适用于大规模数据的高效存储和访问。1.Redis是开源的内存数据结构存储系统,支持多种数据结构。2.它提供极快的读写速度,适合缓存、会话管理等。3.Redis支持持久化,通过RDB和AOF方式确保数据安全。4.使用示例包括基本的键值对操作和高级的集合去重功能。5.常见错误包括连接问题、数据类型不匹配和内存溢出,需注意调试。6.性能优化建议包括选择合适的数据结构和设置内存淘汰策略。

REDIS:现实世界的用例和示例REDIS:现实世界的用例和示例Apr 20, 2025 am 12:06 AM

Redis在现实世界中的应用包括:1.作为缓存系统加速数据库查询,2.存储Web应用的会话数据,3.实现实时排行榜,4.作为消息队列简化消息传递。Redis的多功能性和高性能使其在这些场景中大放异彩。

REDIS:探索其功能和功能REDIS:探索其功能和功能Apr 19, 2025 am 12:04 AM

Redis脱颖而出是因为其高速、多功能性和丰富的数据结构。1)Redis支持字符串、列表、集合、散列和有序集合等数据结构。2)它通过内存存储数据,支持RDB和AOF持久化。3)从Redis6.0开始引入多线程处理I/O操作,提升了高并发场景下的性能。

Redis是SQL还是NOSQL数据库?答案解释了Redis是SQL还是NOSQL数据库?答案解释了Apr 18, 2025 am 12:11 AM

RedisisclassifiedasaNoSQLdatabasebecauseitusesakey-valuedatamodelinsteadofthetraditionalrelationaldatabasemodel.Itoffersspeedandflexibility,makingitidealforreal-timeapplicationsandcaching,butitmaynotbesuitableforscenariosrequiringstrictdataintegrityo

REDIS:提高应用程序性能和可扩展性REDIS:提高应用程序性能和可扩展性Apr 17, 2025 am 12:16 AM

Redis通过缓存数据、实现分布式锁和数据持久化来提升应用性能和可扩展性。1)缓存数据:使用Redis缓存频繁访问的数据,提高数据访问速度。2)分布式锁:利用Redis实现分布式锁,确保在分布式环境中操作的安全性。3)数据持久化:通过RDB和AOF机制保证数据安全性,防止数据丢失。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

螳螂BT

螳螂BT

Mantis是一个易于部署的基于Web的缺陷跟踪工具,用于帮助产品缺陷跟踪。它需要PHP、MySQL和一个Web服务器。请查看我们的演示和托管服务。

EditPlus 中文破解版

EditPlus 中文破解版

体积小,语法高亮,不支持代码提示功能

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

功能强大的PHP集成开发环境

安全考试浏览器

安全考试浏览器

Safe Exam Browser是一个安全的浏览器环境,用于安全地进行在线考试。该软件将任何计算机变成一个安全的工作站。它控制对任何实用工具的访问,并防止学生使用未经授权的资源。

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)