热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发心得:利用Redis构建分布式系统的轻量级协调机制

篇首语:本文由编程笔记#小编为大家整理,主要介绍了使用 Redis 实现分布式系统轻量级协调技术相关的知识,希望对你有一定的参考价值。         在分布式

篇首语:本文由编程笔记#小编为大家整理,主要介绍了使用 Redis 实现分布式系统轻量级协调技术相关的知识,希望对你有一定的参考价值。


        在分布式系统中,各个进程(本文使用进程来描述分布式系统中的运行主体,它们可以在同一个物理节点上也可以在不同的物理节点上)相互之间通常是需要协调进行运作的,有时是不同进程所处理的数据有依赖关系,必须按照一定的次序进行处理,有时是在一些特定的时间需要某个进程处理某些事务等等,人们通常会使用分布式锁、选举算法等技术来协调各个进程之间的行为。因为分布式系统本身的复杂特性,以及对于容错性的要求,这些技术通常是重量级的,比如 Paxos 算法,欺负选举算法,ZooKeeper 等,侧重于消息的通信而不是共享内存,通常也是出了名的复杂和难以理解,当在具体的实现和实施中遇到问题时都是一个挑战。

        Redis 经常被人们认为是一种 NoSQL 软件,但其本质上是一种分布式的数据结构服务器软件,提供了一个分布式的基于内存的数据结构存储服务。在实现上,仅使用一个线程来处理具体的内存数据结构,保证它的数据操作命令的原子特性;它同时还支持基于 Lua 的脚本,每个 Redis 实例使用同一个 Lua 解释器来解释运行 Lua 脚本,从而 Lua 脚本也具备了原子特性,这种原子操作的特性使得基于共享内存模式的分布式系统的协调方式成了可能,而且具备了很大的吸引力,和复杂的基于消息的机制不同,基于共享内存的模式对于很多技术人员来说明显容易理解的多,特别是那些已经了解多线程或多进程技术的人。在具体实践中,也并不是所有的分布式系统都像分布式数据库系统那样需要严格的模型的,而所使用的技术也不一定全部需要有坚实的理论基础和数学证明,这就使得基于 Redis 来实现分布式系统的协调技术具备了一定的实用价值,实际上,人们也已经进行了不少尝试。本文就其中的一些协调技术进行介绍。


signal/wait 操作

        在分布式系统中,有些进程需要等待其它进程的状态的改变,或者通知其它进程自己的状态的改变,比如,进程之间有操作上的依赖次序时,就有进程需要等待,有进程需要发射信号通知等待的进程进行后续的操作,这些工作可以通过 Redis 的 Pub/Sub 系列命令来完成,比如:

import redis, time
rc = redis.Redis()
def wait( wait_for ):
ps = rc.pubsub()
ps.subscribe( wait_for )
ps.get_message()
wait_msg = None
while True:
msg = ps.get_message()
if msg and msg[‘type‘] == ‘message‘:
wait_msg = msg
break
time.sleep(0.001)
ps.close()
return wait_msg
def signal_broadcast( wait_in, data ):
wait_count = rc.publish(wait_in, data)
return wait_count

用这个方法很容易进行扩展实现其它的等待策略,比如 try wait,wait 超时,wait 多个信号时是要等待全部信号还是任意一个信号到达即可返回等等。因为 Redis 本身支持基于模式匹配的消息订阅(使用 psubscribe 命令),设置 wait 信号时也可以通过模式匹配的方式进行。

        和其它的数据操作不同,订阅消息是即时易逝的,不在内存中保存,不进行持久化保存,如果客户端到服务端的连接断开的话也是不会重发的,但是在配置了 master/slave 节点的情况下,会把 publish 命令同步到 slave 节点上,这样我们就可以同时在 master 以及 slave 节点的连接上订阅某个频道,从而可以同时接收到发布者发布的消息,即使 master 在使用过程中出故障,或者到 master 的连接出了故障,我们仍然能够从 slave 节点获得订阅的消息,从而获得更好的鲁棒性。另外,因为数据不用写入磁盘,这种方法在性能上也是有优势的。

上面的方法中信号是广播的,所有在 wait 的进程都会收到信号,如果要将信号设置成单播,只允许其中一个收到信号,则可以通过约定频道名称模式的方式来实现,比如:

频道名称 = 频道名前缀 (channel) + 订阅者全局唯一 ID(myid)

其中唯一 ID 可以是 UUID,也可以是一个随机数字符串,确保全局唯一即可。在发送 signal 之前先使用“pubsub channels channel*”命令获得所有的订阅者订阅的频道,然后发送信号给其中一个随机指定的频道;等待的时候需要传递自己的唯一 ID,将频道名前缀和唯一 ID 合并为一个频道名称,然后同前面例子一样进行 wait。示例如下:

import random
single_cast_script="""
local channels = redis.call(‘pubsub‘, ‘channels‘, ARGV[1]..‘*‘);
if #channels == 0 then
return 0;
end;
local index= math.mod(math.floor(tonumber(ARGV[2])), #channels) + 1;
return redis.call( ‘publish‘, channels[index], ARGV[3]);
"""
def wait_single( channel, myid):
return wait( channel + myid )
def signal_single( channel, data):
rand_num = int(random.random() * 65535)
return rc.eval( single_cast_script, 0, channel, str(rand_num), str(data) )

分布式锁 Distributed Locks

        分布式锁的实现是人们探索的比较多的一个方向,在 Redis 的官方网站上专门有一篇文档介绍基于 Redis 的分布式锁,其中提出了 Redlock 算法,并列出了多种语言的实现案例,这里作一简要介绍。

Redlock 算法着眼于满足分布式锁的三个要素:



  • 安全性:保证互斥,任何时间至多只有一个客户端可以持有锁



  • 免死锁:即使当前持有锁的客户端崩溃或者从集群中被分开了,其它客户端最终总是能够获得锁。



  • 容错性:只要大部分的 Redis 节点在线,那么客户端就能够获取和释放锁。



锁的一个简单直接的实现方法就是用 SET NX 命令设置一个设定了存活周期 TTL 的 Key 来获取锁,通过删除 Key 来释放锁,通过存活周期来保证避免死锁。不过这个方法存在单点故障风险,如果部署了 master/slave 节点,则在特定条件下可能会导致安全性方面的冲突,比如:



  1. 客户端 A 从 master 节点获得锁



  2. master 节点在将 key 复制到 slave 节点之前崩溃了



  3. slave 节点提升为新的 master 节点



  4. 客户端 B 从新的 master 节点获得了锁,而这个锁实际上已经由客户端 A 所持有,导致了系统中有两个客户端在同一时间段内持有同一个互斥锁,破坏了互斥锁的安全性。



在 Redlock 算法中,通过类似于下面这样的命令进行加锁:

SET resource_name my_random_value NX PX 30000

这里的 my_random_value 为全局不同的随机数,每个客户端需要自己产生这个随机数并且记住它,后面解锁的时候需要用到它。

解锁则需要通过一个 Lua 脚本来执行,不能简单地直接删除 Key,否则可能会把别人持有的锁给释放了:

if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end

这个 ARGV[1] 的值就是前面加锁的时候的 my_random_value 的值。

如果需要更好的容错性,可以建立一个有 N(N 为奇数)个相互独立完备的 Redis 冗余节点的集群,这种情况下,一个客户端获得锁和释放锁的算法如下:



  1. 先获取当前时间戳 timestamp_1,以毫秒为单位。



  2. 以相同的 Key 和随机数值,依次从 N 个节点获取锁,每次获取锁都设置一个超时,超时时限要保证小于所有节点上该锁的自动释放时间,以免在某个节点上耗时过长,通常都设的比较短。



  3. 客户端将当前时间戳减去第一步中的时间戳 timestamp_1,计算获取锁总消耗时间。只有当客户端获得了半数以上节点的锁,而且总耗时少于锁存活时间,该客户端才被认为已经成功获得了锁。



  4. 如果获得了锁,则其存活时间为开始预设锁存活时间减去获取锁总耗时间。



  5. 如果客户端不能获得锁,则应该马上在所有节点上解锁。



  6. 如果要重试,则在随机延时之后重新去获取锁。



  7. 获得了锁的客户端要释放锁,简单地在所有节点上解锁即可。



Redlock 算法不需要保证 Redis 节点之间的时钟是同步的(不论是物理时钟还是逻辑时钟),这点和传统的一些基于同步时钟的分布式锁算法有所不同。Redlock 算法的具体的细节可以参阅 Redis 的官方文档,以及文档中列出的多种语言版本的实现。


选举算法

        在分布式系统中,经常会有些事务是需要在某个时间段内由一个进程来完成,或者由一个进程作为 leader 来协调其它的进程,这个时候就需要用到选举算法,传统的选举算法有欺负选举算法(霸道选举算法)、环选举算法、Paxos 算法、Zab 算法 (ZooKeeper) 等,这些算法有些依赖于消息的可靠传递以及时钟同步,有些过于复杂,难以实现和验证。新的 Raft 算法相比较其它算法来说已经容易了很多,不过它仍然需要依赖心跳广播和逻辑时钟,leader 需要不断地向 follower 广播消息来维持从属关系,节点扩展时也需要其它算法配合。

选举算法和分布式锁有点类似,任意时刻最多只能有一个 leader 资源。当然,我们也可以用前面描述的分布式锁来实现,设置一个 leader 资源,获得这个资源锁的为 leader,锁的生命周期过了之后,再重新竞争这个资源锁。这是一种竞争性的算法,这个方法会导致有比较多的空档期内没有 leader 的情况,也不好实现 leader 的连任,而 leader 的连任是有比较大的好处的,比如 leader 执行任务可以比较准时一些,查看日志以及排查问题的时候也方便很多,如果我们需要一个算法实现 leader 可以连任,那么可以采用这样的方法:

import redis
rc = redis.Redis()
local_selector = 0
def master():
global local_selector
master_selector = rc.incr(‘master_selector‘)
if master_selector == 1:
# initial / restarted
local_selector = master_selector
else:
if local_selector > 0:
# I‘m the master before
if local_selector > master_selector:
# lost, maybe the db is fail-overed.
local_selector = 0
else:
# continue to be the master
local_selector = master_selector
if local_selector > 0:
# I‘m the current master
rc.expire(‘master_selector‘, 20)
return local_selector > 0

这个算法鼓励连任,只有当前的 leader 发生故障或者执行某个任务所耗时间超过了任期、或者 Redis 节点发生故障恢复之后才需要重新选举出新的 leader。在 master/slave 模式下,如果 master 节点发生故障,某个 slave 节点提升为新的 master 节点,即使当时 master_selector 值尚未能同步成功,也不会导致出现两个 leader 的情况。如果某个 leader 一直连任,则 master_selector 的值会一直递增下去,考虑到 master_selector 是一个 64 位的整型类型,在可预见的时间内是不可能溢出的,加上每次进行 leader 更换的时候 master_selector 会重置为从 1 开始,这种递增的方式是可以接受的,但是碰到 Redis 客户端(比如 Node.js)不支持 64 位整型类型的时候就需要针对这种情况作处理。如果当前 leader 进程处理时间超过了任期,则其它进程可以重新生成新的 leader 进程,老的 leader 进程处理完毕事务后,如果新的 leader 的进程经历的任期次数超过或等于老的 leader 进程的任期次数,则可能会出现两个 leader 进程,为了避免这种情况,每个 leader 进程在处理完任期事务之后都应该检查一下自己的处理时间是否超过了任期,如果超过了任期,则应当先设置 local_selector 为 0 之后再调用 master 检查自己是否是 leader 进程。


消息队列

消息队列是分布式系统之间的通信基本设施,通过消息可以构造复杂的进程间的协调操作和互操作。Redis 也提供了构造消息队列的原语,比如 Pub/Sub 系列命令,就提供了基于订阅/发布模式的消息收发方法,但是 Pub/Sub 消息并不在 Redis 内保持,从而也就没有进行持久化,适用于所传输的消息即使丢失了也没有关系的场景。

            如果要考虑到持久化,则可以考虑 list 系列操作命令,用 PUSH 系列命令(LPUSH, RPUSH 等)推送消息到某个 list,用 POP 系列命令(LPOP, RPOP,BLPOP,BRPOP 等)获取某个 list 上的消息,通过不同的组合方式可以得到 FIFO,FILO,比如:

import redis
rc = redis.Redis()
def fifo_push(q, data):
rc.lpush(q, data)
def fifo_pop(q):
return rc.rpop(q)
def filo_push(q, data):
rc.lpush(q, data)
def filo_pop(q):
return rc.lpop(q)

        如果用 BLPOP,BRPOP 命令替代 LPOP, RPOP,则在 list 为空的时候还支持阻塞等待。不过,即使按照这种方式实现了持久化,如果在 POP 消息返回的时候网络故障,则依然会发生消息丢失,针对这种需求 Redis 提供了 RPOPLPUSH 和 BRPOPLPUSH 命令来先将提取的消息保存在另外一个 list 中,客户端可以先从这个 list 查看和处理消息数据,处理完毕之后再从这个 list 中删除消息数据,从而确保了消息不会丢失,示例如下:

def safe_fifo_push(q, data):
rc.lpush(q, data)
def safe_fifo_pop(q, cache):
msg = rc.rpoplpush(q, cache)
# check and do something on msg
rc.lrem(cache, 1) # remove the msg in cache list.
return msg

如果使用 BRPOPLPUSH 命令替代 RPOPLPUSH 命令,则可以在 q 为空的时候阻塞等待。


结语

        使用 Redis 作为分布式系统的共享内存,以共享内存模式为基础来实现分布式系统协调技术,虽然不像传统的基于消息传递的技术那样有着坚实的理论证明的基础,但是它在一些要求不苛刻的情况下不失为一种简单实用的轻量级解决方案,毕竟不是每个系统都需要严格的容错性等要求,也不是每个系统都会频繁地发生进程异常,而且 Redis 本身已经经受了工业界的多年实践和考验。另外,用 Redis 技术还有一些额外的好处,比如在开发过程中和生产环境中都可以直接观察到锁、队列的内容,实施的时候也不需要额外的特别配置过程等,它足够简单,在调试问题的时候逻辑清晰,进行排查和临时干预也比较方便。在可扩展性方面也比较好,可以动态扩展分布式系统的进程数目,而不需要事先预定好进程数目。

Redis 支持基于 Key 值 hash 的集群,在集群中应用本文所述技术时建议另外部署专用 Redis 节点(或者冗余 Redis 节点集群)来使用,因为在基于 Key 值 hash 的集群中,不同的 Key 值会根据 hash 值被分布到不同的集群节点上,而且对于 Lua 脚本的支持也受到限制,难以保证一些操作的原子性,这一点是需要考虑到的。使用专用节点还有一个好处是专用节点的数据量会少很多,当应用了 master/slave 部署或者 AOF 模式的时候,因为数据量少,master 和 slave 之间的同步会少很多,AOF 模式实时写入磁盘的数据也少很多,这样子也可以大大提高可用性。

本文示例所列 Python 代码在 Python3.4 下运行,Redis 客户端采用 redis 2.10.3,Redis 服务端版本为 3.0.1 版。

 ================================================================

更多干货,请关注微信公众号:newssoft

技术分享 


推荐阅读
  • Redis 是一个高性能的开源键值存储系统,支持多种数据结构。本文将详细介绍 Redis 中的六种底层数据结构及其在对象系统中的应用,包括字符串对象、列表对象、哈希对象、集合对象和有序集合对象。通过12张图解,帮助读者全面理解 Redis 的数据结构和对象系统。 ... [详细]
  • 问题描述现在,不管开发一个多大的系统(至少我现在的部门是这样的),都会带一个日志功能;在实际开发过程中 ... [详细]
  • 电商高并发解决方案详解
    本文以京东为例,详细探讨了电商中常见的高并发解决方案,包括多级缓存和Nginx限流技术,旨在帮助读者更好地理解和应用这些技术。 ... [详细]
  • Redis:缓存与内存数据库详解
    本文介绍了数据库的基本分类,重点探讨了关系型与非关系型数据库的区别,并详细解析了Redis作为非关系型数据库的特点、工作模式、优点及持久化机制。 ... [详细]
  • 第二章:Kafka基础入门与核心概念解析
    本章节主要介绍了Kafka的基本概念及其核心特性。Kafka是一种分布式消息发布和订阅系统,以其卓越的性能和高吞吐量而著称。最初,Kafka被设计用于LinkedIn的活动流和运营数据处理,旨在高效地管理和传输大规模的数据流。这些数据主要包括用户活动记录、系统日志和其他实时信息。通过深入解析Kafka的设计原理和应用场景,读者将能够更好地理解其在现代大数据架构中的重要地位。 ... [详细]
  • Maven + Spring + MyBatis + MySQL 环境搭建与实例解析
    本文详细介绍如何使用MySQL数据库进行环境搭建,包括创建数据库表并插入示例数据。随后,逐步指导如何配置Maven项目,整合Spring框架与MyBatis,实现高效的数据访问。 ... [详细]
  • 软件测试行业深度解析:迈向高薪的必经之路
    本文深入探讨了软件测试行业的发展现状及未来趋势,旨在帮助有志于在该领域取得高薪的技术人员明确职业方向和发展路径。 ... [详细]
  • 我的读书清单(持续更新)201705311.《一千零一夜》2006(四五年级)2.《中华上下五千年》2008(初一)3.《鲁滨孙漂流记》2008(初二)4.《钢铁是怎样炼成的》20 ... [详细]
  • 在OpenCV 3.1.0中实现SIFT与SURF特征检测
    本文介绍如何在OpenCV 3.1.0版本中通过Python 2.7环境使用SIFT和SURF算法进行图像特征点检测。由于这些高级功能在OpenCV 3.0.0及更高版本中被移至额外的contrib模块,因此需要特别处理才能正常使用。 ... [详细]
  • 使用 MATLAB 将高光谱数据集转换为伪彩色 CIE 图像
    本文介绍了一种方法,通过 MATLAB 将高光谱数据集的每个维度的图像转换为伪彩色 CIE 图像。用户只需指定波段即可完成转换。 ... [详细]
  • Windows操作系统提供了Encrypting File System (EFS)作为内置的数据加密工具,特别适用于对NTFS分区上的文件和文件夹进行加密处理。本文将详细介绍如何使用EFS加密文件夹,以及加密过程中的注意事项。 ... [详细]
  • 本文总结了一些开发中常见的问题及其解决方案,包括特性过滤器的使用、NuGet程序集版本冲突、线程存储、溢出检查、ThreadPool的最大线程数设置、Redis使用中的问题以及Task.Result和Task.GetAwaiter().GetResult()的区别。 ... [详细]
  • 阿里巴巴终面技术挑战:如何利用 UDP 实现 TCP 功能?
    在阿里巴巴的技术面试中,技术总监曾提出一道关于如何利用 UDP 实现 TCP 功能的问题。当时回答得不够理想,因此事后进行了详细总结。通过与总监的进一步交流,了解到这是一道常见的阿里面试题。面试官的主要目的是考察应聘者对 UDP 和 TCP 在原理上的差异的理解,以及如何通过 UDP 实现类似 TCP 的可靠传输机制。 ... [详细]
  • HBase Java API 进阶:过滤器详解与应用实例
    本文详细探讨了HBase 1.2.6版本中Java API的高级应用,重点介绍了过滤器的使用方法和实际案例。首先,文章对几种常见的HBase过滤器进行了概述,包括列前缀过滤器(ColumnPrefixFilter)和时间戳过滤器(TimestampsFilter)。此外,还详细讲解了分页过滤器(PageFilter)的实现原理及其在大数据查询中的应用场景。通过具体的代码示例,读者可以更好地理解和掌握这些过滤器的使用技巧,从而提高数据处理的效率和灵活性。 ... [详细]
  • 分布式开源任务调度框架 TBSchedule 深度解析与应用实践
    本文深入解析了分布式开源任务调度框架 TBSchedule 的核心原理与应用场景,并通过实际案例详细介绍了其部署与使用方法。首先,从源码下载开始,详细阐述了 TBSchedule 的安装步骤和配置要点。接着,探讨了该框架在大规模分布式环境中的性能优化策略,以及如何通过灵活的任务调度机制提升系统效率。最后,结合具体实例,展示了 TBSchedule 在实际项目中的应用效果,为开发者提供了宝贵的实践经验。 ... [详细]
author-avatar
邵世以_806
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有