一、背景需求
需求主要为:
1、优先级分为高、中、低(优先级队列);
2、支持多生产者、多消费者模型(解决竞争);
3、期间进程退出,队列内容不会丢失(持久化);
选型方面考虑了第三方工具,先调研了一下Redis,发现其中LIST数据结构非常适用上诉需求;
二、相关知识
2.1 List结构
Redis List是Redis最重要的数据结构之一,内部实现是一个双向链表,链表上的每个节点都包含一个字符串。主要功能有:
1、从链表两端推入(push)、弹出(pop)元素;
2、根据偏移量对链表进行修剪(trim);
3、读取单个或者多个元素;
4、根据值查找或者移出元素;
2.2 List结构相关命令(help @list)
BLPOP key [key ...] timeout
summary: Remove and get the first element in a list, or block until one is available
BRPOP key [key ...] timeout
summary: Remove and get the last element in a list, or block until one is available
BRPOPLPUSH source destination timeout
summary: Pop a value from a list, push it to another list and return it; or block until one is available
LINDEX key index
summary: Get an element from a list by its index
LINSERT key BEFORE|AFTER pivot value
summary: Insert an element before or after another element in a list
LLEN key
summary: Get the length of a list
LPOP key
summary: Remove and get the first element in a list
LPUSH key value [value ...]
summary: Prepend one or multiple values to a list
LPUSHX key value
summary: Prepend a value to a list, only if the list exists
LRANGE key start stop
summary: Get a range of elements from a list
LREM key count value
summary: Remove elements from a list
LSET key index value
summary: Set the value of an element in a list by its index
LTRIM key start stop
summary: Trim a list to the specified range
RPOP key
summary: Remove and get the last element in a list
RPOPLPUSH source destination
summary: Remove the last element in a list, prepend it to another list and return it
RPUSH key value [value ...]
summary: Append one or multiple values to a list
RPUSHX key value
summary: Append a value to a list, only if the list exists
2.3 基础C语言接口
Redis源码包中包含了C语言接口库Hiredis(deps/hiredis),接口的快速入门如下:
1、连接Redis实例
redisContext *redisConnect(const char *ip, int port);
The function `redisConnect` is used to create a so-called `redisContext`.
The context is where Hiredis holds state for a connection.
The `redisContext` struct has an integer `err` field that is non-zero when the connection is in an error state.
The field `errstr` will contain a string with a description of the error.
2、发送命令
void *redisCommand(redisContext *c, const char *format, ...);
There are several ways to issue commands to Redis. The first that will be introduced is `redisCommand`
reply = redisCommand(context, "SET foo bar");
The specifier `%s` interpolates a string in the command, and uses `strlen` to determine the length of the string:
reply = redisCommand(context, "SET foo %s", value);
When you need to pass binary safe strings in a command, the `%b` specifier can be used.
Together with a pointer to the string, it requires a `size_t` length argument of the string:
reply = redisCommand(context, "SET foo %b", value, (size_t) valuelen);
3、解析回复
The standard replies that `redisCommand` are of the type `redisReply`.
The `type` field in the `redisReply` should be used to test what kind of reply was received:
1) REDIS_REPLY_STATUS
The command replied with a status reply. The status string can be accessed using `reply->str`.
The length of this string can be accessed using `reply->len`.
2) REDIS_REPLY_ERROR
The command replied with an error. The error string can be accessed identical to `REDIS_REPLY_STATUS`.
3) REDIS_REPLY_INTEGER
The command replied with an integer. The integer value can be accessed using the `reply->integer` field of type `long long`.
4) REDIS_REPLY_NIL
The command replied with a **nil** object. There is no data to access.
5) REDIS_REPLY_STRING
A bulk (string) reply. The value of the reply can be accessed using `reply->str`.
The length of this string can be accessed using `reply->len`.
6) REDIS_REPLY_ARRAY
A multi bulk reply. The number of elements in the multi bulk reply is stored in `reply->elements`.
Every element in the multi bulk reply is a `redisReply` object as well and can be accessed via `reply->element[..index..]`.
Redis may reply with nested arrays but this is fully supported.
4、释放、断开实例连接
Replies should be freed using the `freeReplyObject()` function.
To disconnect and free the context the following function can be used: void redisFree(redisContext *c);
三、编程实例
该实例使用LPUSH进行入列操作、BRPOP进行出列操作,由于BRPOP支持超时阻塞,所以可以避免轮询带来的延迟
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <hiredis.h>
#define FAILURE EXIT_FAILURE
#define SUCCESS EXIT_SUCCESS
#define FREE_REDIS(c) if (c) { \
redisFree(c); \
c = NULL; \
}
#define FREE_REPLY(r) if (r) { \
freeReplyObject(r); \
r = NULL; \
}
static void __do_handle(redisReply *reply)
{
if (reply->elements == 2) {
printf("DEQUEUE: %s %s\n",
reply->element[0]->str, reply->element[1]->str);
usleep(3000); //TODO something
}
else {
printf("Unknow\n");
}
}
int test_enqueue(redisContext *c, int times)
{
int ix = 0;
const char key[3][10] = {"hig", "mid", "low"};
redisReply *reply = NULL;
for (ix = 0; ix < times; ix++) {
reply = redisCommand(c, "LPUSH %s element-%05d", key[random()%3], ix);
FREE_REPLY(reply);
}
return SUCCESS;
}
int test_dequeue(redisContext *c)
{
int ret = FAILURE;
redisReply *reply = NULL;
while (1) {
/* Dequeue timeout 10s */
reply = redisCommand(c, "BRPOP hig mid low 10");
switch (reply->type) {
case REDIS_REPLY_NIL:
printf("empty timeout\n");
continue;
case REDIS_REPLY_ARRAY:
__do_handle(reply);
continue;
default:
printf("Unknow type: %d\n", reply->type);
ret = FAILURE;
goto _E1;
}
FREE_REPLY(reply);
}
ret = SUCCESS;
_E1:
FREE_REPLY(reply);
return ret;
}
int main(int argc, char *argv[])
{
int ret = FAILURE;
struct timeval timeout = { 1, 500000 }; // 1.5 seconds
const char *hostname = "127.0.0.1";
int port = 6379;
redisContext *c = NULL;
c = redisConnectWithTimeout(hostname, port, timeout);
if (c == NULL || c->err) {
if (c) {
printf("Connection error: %s\n", c->errstr);
}
else {
printf("Connection error: can't allocate redis context\n");
}
ret = FAILURE;
goto _E1;
}
printf("Connect success\n");
if (argv[1]) {
assert(SUCCESS == test_enqueue(c, atoi(argv[1])));
}
else {
assert(SUCCESS == test_dequeue(c));
}
printf("Done\n");
ret = SUCCESS;
_E1:
/* Disconnects and frees the context */
FREE_REDIS(c);
printf("Result:\t\t\t\t[%s]\n", ret ? "Failure" : "Success");
exit(ret ? EXIT_FAILURE : EXIT_SUCCESS);
}
四、执行结果
使用单生产者、双消费者进行测试:
producter顺序推送 element-00001 到 element-00019,随机优先级入列
customer-1 信息如下:
customer-2 信息如下:
可见第一批信息 element-00001、element-00002刚入列就分别被 customer-1、customer-2抢到,
然后在usleep(3000)期间,producter将20个elements全部入列,下一轮由两个customer进行优先级排序瓜分;
参考文章:
[1] https://github.com/antirez/redis/blob/unstable/deps/hiredis/README.md
[2] https://github.com/antirez/redis/blob/unstable/README.md