Redis作为当下最流行的内存Nosql数据库,有着诸多的应用场景。在不同的应用场景,对Redis的部署、配置以及使用方式都存在的不同地方。根据我的工作经验,把队列、缓存、归并、去重等应用场景的“最佳实践”整理如下。
本文中的所有代码,均可在github上找到:https://github.com/huyanping/RedisStudy
队列
Redis的list数据结构经常会被用作队列来使用,常用的方法有:lpop/rpop、lpush/rpush、llen、lindex等。由于Redis提供的list是一个双向链表,我们也可以把list当做栈来使用。使用Redis的list作为队列时,需要注意以下几个问题:
-
队列中的数据一般具有比较高的可靠性要求,Redis的持久化机制最好使用AOF方式,保证数据不丢失
-
同样由于对数据的可靠性要求较高,内存监控尤为重要,如果出现队列堆积内存用光造成无法提供服务的情况
-
如果只有一个消费者在消费队列,推荐使用lindex先读取消息,消费完之后在lpop扔掉,这样可以保证事务性,避免消息处理失败后消息丢失
-
如果是多个消费者在消费队列,消息处理失败的情况下可以将消息重新写入队列,前期是消息没有有序性要求
通过批量(multi)和并行的方式可以提高生产者和消费者的处理能力。批量处理可以减少网络通信量,同时减少Redis在不同任务间切换的开销。并行的好处就是当一个客户端在准备或处理数据时并且Redis空闲时,另一个客户端可以从Redis读取数据;这样可以尽量保证Redis始终保持在繁忙状态。
如果通过以上优化,仍然有队列堆积的情况,建议启动多个Redis实例。由于Redis是单线程模型,无法利用多核CPU,开启多个实例能够明显提升吐吞量。Redis的集群方案有很多种,也可以简单的在客户端使用hash算法实现。具体实现方案已经超出本文叙述范围,不再累赘。
基于Redis list的消息队列使用示例代码如下:
// 生产者`
`namespace` `jenner\redis\study\queue;`
`use` `Jenner\SimpleFork\Process;`
`use` `Jenner\SimpleFork\Queue\RedisQueue;`
`class` `Producer` `extends` `Process`
`{`
`/**`
`* start producer process`
`*/`
`public` `function` `run()`
`{`
`$queue` `=` `new` `RedisQueue(``'127.0.0.1'``, 6379, 1);`
`for` `(``$i` `= 0;` `$i` `< 100000;` `$i``++) {`
`$queue``->put(``getmypid``() .` `'-'` `. mt_rand(0, 1000));`
`}`
`$queue``->close();`
`}`
`}`
/ 消费者`
`namespace` `jenner\redis\study\queue;`
`use` `Jenner\SimpleFork\Process;`
`use` `Jenner\SimpleFork\Queue\RedisQueue;`
`class` `Consumer` `extends` `Process`
`{`
`/**`
`* start consumer process`
`*/`
`public` `function` `run()`
`{`
`$queue` `=` `new` `RedisQueue(``'127.0.0.1'``, 6379, 1);`
`while` `(true) {`
`$res` `=` `$queue``->get();`
`if` `(``$res` `!== false) {`
`echo` `$res` `. PHP_EOL;`
`}` `else` `{`
`break``;`
`}`
`}`
`}`
`}`
缓存
这里我们说的缓存,是可以丢失或过期的数据,不能丢失或过期的缓存(或者应该叫做数据库了)不在本文的叙述范围。Redis的字典结构常用来做缓存使用,常用的方法有:set/get、hget/hgetall/hset等。使用redis作为缓存时,需要注意以下几个问题:
-
由于Redis可用的内存是有限的,不能容忍redis内存的无限增加,最好设置最大内存maxmemory
-
在开启maxmemory的情况下,可以启用lru机制,设置key的expire,当到达Redis最大内存时,Redis会根据最近最少用算法对key进行自动淘汰;lru的策略有6种,可参考:http://www.aikaiyuan.com/7089.html
-
Redis的持久化策略和Redis故障恢复时间是一个博弈的过程,如果你希望在发生故障时能够尽快恢复,应该启用dump备份机制,但dump机制要求你必须保留至少1/3(经验值)的可用内存(写时复制),所以你可能没办法分配尽可能多的内存给Redis;如果能够容忍Redis漫长的故障恢复时间,可以使用AOF持久化机制,同时关闭dump机制,这样可以突破保留1/3内存的限制。
关于缓存的使用方法,不属于本文叙述范围,可参考:http://tech.meituan.com/avalanche-study.html
示例代码太多,这里就只贴个地址:https://github.com/huyanping/RedisStudy/tree/master/src/spider
计算
Redis提供的原子递增递减方法以及有序集合等可以承担一些计算任务,例如访问量统计等。常用的方法有:incr/decr、hincrby、zadd/zcard等。
在使用redis作为计算服务时,需要注意一下几个问题:
-
计算场景的数据一般对可靠性要求比较高,建议启用AOF持久化机制,根据恢复时间和内容利用率的考虑确定是否开启dump机制。
-
redis的单线程模型决定了redis无法利用多核CPU,这里建议引入redis集群解决方案,当然仍然可以在客户端通过hash方案解决。
-
批量发送、批量导出
去重
Redis的hset和HyperLogLog数据结构可以在使用少量内存的情况下对数据进行去重。在有大量数据需要去重的场景比较试用。Redis的HyperLogLog只需要使用12K的内存空间即可对2的64次方个记录进行去重。具体选用哈希字典还是HyperLogLog需要根据你需要去重的数据量综合决定,如果你需要去重的数据总体占用空间远小于12K,使用哈希字典即可,如果超过12K,推荐使用HyperLogLog。常用的命令有:pfadd/pfcount、hset/hlen。这里需要注意,pfadd返回的是布尔型,表示该值是否已经存在;不可以通过累加pfadd的结果判定唯一记录数,必须调用pfcount获取,这个应该是算法的原因,记住就好。有兴趣的童鞋可以深究一下HyperLogLog的算法。
两种方式的示例代码分别如下:
`<?php`/
/ HyperLogLog`
`namespace` `jenner\redis\study\unique;`
`use` `jenner\redis\study\tool\Logger;`
`class` `HyperLogLog`
`{`
`/**`
`* @var \Redis`
`*/`
`protected` `$redis``;`
`/**`
`* @var array`
`*/`
`protected` `$ips``;`
`/**`
`* default hyperloglog key`
`*/`
`const` `KEY =` `"ip-unique-hyperloglog"``;`
`/**`
`* HyperLogLog constructor.`
`* @param array $ips`
`*/`
`public` `function` `__construct(``array` `$ips``)`
`{`
`$this``->redis =` `new` `\Redis();`
`$this``->redis->connect(``"127.0.0.1"``, 6379);`
`$this``->redis->select(3);`
`$this``->ips =` `$ips``;`
`}`
`/**`
`* start to count ips using hyperloglog`
`*/`
`public` `function` `start()`
`{`
`Logger::info(``"unique process start"``);`
`$this``->redis->pfadd(self::KEY,` `$this``->ips);`
`Logger::info(``"unique done. ip count:"` `.` `$this``->redis->pfcount(self::KEY));`
`}`
`}`
<?php
// set
`namespace` `jenner\redis\study\unique;`
`use` `jenner\redis\study\tool\Logger;`
`class` `Set`
`{`
`/**`
`* @var \Redis`
`*/`
`protected` `$redis``;`
`/**`
`* @var array`
`*/`
`protected` `$ips``;`
`/**`
`*`
`*/`
`const` `KEY =` `"ip-unique-normal"``;`
`/**`
`* Set constructor.`
`* @param array $ips`
`*/`
`public` `function` `__construct(``array` `$ips``)`
`{`
`$this``->redis =` `new` `\Redis();`
`$this``->redis->connect(``"127.0.0.1"``, 6379);`
`$this``->redis->select(3);`
`$this``->ips =` `$ips``;`
`}`
`/**`
`* start to count ips using set`
`*/`
`public` `function` `start()`
`{`
`Logger::info(``"unique process start"``);`
`foreach` `(``$this``->ips` `as` `$ip``) {`
`$this``->redis->sAdd(self::KEY,` `$ip``);`
`}`
`Logger::info(``"unique done. ip count:"` `.` `$this``->redis->sCard(self::KEY));`
`}`
`}`
发布订阅 —-
Redis的发布订阅机制,在客户端与服务端由于某些问题链接失效时,中间订阅的数据会丢失;所以在实际生产环境中,很少应用这种机制。
示例代码如下:
`<?php`
`// publisher`
`namespace` `jenner\redis\study\pubsub;`
`class` `Publisher`
`{`
`/**`
`* @var \Redis`
`*/`
`protected` `$redis``;`
`/**`
`* default pubsub key`
`*/`
`const` `KEY =` `"pubsub-demo"``;`
`/**`
`* Publisher constructor.`
`*/`
`public` `function` `__construct()`
`{`
`$this``->redis =` `new` `\Redis();`
`$this``->redis->connect(``"127.0.0.1"``, 6379);`
`$this``->redis->select(4);`
`}`
`public` `function` `publish()`
`{`
`$count` `= 10;`
`for` `(``$i` `= 0;` `$i` `<` `$count``;` `$i``++) {`
`$this``->redis->publish(self::KEY, mt_rand(0, 10000));`
`}`
`}`
`}`
`<?php`
`// subscriber`
`namespace` `jenner\redis\study\pubsub;`
`use` `jenner\redis\study\tool\Logger;`
`class` `Subscriber`
`{`
`/**`
`* default pubsub key`
`*/`
`const` `KEY =` `"pubsub-demo"``;`
`/**`
`* @var \Redis`
`*/`
`protected` `$redis``;`
`/**`
`* Subscriber constructor.`
`*/`
`public` `function` `__construct()`
`{`
`$this``->redis =` `new` `\Redis();`
`$this``->redis->connect(``"127.0.0.1"``, 6379);`
`$this``->redis->select(4);`
`}`
`public` `function` `subscribe()`
`{`
`$this``->redis->subscribe(``array``(self::KEY),` `function` `(``$redis``,` `$channel``,` `$message``) {`
`Logger::info(``"get message["` `.` `$message` `.` `"] from channel["` `.` `$channel` `.` `"]"``);`
`});`
`}`
`}`
Redis lua应用 ———–
Lua 脚本功能是 Reids 2.6 版本的最大亮点, 通过内嵌对 Lua 环境的支持, Redis 解决了长久以来不能高效地处理 CAS (check-and-set)命令的缺点, 并且可以通过组合使用多个命令, 轻松实现以前很难实现或者不能高效实现的模式。
优点:原子性(由于Redis是单线程模型,同一时刻只能处理一个lua脚本),更小的请求包
应用场景:事务实现,批量处理
以下示例代码实现了getAndSet命令:
`<?php`
`// redis lua`
`namespace` `jenner\redis\study\lua;`
`use` `jenner\redis\study\tool\Logger;`
`class` `Lua`
`{`
`/**`
`* @var \Redis`
`*/`
`protected` `$redis``;`
`/**`
`* Lua constructor.`
`*/`
`public` `function` `__construct()`
`{`
`$this``->redis =` `new` `\Redis();`
`$this``->redis->connect(``"127.0.0.1"``, 6379);`
`$this``->redis->select(2);`
`}`
`/**`
`* @param $key`
`* @param $value`
`* @return bool`
`*/`
`public` `function` `set(``$key``,` `$value``)`
`{`
`return` `$this``->redis->set(``$key``,` `$value``);`
`}`
`/**`
`* @param $key`
`* @param $value`
`*/`
`public` `function` `getAndSet(``$key``,` `$value``)`
`{`
`$lua` `= <<<GLOB_MARK`
`local value = redis.call(``'get'``, KEYS[1])`
`redis.call(``'set'``, KEYS[1], ARGV[1])`
`return` `value`
`GLOB_MARK;`
`$result` `=` `$this``->redis->``eval``(``$lua``,` `array``(``$key``,` `$value``), 1);`
`Logger::info(``"eval script result:"` `. var_export(``$result``, true));`
`}`
`/**`
`* @return string`
`*/`
`public` `function` `error()`
`{`
`return` `$this``->redis->getLastError();`
`}`
`}`
Dump故障 —— 当Redis使用内存大于操作系统剩余内存的2倍时,使用dump持久化机制可能会造成服务器宕机、假死等情况。原因是dump时,Redis会fork一个子进程,根据写实复制原则,如果Redis中的数据会发生修改时,操作系统会把服务进程的内存copy一份给子进程,具体copy多少根据数据修改的覆盖度;这时如果内存不够用,操作系统会使用swap扩展内存,性能急剧下降,如果swap也不够了,则可能发生宕机、假死等情况。
解决方案:设置maxmemory,监控Redis内存使用(Redis info命令),场景允许的情况下开启lru机制。
maxmemory故障
故障描述:设置了maxmemory,内存用完,客户端无法写入
解决方案:对Redis内存使用进行监控,根据业务场景控制内存使用;如果内存确实不够用了,考虑引入分布式Redis集群方案
redis访问漏洞
这个漏洞的原理非常简单,只需执行以几条命令即可:
redis> config` `set` `dbfilename authorized_keys`
redis> config` `set` `dir` `'/root/.ssh'`
redis>` `set` `xxoo` `"\n\n\nyour public ssh key"`
redis> save`
通过以上几条命令,可以将你的ssh公钥写入对方的Redis服务器,从而获取root权限。这个漏洞的利用条件也比较苛刻,需要满足以下几个条件:
-
Redis需是root用户运行,或已知Redis运行用户
-
6379端口无防火墙拦截
-
Redis无访问密码
-
config set命令没有被禁用
根据以上利用条件,对应防御手段如下:
-
优先监听127.0.0.1网卡(如过redis是给本机访问),优先监听内网网卡
-
防火墙对6379端口访问进行限制
-
使用非root用户运行redis
-
redis开启密码访问(养成好习惯)
-
禁用config set命令
一般情况下做到第二点,基本就不会被黑了,但我们应该尽量做到第四点,尽善尽美。
redis自动重连
RedisRetry是一个支持自动重连的redis客户端封装,项目地址:https://github.com/huyanping/RedisRetry
原理:使用__call方法对redis的原生方法封装,当发生RedisException时,自动关闭并重新建立连接,执行n次,每次相隔m毫秒。
常量:
REDIS_RETRY_TIMES 重试次数
REDIS_RETRY_DELAY 间隔时间,单位毫秒
使用方式:把使用Redis类的地方,添加’use \Jenner\RedisRetry\Redis’即可。
以上是我在Redis使用过程中整理的应用场景与“最佳实践”。