笔者代表阿里云参加了RedisConf 2018的会议,在会议上对开源Redisson客户端的作者Rui Gu做了一个访谈,Rui Gu在Redis社区国际上的影响力还有在开源上的工作给笔者留下了深刻的印象,以下是访谈的具体内容。
以上照片为阿里云夏周、Rui Gu、阿里云白宸、阿里泽贤
- 当初为什么参与设计开发Redisson?
自04年从事工业自动化、工业IoT工作至今,涉及到很多场景需要对一系列设备进行监控和信号处理等工作。该类场景对实时处理能力,系统稳定性,高可用性,容灾能力等等要求非常高。从12年时决定采用Redis作为实时数据库时就产生了许多想法。Redis与Java这样的编程语言中的常用数据结构看似相像却又不同,一直希望能够用什么方法将两者联系起来。13年开始商用Redis以后这种想法越加强烈。于是在工作之余自行开始了一些相关的摸索与实践,最终决定采用动态类的形式让Redis的数据结构操作起来更像Java对应的结构。谁知远在莫斯科的Nikita似乎也有类似的想法,他从14年元旦便开始了实际应用的开发,并很快的开源了Redisson。于此同时我的实践也有了许些进展,并初步的实现了一些基本功能。不过由于工作上的种种原因,再加上当时自己也缺乏足够的信心,毕竟这是条没人走过的路,大半年过去了进展比较缓慢。殊不知Nikita面对这同样的问题,但是他不仅艰难地坚持了下来,而且丝毫没有放弃的意思。14年下半年时我开始注意到了Redisson项目,仔细了解了以后顿时产生了很强的共鸣,虽然和我的实践有着同样的理念却又是不同的出发点。于是乎,在有了这样的火花以后,我们开始了相互之间的沟通和交流,最后在15年初时决定,放弃自己的实践项目,加入Redisson。至此,在这条没人走过的路上我们不再独行。 - Redisson解决了什么问题?相比其他Redis客户端它有什么优势?
2.1)IoT行业里,一组设备的各种实时状态值往往是作为一个具有业务意义的对象,由JVM管理在内存里,如果将这个对象存储到Redis数据库的String结构里,每次更新一个状态值,就需要做一次序列化和反序列化。同时还有可能面临着同一时刻操作同一个对象的不同状态值带来的并发难题。实际应用时采用了Redis提供的Hash数据结构来储存这个对象,只有这样才能有效地避免这类问题的发生。尽管Redis的Hash结构和Java里的HashMap极为相似,但是在程序操作Redis的时候不能像操作HashMap一样便捷。而且如果对Redis相关命令的用法不能稔熟于心,或在细节之处处理不当,便会最终造成业务上的各种问题。Redisson的Map就是为了填补Redis的Hash和Java的HashMap两者之间的空缺而产生。
图1 - Jedis的Hash操作
图2 - Java的ConcurrentHashMap
图3 - Redisson的ConcurrentHashMap
2.2)工控和某些IoT场景对实时处理能力要求很高,所有的信号都必须实现毫秒级响应。这类场景还具有并发量巨大的特点。与社交电商等场景不同的是这类应用场景基本没有峰谷流量,时时刻刻都是峰值。因此其它场景里常见的削峰填谷措施在这里只能加重负担。在这样的场景下如果使用像Jedis这样采用同步编程模型的客户端时,就需要随时确保并发线程数与连接数一对一,否则获取不到可用连接会直接报错。相比之下Redisson利用了Netty异步编程框架,使用了与Redis服务端结构类似的事件循环(EventLoop)式的线程池,并结合连接池的方式弹性管理连接。最终做到了使用少量的连接既可以满足对大量线程的要求,从根本上缓解线程之间的竞争关系。同时异步操作的模式还能够避免数据请求造成业务线程的阻塞。
2.3)Redis 发展至今经历了多次技术变迁。官方版在迭代的过程中不但增加了许多有用的功能,同时也发展了几种高可用性方案。于此同时,社区和云计算商在官方版上进而开发出了多种基于代理(Proxy)的高可用方案。相比之下,这些方案各有优劣,适用场景也各自不一。多样化的方案在带来便利的同时也带来了麻烦。比如在业务扩容,从简单的单机或主从模式迁移到哨兵或集群模式;或是业务迁移,从自建的Redis环境迁移到云上;亦或是项目的持续性交付CD/CI过程中,不同的阶段使用不同Redis运行模式等等情况。往往需要开发人员针对不同的高可用方案开发出一套与之匹配的使用方法。使得一个项目对Redis运行模式的耦合度高,在Redis运行模式变化时就必须更改业务代码。Redisson针对这种情况提供了一套便捷的文件化配置方法,在无需修改程序代码的情况下,通过不同的JSON,YAML或SpringXML文件实现对不同Redis运行模式和环境的支持。这既降低了开发难度,也降低了运维难度。
- Redisson在分布式锁方面的工作非常多,能否介绍下这方面的实践?
对于Redis分布式锁的实现方式,网上讨论相关文章都基本都“烂大街”了。然而几乎所有相关介绍都是在单纯使用setnx命令的基础上进行一个简单封装,且少有文章分析这样设计的缺陷。在这个博客满天飞,代码随便贴的时代,这样的局面无形之中给了大家一个假象,就是Redis分布式锁只能是以这样简单的形式存在,即便有缺陷也只能在业务代码里规避。那么为什么不换位思考一下,即用稍微复杂点的设计来弥补它的不足,从而换取业务上的灵活性呢?再重新设计Redis分布式锁之前,我们先了解一下单纯使用setnx命令封装的分布式锁有哪些不足。
1). 不具备可重入性
在执行setnx命令时,通常采用业务上指定的名称作为key名,用时间或随机值作为value来实现。这样的实现方式不具备追踪请求线程的能力,同时也不具备统计重入次数的能力,甚至有些实现方式都不具备操作的原子性。当遇到业务上需要在多个地方用到同样一个锁的时候,很显然使用不具有可重入的锁会很容易发生死锁的现象。特别是在有递归逻辑的场景里,发生死锁的几率会更高。Java并发工具包里的Lock对象和sychronized语块都具有可重入性,对于经常使用这些工具的人来说,往往会很容易忽略setnx的这个缺陷。
2). 不支持续约
在分布式环境中,为了保证锁的活性和避免程序宕机造成的死锁现象,分布式锁往往会引入一个失效时间,超过这个时间则认为自动解锁。这样的设计前提是开发人员对这个自动解锁时间的粒度有一个很好的把握,太短了可能会出现任务没做完锁就失效了,而太长了在出现程序宕机或业务节点挂掉时,其它节点需要等很长时间才能恢复,而难以保证业务的SLA。setnx的设计缺乏一个延续有效期的续约机制,无法保证业务能够先工作做完再解锁,也不能确保在某个程序宕机或业务节点挂掉的时候,其它节点能够很快的恢复业务处理能力。
3). 不具备阻塞的能力
平常大家多少都接触过的锁,由于加锁策略(Locking Strategy)的差别,使得每种锁都有各自不同的特性。但是在通常情况下这些锁都具备两个共性:一是互斥性,二是阻塞性。互斥性是指在任何时刻最多只能有一个线程获得通行的资格。阻塞性是指的在有竞争的情况下,未获取到资源的线程会停止继续操作,直到成功获取到资源或取消操作。很显然setnx命令只提供了互斥的特性,却没有提供阻塞的能力。虽然在业务代码里可以引入自旋机制来进行再次获取,但这仅仅是把原本应该在锁里实现的功能搬到了业务代码里,通过增加业务代码的复杂程度来简化锁的实现似乎显得有点南辕北辙。
Redisson的分布式锁在满足以上三个基本要求的同时还增加了线程安全的特点。利用Redis的Hash结构作为储存单元,将业务指定的名称作为key,将随机UUID和线程ID作为field,最后将加锁的次数作为value来储存。同时UUID作为锁的实例变量保存在客户端。将UUID和线程ID作为标签在运行多个线程同时使用同一个锁的实例时,仍然保证了操作的独立性,满足了线程安全的要求。
加锁时通过Lua脚本先检查锁是否存在,如不存在则创建hash相关字段并设定过期时间后返回,这表示加锁成功。如果该hash字段已经存在,再检查随机字段和线程id是否一致。如果一致则递增value的值并重新更新过期时间后返回,此时表示同一节点同一线程再次成功加锁,从而保证了可重入性。如果hash存在且字段不一致,说明其他节点或线程已经拥有了这个锁。因此Lua脚本返回这个hash的当前有效期。当结果返回到在客户端后,如果加锁成功,则通过线程池依照设定好的参数定时执行续约,最后通知请求线程继续后续操作。如果加锁没有成功,则监听一个以这个key为后缀的pubsub频道,直到收到解锁消息后再次重试。
解锁时通过Lua脚本先检查锁是否存在,如果已经不存在则直接发布解锁消息并返回。如果任然存在则检查标签是否存在,如果不存在则表示这个锁并不为本线程所拥有,这种情况请求线程将收到报错。如果存在则表示该锁正是被该线程所拥有。在这种情况下,递减标签字段后判断,如果返回的加锁数量仍然大于0,说明当前的锁仍然有效,仅仅只是重入次数减少了。相反这表示锁已经完全解开,则立即删除该锁并发布解锁信息。
Redisson的可重入锁解决了setnx锁的许多先天性不足,但是由于它仍然是以单一一个key的方式储存在固定的一个Redis节点里,并且有自动失效期。这样的设计虽然可以很大程度上避免客户端程序宕机或业务节点挂掉造成的影响,但是随之带来的弊端是遇到服务端Redis进程宕机或节点挂掉的情况,还是有可能会造成锁的信息丢失,这样的缺陷显然无法满足某些特定场景提出的高可用性要求。
介于这种情况,Redis作者Salvatore提出了一个基于多个节点的高可用分布式锁的算法,起名叫红锁(RedLock: https://redis.io/topics/distlock)。在这种算法下,客户端需要同时在多个节点里同时尝试获取一个独立的锁,只有当一次性成功获取了大多数锁的情况下才能被视为赢得了高可用分布式锁,否则需要解除已经部分获取到的锁,等待一个随机时间后再次重试。
在算法设计上,Salvatore依然采用的是setnx作为举例讲解分布式锁的互斥特性。在算法实现上,Redisson的RedissonRedLock采用的是前面提到的更加灵活方便的可重入锁。Redisson的扩展算法是Redis官网唯一认可的Java实现。
虽然Redlock的算法提供了高可用的特性,但建立在大多数可见原则的前提下,这样的算法适用性仍然有一定局限。Redisson为此提供了基于增强型的算法的高可用分布式联锁RedissonMultiLock。这种算法要求客户端必须成功获取全部节点的锁才被视为加锁成功,从而更进一步提高了算法的可靠性。
4.能否介绍下Redisson最前沿的发展方向?
Redisson的发展路线决定了它在Redis的功能扩展及应用方式上始终走在业界的前列,其中最具有代表性的便是本地缓存功能了。2016年为了解决一企业版用户的切实需求开发了这一功能。其原理是采用牺牲客户端自身内存的空间的方式,换取在频繁获取某些常用数据时消耗在网络上的时间。该功能在同年9月开源后便立即受到了广大用户的关注。这一功能的出现加速了传统IT用户从其他类似平台迁移到Redis的速度。其受欢迎程度大大超乎了Nikita和我的想象。以至于每年都有企业用户不远万里去Redis大会等类似国际交流大会,并分享它们使用Redisson从其他平台向Redis迁移过程和经验。也正是因为这种趋势而引起了Redis作者Salvatore的注意,在同一些用户面对面沟通交流之后,Salvatore决定将客户端缓存功能作为Redis今后发展的重要方向,并为此提出了RESP3协议。RESP3的出现将为客户端缓存功能提供服务端协调的能力。同时Salvatore还邀请Redisson团队作为专家组成员参与Redis客户端缓存标准的指定。
5.Redisson做为开源项目如何保证持续的发展?
为了保证Redisson项目的可持续性的健康发展,为了避免像其他开源项目面临的一段时间以后就无人维护的尴尬局面,17年初Nikita和我商量后决定在开源项目基础上提供收费咨询服务,为项目的正常运作提供必要的资金。同时还针对大型企业用户遇到的特殊场景提供了企业级的综合性解决方案,最后将这些所有的方案与企业级SLA支持服务打包作为Redisson PRO正式面向企业用户。
相对于其他客户端而言,虽然Redisson项目创立的时间较短,但已经受到了来自不同行业企业的信任,其中不乏许多行业领头羊企业,其中最值得介绍的是这几个世界级的企业用户:
• 计算机行业的IBM。想必大家都熟悉IBM,PC机的鼻祖。,业界少有同时具有超强硬件软件研发能力的企业,即便如此,IBM也心甘情愿的使用Redisson,这种信任是对我们最大的支持。
• 航空国防制造业的波音。在它们主动联系我们以前,我很难想象波音也会对Redisson感兴趣。事实上波音除了造飞机以外,它也是全球最大的飞行航图提供商和移动电子飞行包的方案提供商,几乎每个航空公司都是他们的用户。Redisson为他们的在线飞行导航业务提供了扎实的基础。
• 保险业的美国国际集团(AIG)。美国国际集团成立于1919年的中国上海,它是首个将保险概念带给中国人的西方企业,其业务遍布全球130多个国家和地区。虽然08年经济危机中,遭遇股价瞬间暴跌的惨剧将AIG推入了吃瓜群主的视线,但它今天仍是一个拥有99年历史,总资产为6千多亿美元的国际性大型企业。在经过AIG团队长时间的调研后,Redisson被用于支持其名目众多的金融保险业务。
• 金融机构标准普尔(S&P Global)。提到经济危机就不得不提一下世界权威金融分析机构标准普尔。它是美国证券交易委员会(SEC)认可的三大评级组织之一,专门为投资者提供信用评级,投资研究和咨询等服务。在业内外的知名度很高,享有盛名的S&P 500美国股指便是由它创建并维护着。标准普尔不仅对外提供针对上市企业的评级,还提供针对国家*的评级。它在2011年时断然降低美国*的评级,并把其前景调整为负面以后,立马引发了金融业的剧烈波动。但正是这个呼风唤雨无所不能,连美国*都不放眼里的机构也成为了Redisson的忠实用户,并将其用于提供复杂的金融数据的分析和处理。由此可见Redisson的信任评级是非常的高[奸笑]。