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的消息队列使用示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
缓存
这里我们说的缓存,是可以丢失或过期的数据,不能丢失或过期的缓存(或者应该叫做数据库了)不在本文的叙述范围。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的算法。
两种方式的示例代码分别如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
发布订阅
Redis的发布订阅机制,在客户端与服务端由于某些问题链接失效时,中间订阅的数据会丢失;所以在实际生产环境中,很少应用这种机制。
示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
Redis lua应用
Lua 脚本功能是 Reids 2.6 版本的最大亮点, 通过内嵌对 Lua 环境的支持, Redis 解决了长久以来不能高效地处理 CAS (check-and-set)命令的缺点, 并且可以通过组合使用多个命令, 轻松实现以前很难实现或者不能高效实现的模式。
优点:原子性(由于Redis是单线程模型,同一时刻只能处理一个lua脚本),更小的请求包
应用场景:事务实现,批量处理
以下示例代码实现了getAndSet命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|
Dump故障
当Redis使用内存大于操作系统剩余内存的2倍时,使用dump持久化机制可能会造成服务器宕机、假死等情况。原因是dump时,Redis会fork一个子进程,根据写实复制原则,如果Redis中的数据会发生修改时,操作系统会把服务进程的内存copy一份给子进程,具体copy多少根据数据修改的覆盖度;这时如果内存不够用,操作系统会使用swap扩展内存,性能急剧下降,如果swap也不够了,则可能发生宕机、假死等情况。
解决方案:设置maxmemory,监控Redis内存使用(Redis info命令),场景允许的情况下开启lru机制。
maxmemory故障
故障描述:设置了maxmemory,内存用完,客户端无法写入
解决方案:对Redis内存使用进行监控,根据业务场景控制内存使用;如果内存确实不够用了,考虑引入分布式Redis集群方案
redis访问漏洞
这个漏洞的原理非常简单,只需执行以几条命令即可:
1 2 3 4 |
|
通过以上几条命令,可以将你的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’即可。