redis拥有五大基本的数据结构:string(字符串)、list(列表)、hash(哈希)、sets(集合)、sorted sets(有序集合)。列表结构类似于线性表里的双向链表,双向链表的特点是既可以从表头顺序遍历链表,也可以从表尾开始顺序遍历链表。双向链表示意图:
利用list数据结构,我们可以实现简单的消息队列。redis提供了很多操作list的api,这里以php的redis扩展包装的redis api为例,简单介绍下主要的操作方法,详细的参数及使用方法参考官方文档。
-
blPop, brPop
阻塞版的lPop方法。
-
bRPopLPush
阻塞版的rPopLPush方法
-
lIndex, lGet
返回按顺序索引的特定序号的元素
-
lInsert
在元素的前面或后面插入值
-
lPop
返回并移除列表的第一个元素
-
lPush
在列表的头部添加一个字符串值,若列表不存在,则创建列表后再添加。
-
lPushx
如果列表存在,则在列表的头部添加一个字符串值
-
lRange, lGetRange
返回列表中特定范围的值
-
lRem, lRemove
将列表中重复出现的某个值,按列表顺序删除指定个数
-
lSet
设置某个索引处的值为特定值
-
lTrim, listTrim
移除列表中的某个范围值
-
rPop
返回并删除列表最后一个值
-
rPopLPush
移除第一个列表的尾部元素,然后压入第二个列表的头部,且返回这个元素的值
-
rPush
在列表的尾部添加一个字符串值,若列表不存在,则创建列表后再添加。
-
rPushX
如果列表存在,则在列表的尾部添加一个字符串值
-
lLen, lSize
返回列表的长度
需要注意的一点是,redis的列表结构只能保存字符串值。我们可以用redis的list数据结构来实现消息队列,具体点就是用redis的list结构来保存生产者产生的消息,此外,我们还需要一个生产者来生产消息,和一个消费者来消费或者说处理消息。php的redis扩展提供了很好的操作redis的接口,因此可以用php来实现生产者和消费者。首先新建一个MessageProducer.php文件,里面有我们要的生产者类MessageProducer:
<?php
/**
* 消息生产者
*
*@author ldy
*/
class MessageProducer
{
//Redis实例
private $redis = null;
/**
*连接redis服务器
*
*@param array $config redis连接配置
*/
public function connect($config)
{
$this->redis = new \Redis;
$this->redis->connect($config['host'], $config['port']);
if(isset($config['password']) && !empty($config['password'])){
$this->redis->auth($config['password']);
}
if(isset($config['db']) && !empty($config['db'])){
$this->redis->select($config['db']);
}
}
/**
*发布一条消息到指定队列
*
*@param string $queueName 队列名称
*@param string $message 消息
*@param int $expireAt 过期时间,unix秒级时间戳
*
*@return true|throw exception
*
*/
public function publish($queueName, $message, $expireAt=null)
{
$message = array(
'message' => $message,
'expireAt' => $expireAt,
'publishAt' => time(),
);
//redis的列表结构只能保存字符串数据,这里将message数组 转换成json格式
$message = json_encode($message);
//发布消息到redis队列
try {
$this->redis->lPush($queueName, $message);
return true;
} catch (\RedisException $e) {
return false;
} catch (\ErrorException $e) {
throw $e;
}
}
}
然后再创建一个消费者类处理消息:
<?php
/**
*消费者
*
*@author ldy
*/
class MessageConsumer
{
//Redis实例
private $redis = null;
//队列名称
protected $queueName;
//设置消息处理器
protected $handler;
/**
*
*@param string $queueName
*@param object|\Closure $handler
*/
function __construct($queueName, $handler)
{
$this->queueName = $queueName;
$this->handler = $handler;
}
/**
*设置队列名称
*@param string $queueName
*/
public function setQueueName($queueName)
{
$this->queueName = $queueName;
}
/**
*设置消息处理器
*@param object|\Closure $handler
*/
public function setHandler($handler)
{
$this->handler = $handler;
}
/**
*连接redis服务器
*
*@param array $config redis连接配置
*/
public function connect($config)
{
$this->redis = new \Redis;
$this->redis->connect($config['host'], $config['port']);
if(isset($config['password']) && !empty($config['password'])){
$this->redis->auth($config['password']);
}
if(isset($config['db']) && !empty($config['db'])){
$this->redis->select($config['db']);
}
}
/*处理消息*/
public function consume()
{
//从队列中取出最早的一条消息
$message = $this->redis->rPop($this->queueName);
if($message===false)
return;
//转换回数组格式
$message = json_decode($message, true);
//如果消息已经过期,则丢弃不做处理
if($message['expireAt'] != null && time()>$message['expireAt'])
return ;
if($this->handler instanceof \Closure){
call_user_func($this->handler, $message['message']);
}elseif(is_object($this->handler)){
$this->handler->handle($message['message']);
}else{
throw new Exception("消息处理器类型错误,消息处理器是一个对象或闭包", 1);
}
}
/*运行消息消费者*/
public function run()
{
// 让脚本一直运行
while (true) {
$this->consume();
//每隔五秒读取消息进行处理
sleep(5);
}
}
}
创建一个脚本publish.php产生消息,这个脚本调用了上面的生产者类:
#!/usr/bin/php
<?php
require './MessageProducer.php';
$producer = new MessageProducer();
$producer->connect([
'host' => '127.0.0.1',
'port' => 6379,
]);
$producer->publish('my-queue', 'You sad hello world!');
最后再创建一个testConsumer.php文件测试消费者是否正常工作:
#!/usr/bin/php
<?php
require './MessageConsumer.php';
//创建一个闭包来处理消息
$handler = function($message){
$f = fopen('message.log', 'a+');
fwrite($f, '['.date('Y-m-d H:i:s').'] Got message:'.$message."\n");
};
$consumer = new MessageConsumer('my-queue', $handler);
$consumer->connect([
'host' => '127.0.0.1',
'port' => 6379,
]);
$consumer->run();
在linux命令行运行testConsumer.php文件:
root@vagrant-ubuntu-trusty-64:/vagrant/php# php testConsumer.php
这样php脚本程序就开始监听队列消息了。运行php publish.php脚本生产消息,测试消息是否被消费者接收到:
消息被成功接收到。这样一个简单的消息队列服务器就实现了。在生产环境我们可能希望消费者程序成为一个常驻进程,这样就不用每次都要在命令行运行testConsumer.php脚本开启。这里推荐用Supervisor进程管理器实现,详情可以查看官网,这里不做详细介绍。