Redis实现消息队列技巧

  Redis是高性能的数据结构服务器,使用list数据类型实现消息队列非常简单:

127.0.0.1:6379> lpush mq msg1
(integer) 1
127.0.0.1:6379> lpush mq msg2
(integer) 2
127.0.0.1:6379> lpush mq msg3
(integer) 3
127.0.0.1:6379> rpop mq
"msg1"

  但这种实现毕竟不是一个完整的消息队列产品,比如消息队列的长度限制、消息的有效时间限制、消息可靠处理等均需要编码实现,下面就介绍一下具体实现方法。

 

消息队列长度限制

  一般地,我们最先想到的方法就是在客户端执行消息入列后检查消息的长度,如超过设定长度时截断队列即可,PHP实现示例代码如下:

<?php
$redis = new Redis();
$redis->connect("127.0.0.1", 6379);
for ($i = 1; $i <= D_MQ_MAX_SIZE + 1; $i++)
{
  $len = $redis->lpush("mq", "msg".$i);
  if ($len > 3)
    $redis->ltrim("mq", 0, 2);
}
$msg = $redis->rpop("mq");
echo $msg;
?>

  这种方式可以解决限制消息队列长度问题,但需要和服务器两次通讯,可以换另一种方法,就是使用Lua脚本方式执行,Redis从2.6.0开始内置Lua解析器,可以保证一个脚本里的命令在服务器原子性执行,命令行示例代码:

127.0.0.1:6379> script load "local len = redis.call('lpush', KEYS[1], ARGV[1]); local size = tonumber(ARGV[2]); if (len > size) then len = size; redis.call('ltrim', KEYS[1], 0, size - 1); end; return len;"
"cd4a3153713ef4e3f55a6fa85bf63a3ead99fbb8"
127.0.0.1:6379> evalsha cd4a3153713ef4e3f55a6fa85bf63a3ead99fbb8 1 mq "test1" 3
(integer) 1
127.0.0.1:6379> evalsha cd4a3153713ef4e3f55a6fa85bf63a3ead99fbb8 1 mq "test2" 3
(integer) 2
127.0.0.1:6379> evalsha cd4a3153713ef4e3f55a6fa85bf63a3ead99fbb8 1 mq "test3" 3
(integer) 3
127.0.0.1:6379> evalsha cd4a3153713ef4e3f55a6fa85bf63a3ead99fbb8 1 mq "test4" 3
(integer) 3
127.0.0.1:6379> rpop mq
"test2"

  上面的代码加载一段Lua脚本,之后执行4次消息入列,消息内容分别是test1、test2、test3、test4,消息的队列长度限定为3,test1、test2、test3入列时返回的消息队列长度分别是1、2、3,test4入列时返回的队列长度仍然为3,最后pop消息是test2,显然test4入列时test1被丢弃了。

 

消息有效时间限制

  我们知道,Redis不支持元素的生存时间,看了前面的Lua脚本,实现消息有效时间限制很自然的想到使用Lua脚本来实现,另外建立一个list来保存消息入列时间,例如:

local len = redis.call('lpush', KEYS[1], ARGV[1]);
local sec = redis.call('time')[1];
redis.call('lpush', KEYS[2], sec);
return len;

  不过很遗憾,这段Lua脚本执行会报错,Redis要求执行写操作的Lua脚本在不同主机、不同时间都要产生相同的结果,亦即“无副作用的纯函数”,原因是要保证Redis复制、AOF重加载时相同的命令产生的数据必须相同。没有办法,这样就需要在客户端调用Redis time命令获取Redis服务器时间再执行消息入列,PHP代码如下:

<?php
$redis = new Redis();
$redis->connect("127.0.0.1", 6379);
$time = $redis->time();
$lua = "local len = redis.call('lpush', KEYS[1], ARGV[1]); redis.call('lpush', KEYS[2], ARGV[2]); return len;";
$len = $redis->eval($lua, array("mq", "mqt", "msg", $time[0]), 2);
?>

  然后,我们使用以下PHP代码从消息队列中pop消息:

<?php
$redis = new Redis();
$redis->connect("127.0.0.1", 6379);
$time = $redis->time();
$sec = $time[0] - 10;
$lua = <<<EOT
local msg = nil;
while true do
  msg = redis.call('rpop', KEYS[1]);
  if not msg then
    break;
  end;
  local sec = tonumber(redis.call('rpop', KEYS[2]));
  if sec > tonumber(ARGV[1]) then
    break;
  end;
end;
return msg;
EOT;
$msg = $redis->eval($lua, array("mq", "mqt", $sec), 2);
var_dump($msg);
?>

  这段代码设定消息的生存时间为5秒,超过5秒的消息被丢弃,pop时返回最早一条未超时的消息。

 

消息可靠处理

  在某些场景下要求消息队列里的每一个消息都要得到处理,如果客户端在pop一条消息后没处理就宕机了,那么就会产生消息丢失,这是我们不希望看到的。还好,Redis提供了rpoplpush命令,可以在消息出列同时加入到另一个副本队列,我们可以开一个管理进程监视副本队列,当副本队列中的元素长时间没得到处理时可以重新压入消息队列供其它客户端处理,具体代码就不贴了。


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值