基于Redis实现延迟队列
背景
在后端服务中,经常有这样一种场景,写数据库操作在异步队列中执行,且这个异步队列是多进程运行的,这时如果对同一资源进行写库操作,很有可能产生数据被覆盖等问题,于是就需要业务层在更新数据库之前进行加锁,这样保证在更改同一资源时,没有其他更新操作干涉,保证数据一致性。
但如果在更新前对数据库更新加锁,那此时又来了新的更新数据库的请求,但这个更新操作不能丢弃掉,需要延迟执行,那这就需要添加到延迟队列中,延迟执行。
那么如何实现一个延迟队列?利用Redis
的SortedSet
和String
这两种结构,就可以轻松实现。
# coding: utf8
"""Delay Queue"""
import json
import time
import uuid
import redis
class DelayQueue(object):
"""延迟队列"""
QUEUE_KEY = 'delay_queue'
DATA_PREFIX = 'queue_data'
def __init__(self, conf):
host, port, db = conf['host'], conf['port'], conf['db']
self.client = redis.Redis(host=host, port=port, db=db)
def push(self, data):
"""push
:param data: data
"""
# 唯一ID
task_id = str(uuid.uuid4())
data_key = '{}_{}'.format(self.DATA_PREFIX, task_id)
# save string
self.client.set(data_key, json.dumps(data))
# add zset(queue_key=>data_key,ts)
self.client.zadd(self.QUEUE_KEY, data_key, int(time.time())) def pop(self, num=5, previous=3):
"""pop多条数据
:param num: pop多少个
:param previous: 获取多少秒前push的数据
"""
# 取出previous秒之前push的数据
until_ts = int(time.time()) - previous
task_ids = self.client.zrangebyscore(
self.QUEUE_KEY, 0, until_ts, start=0, num=num)
if not task_ids:
return []
# 利用删除的原子性,防止并发获取重复数据
pipe = self.client.pipeline()
for task_id in task_ids:
pipe.zrem(self.QUEUE_KEY, task_id)
data_keys = [
data_key
for data_key, flag in zip(task_ids, pipe.execute())
if flag
]
if not data_keys:
return []
# load data
data = [
json.loads(item)
for item in self.client.mget(data_keys)
]
# delete string key
self.client.delete(*data_keys)
return data
实现思路
push
在push
数据时,执行如下几步:
- 生成一个唯一
key
,这里使用uuid4生成(uuid4是根据随机数生成的,重复概率非常小,具体参考这里) - 把数据序列化后存入这个唯一
key
的String
结构中 - 把这个唯一
key
加到SortedSet
中,score
是当前时间戳
这里利用
SortedSet
记录添加数据的时间,便于在获取时根据时间获取之前的数据,达到延迟的效果。而真正的数据则存放在
String
结构中,等获取时先拿到数据的key
再获取真正的数据。
这里可能有人会疑问,为什么不把真正的数据放到SortedSet
的name
中?
- 把数据放入
name
中可能会产生瞬间写入相同数据导致数据多条变一条的情况 - 把数据序列化放到
SortedSet
的name
中有些过大,不太符合使用习惯
pop
此pop
是可以获取多条数据的,上面的代码默认是获取延迟队列中3秒前的5条数据,具体思路如下:
- 计算
previous
秒前的时间戳,使用SortedSet
的zrangebysocre
方法获取previous
秒之前添加的唯一key
- 如果
SortedSet
中有数据,则利用Redis
删除的原子性,使用zrem
依次删除SortedSet
的元素,如果删除成功,则使用,防止多进程并发执行此方法,拿到相同的数据 - 那到可用的唯一
key
,从String
中获取真正的数据即可
这里最重要的是第二步,在拿出
SortedSet
的数据后,一定要防止其他进程并发获取到相同的数据,所以在这里使用zrem
依次删除元素,保证只有删除成功的进程才能使用这条数据。
使用
# coding: utf8
import time
from delay import DelayQueue
redis_conf = {'host': '127.0.0.1', 'port': 6379, 'db': 0}
# 构造延迟队列对象
queue = DelayQueue(redis_conf)
# push 20条数据
for i in range(20):
item = {'user': 'user-{}'.format(i)}
queue.push(item) # 从延迟队列中马上获取10条数据
data = queue.pop(num=10)
# 刚添加的马上获取是获取不到的
assert len(data) == 0
# 休眠10秒
time.sleep(10)
# 从延迟队列中获取10条数据
data = queue.pop(num=10)
assert len(data) == 10
# 从延迟队列中获取截止到5秒之前添加的10条数据
data = queue.pop(num=10, previous=5)
assert len(data) == 10
使用就比较简单了,在实际使用过程中,每次在处理正常队列时,通过上面的方法获取一下延迟队列的数据,如果延迟队列中有数据,那么按照业务正常处理就可以了,这样就达到了数据延迟处理的效果。
转自:Kaito的博客
基于Redis实现延迟队列的更多相关文章
- Delayer 基于 Redis 的延迟消息队列中间件
Delayer 基于 Redis 的延迟消息队列中间件,采用 Golang 开发,支持 PHP.Golang 等多种语言客户端. 参考 有赞延迟队列设计 中的部分设计,优化后实现. 项目链接:http ...
- 基于redis的延迟消息队列设计
需求背景 用户下订单成功之后隔20分钟给用户发送上门服务通知短信 订单完成一个小时之后通知用户对上门服务进行评价 业务执行失败之后隔10分钟重试一次 类似的场景比较多 简单的处理方式就是使用定时任务 ...
- 基于redis的延迟消息队列设计(转)
需求背景 用户下订单成功之后隔20分钟给用户发送上门服务通知短信 订单完成一个小时之后通知用户对上门服务进行评价 业务执行失败之后隔10分钟重试一次 类似的场景比较多 简单的处理方式就是使用定时任务 ...
- 基于Redis实现延时队列服务
背景 在业务发展过程中,会出现一些需要延时处理的场景,比如: a.订单下单之后超过30分钟用户未支付,需要取消订单 b.订单一些评论,如果48h用户未对商家评论,系统会自动产生一条默认评论 c.点我达 ...
- 【转】基于Redis实现延时队列服务
背景 在业务发展过程中,会出现一些需要延时处理的场景,比如: a.订单下单之后超过30分钟用户未支付,需要取消订单b.订单一些评论,如果48h用户未对商家评论,系统会自动产生一条默认评论c.点我达订单 ...
- 基于Redis的消息队列php-resque
转载:http://netstu.5iunix.net/archives/201305-835/ 最近的做一个短信群发的项目,需要用到消息队列.因此开始了我对消息队列选型的漫长路. 为什么选型会纠结呢 ...
- [视频教程] 基于redis的消息队列实现与思考
使用redis的list列表来实现消息队列功能,相信大家都听过消息队列,但是在业务中可能并没有真正去使用它.在公司项目中正好有个场景使用到了消息队列,因此就来说一下流程.在web界面上有个功能是群发邮 ...
- 基于Redis的消息队列使用:spring boot2.0整合redis
一 . 引入依赖 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="ht ...
- Redis实现延迟队列的正确姿势
在之前探讨延时队列的文章中我们提到了 redisson delayqueue 使用 redis 的有序集合结构实现延时队列,遗憾的是 go 语言社区中并无类似的库.不过问题不大,没有轮子我们自己造. ...
随机推荐
- JDK 7中的函数式编程思想[转载]
原文作者的观点是Lambda表达式一定会包含在JDK 7中,而全文也着重介绍了这方面的知识,作者认为函数式编程的概念也将出现在JDK 7中. Lambda表达式 Lambda表达式并不是什么新概念,自 ...
- Java自定义注解Annotation详解
注解相当于一种标记,在程序中加了注解就等于为程序打上了某种标记,没加,则等于没有某种标记,以后,javac编译器,开发工具和其他程序可以用反射来了解你的类及各种元素上有无何种标记,看你有什么标记,就去 ...
- C# Json 序列化和反序列化 工具类 Newtonsoft.Json.dll
引用: Newtonsoft.Json.dll // 引用: using Newtonsoft.Json; using Newtonsoft.Json.Converters; // 定义 实体测试类 ...
- 畅通project(杭电1232)
畅通project Time Limit: 4000/2000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others) Total ...
- zookeeper伪分布式集群安装
1.安装3个zookeeper 1.1创建集群安装的目录 1.2配置一个完整的服务 这里不做详细说明,参考我之前写的 zookeeper单节点安装 进行配置即可,此处直接复制之前单节点到集群目录 创建 ...
- RESTful最佳实践
哲学 不要为了RESTful而RESTful 在能表达清楚的情况下,简单就是美 接口路径设计 接口设计原则 URI指向的是唯一的资源对象 示例: 指向ID为yanbo.ai的Account对象 GET ...
- C#秘密武器之异步编程
一.概述 1.什么是异步? 异步操作通常用于执行完成时间可能较长的任务,如打开大文件.连接远程计算机或查询数据库.异步操作在主应用程序线程以外的线程中执行.应用程序调用方法异步执行某个操作时,应用程序 ...
- liunx系统安装jdk的方法
1.下载jdk 下载地址: http://www.oracle.com/technetwork/java/javasebusiness/downloads/java-archive-downloads ...
- 【一步一步走(1)】远程桌面软件VNC的安装与配置
近期在VPS上搭建Python Web环境.走了非常多弯路,借此记下. 先说说购买的VPS(PhotonVPS),我可不是打广告.仅仅是感觉这个VPS服务提供商还不错推荐给你大家,我之前也是体验过阿里 ...
- 【Datastage】函数大全
一. 类型转换函数 类型转换函数用于更改参数的类型. 以下函数位于表达式编辑器的"类型转换"类别中.方括号表示参数是可选的.缺省日期格式为 %yyyy-%mm-%dd. 以下示例按 ...