一、业务背景从技术角度来看,技术方案的选择受实际业务场景的限制,均以解决实际业务场景为目标。在我们实际的业务场景中,需要收集和上报游戏维度的行为数据,考虑数据的量级,进行尽力而为的投放,允许部分丢弃数据。数据上报支持游戏维度批量上报,支持同一游戏128个行为的批量上报。数据上报需要时效性控制,上报数据必须是上报时间前3分钟的数据。整体数据的业务形态如下图所示:2.技术选型从业务角度来看,包括数据采集和数据上报。我们将数据收集与生产者进行比较,将数据报告与消费者进行比较。典型的生产消费模型。生产消费模型通过队列+锁或无锁Disruptors在JVM进程内部实现,跨进程场景通过MQ(RocketMQ/kafka)解耦。但从具体的业务场景来看,消息消费有很多限制,包括:游戏维度的批量行为上报、行为上报的时限、各种技术方案的选择比较等。方案一使用RocketMQ或Kafaka等消息队列存储上报的消息,但消费端在业务过程中需要根据游戏维度考虑聚合。技术细节涉及到按照游戏维度进行拆分。报告是在前提下触发的。在该方案中,消息中间件所扮演的角色本质上是一个消息中转站,并没有解决任何业务场景中提到的游戏维度拆分、批处理和时效性。方案二在方案一的基础上,寻求解决游戏维度的消息分组、批量消费、时效性的技术方案。队列通过Redis的list结构实现(进一步需要实现定长队列)解决游戏维度的消息分组;Redis列表支持的Lrange用于实现批量消费;业务端多线程用于解决时效性问题,针对高频游戏使用单独的线程池进行处理,以上两种方式可以保证消费速度大于生产速度。方案对比两种方案对比后,决定使用Redis实现一个伪消息中间件:通过List对象实现一个定长队列,用于保存游戏维度的行为消息(以游戏为key的List对象保存用户行为);保存所有带有行为数据的游戏列表;使用Set进行去重判断,保证2中List对象的唯一性,整体技术方案如下图所示:制作流程第1步:从游戏维度PUSH某些行为数据到游戏维度的队列中.Step2:判断该游戏是否在游戏集合中,如果是则直接返回,否则转步骤3。Step3:将游戏PUSH到游戏列表中。消费过程第一步:循环从游戏对象列表中取出一个游戏。步骤2:将步骤1得到的游戏对象送入游戏对象的行为数据队列,批量获取数据进行处理。3.技术原理在Redis的支持命令中,List和Set的基本命令结合Lua脚本实现了整个技术方案。在消息数据层面,通过单独的List循环维护要消费的游戏维度的数据,每个游戏维度使用一个定长的List来保存消息。在消息生产过程中,通过结合List的llen+lpop+rpush实现游戏维度的定长队列,保证队列长度可控。在消息消费过程中,通过组合List的lrange+ltrim实现游戏维度消息的批量消费。在整个执行的复杂度层面,需要保证时间复杂度在0(N)常量维度,以保证时间可控。3.1Lua脚本EVAL脚本numkeyskey[key...]arg[arg...]时间复杂度:取决于脚本本身的执行时间复杂度。>eval"return{KEYS[1],KEYS[2],ARGV[1],ARGV[2]}"2key1key2firstsecond1)"key1"2)"key2"3)"first"4)"second"Redis使用相同的Lua解释器来运行所有命令。Redis还保证脚本以原子方式执行:在执行脚本时不会执行其他脚本或Redis命令。这种语义类似于MULTI/EXEC的语义。从所有其他客户端的角度来看,脚本的效果要么仍然不可见,要么已经完成。Redis使用同一个Lua解释器运行所有命令,我们可以保证脚本的执行是原子的。效果类似于添加MULTI/EXEC。Lua脚本中的多个命令都是原子执行的,保证了命令执行的线程安全。Lua脚本结合List命令实现定长队列,实现批量消费。lua脚本只支持单键操作,不支持多键操作。3.2ListobjectLLENkey计算List的长度时间复杂度:O(1)。LPOPkey[count]从List左侧移除元素时间复杂度:O(N),其中N为移除元素的个数。RPUSHkeyelement[element...]从List右侧开始保存元素时间复杂度:O(N),其中N为保存元素的个数。List的基本命令包括计算List的长度、删除数据和添加数据。整个命令的复杂度是O(N)常数时间。综合以上三个命令,我们可以保证一个定长队列,这是通过判断队列长度是否达到定长,结合增删队列元素来完成的。LRANGEkeystartend时间复杂度:O(S+N),S为偏移量start,N为指定区间的元素个数。索引参数start和stop都是以0为底,即0代表列表的第一个元素,1代表列表的第二个元素,以此类推。您还可以使用负数下标,-1表示列表的最后一个元素,-2表示列表的倒数第二个元素,依此类推。LTRIMkeystartstop时间复杂度:O(N)其中N是操作要删除的元素数。修剪(trim)一个现有列表,使列表将只包含指定范围内的指定元素。List的基本命令包括批量返回数据和裁剪数据,整体命令复杂度为O(N)常数时间。结合以上两条命令,我们可以批量消费数据并移除队列数据,通过LRANGE批量返回数据,通过LTRIM保留剩余数据。3.3SetobjectSADDkeymember[member...]向Set集合中添加数据。时间复杂度:O(1)。SISMEMBER关键成员确定Set集合中是否有元素。时间复杂度:O(1)。通过Set集合保证数据的唯一性,时间复杂度可控。4.技术应用4.1生产消息定义LUA脚本CACHE_NPPA_EVENT_LUA="localretVal=0"+"localkey=KEYS[1]"+"localnum=tonumber(ARGV[1])"+"localval=ARGV[2]"+"localexpire=tonumber(ARGV[3])"+"if(redis.call('llen',key)
