最近在学习redis,觉得redis确实是分布式系统中的一个利器,于是看了很多官方文档,带着一些问题,结合平时项目中使用情况作了一些总结,本文不适合redis初学者,初学者可以查看Redis 命令参考先学习下redis。
一、redis和memcached的区别
以下来自Stack Overflow的一个问答memcached-vs-redis,已经说的十分清楚了:
redis比memcached更强大、更流行、更受支持。Memcached只能做Redis能做的一小部分事情。即使在它们重叠的一些地方,redis也能做的更好。
以下是详细对比:
- 速度:两者都非常快。基准测试因工作负载、版本和许多其他因素而异,但通常显示redis与memcached一样快或几乎一样快。我推荐使用redis,但并不是因为memcached慢
- 内存使用:
- memcached:你可以指定缓存大小,当插入项目时,守护进程会快速增长到略大于此大小。除了重新启动memcached之外,从来没有真正的方法可以回收这些空间。您所有的键都可以过期,您可以刷新数据库,它仍然会使用您配置它时使用的全部RAM。
- redis:设置最大大小取决于您。Redis使用的内存永远不会超过它必须使用的内存,它会将不再使用的内存还给您。
- 我将100000个大约2KB字符串(大约200MB)的随机句子存储到这两个字符串中。Memcached内存的使用增长到大约225MB。Redis内存的使用增长到大约228MB。刷新后,redis下降到大约29MB, memcached保持在~225MB。它们在存储数据方面也同样高效,但redis能够回收数据。
- 持久化:这对redis来说是一个明显的胜利,因为它在默认情况下是这样做的,并且持久化有很多可配置项。Memcached没有在没有第三方工具的情况下转储到磁盘的机制。
- 扩展: 在您需要一个以上的实例作为缓存之前,这两种方法都为您提供了大量的空间。Redis提供了一些工具来帮助您做这些事,而memcached不提供这些工具。
二、批量导入数据
有时候我们需要批量导入一些数据,这时候可以通过将命令写入文本中,然后通过管道导入redis,文本中的命令不需要显示的分隔符结尾,内容如下:
set key1 value1
set key2 value2
set key3 value3
导入命令如下:cat command.txt|redis-cli -h 10.10.23.15
也可以加上--pipe
批量执行命令
三、关于redis Sentinel和Cluster
redis的哨兵是官方作为redis高可用的解决方案,针对的是redis的主从故障转移,没有分片的功能。如果你的数据一个redis实例能够完全存放时,那么redis Sentinel是一种不错的方案,能够监视redis集群,并提供故障转移的功能。但是如果一台redis不能完全放下你的数据时,你必须选择扩展redis,有很多解决方案,早期很多采用了客户端分片或者代理的形式,在redis3.0官方支持集群,这种方案是服务端分片,一开始不理解哨兵和集群的关系,以为哨兵是用来做高可用,集群只是分片,但是其实集群本身就是自治的,并且已经有了高可用的功能,在配置集群的时候,可以配置主节点和相应的从节点,集群会相互探测节点的存活,在主节点下线时进行故障转移,等于说,搭建集群事并不需要也不应该才让哨兵介入。
四、提高redis性能
这里我看到了一篇十分不错的文章:Redis性能问题排查解决手册,内容十分不错,在排查redis性能问题的时候可以当一个手册来看,我主要关注的点如下:
- slowlog: 这个命令用来查redis的慢查询日志,之前在优化mysql时,慢查询日志非常有用,让我对这个比较敏感,另外redis的慢查询日志是保存在内存中的,在重启的时候,日志会丢失。像对于redis来说,一般的命令执行非常快,瓶颈主要会在网络io,所以设置10ms是比较保守的,一个命令如果要
- 命令复杂度:redis在处理客户端请求时是单线程的,所以redis对cpu非常敏感,如果一个命令非常耗时就会导致其他的命令被阻塞,因此我们得十分注意redis命令的复杂度,例如集合的运算和list的随机存取时,在用到复杂度高的命令时需要十分注意value的大小,像list的lset命令是O(n)级别的,如果能始终控制list在几百以下,那么经常使用也是完全没问题的,但是如果上万的场景,如果这样的需求,是不是应该考虑做列表的切片,或者采用zset来代替。
- 批量执行命令:redis的很多命令都提供了批量执行的版本,可以用这些命令很容易优化,上面也说了redis的主要瓶颈在io,所以如果可以用批量执行命令的话,尽量批量执行。对于不同类型的命令,redis还提供了流水线,流水线是一种客户端行为,你打开流水线之后发送的命令会被客户端缓存着,当你告知客户端发送命令时,命令会被批量传送到redis,执行完后批量返回,只有一次往返,提高了吞吐量。
这里想说下zset的两个命令: ZRANGE key start stop [WITHSCORES]
和ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
,zrange的复杂度O(log(N)+M),N为有序集的基数,M为被结果集的基数没有太多疑问。但是ZRANGEBYSCORE这个命令的服务度,如果没有后面的[LIMIT offset count]那么他和zange的复杂度是一样的,但是如果加上后面的分页参数他的复杂度实际是O(log(N)+M+offset),试想这样的场景:zset有200000个元素,且分数一样(这样的场景是存在的,见:redis实战),查找排名199990到200000的元素,两者的复杂度天差地别,实现的分页功能确实一样的。所以,一共要小心这种隐藏的坑。
五、redis的锁
在实现一个需要加锁的操作时,redis提供了三种方案:
- 事务: 乐观锁,适合冲突很少的场景
- 分布式锁: 悲观锁,适合冲突频繁的场景,如果冲突频繁事务的重试次数为激增,大量消耗cpu
- lua: 适合性能瓶颈的优化,是一种锁的优化手段
六、lua脚本和事务
redis中lua脚本十分简单,可以把它理解为redis中的存储过程。在数据库中,尤其是mysql,很多公司都禁止使用存储过程,因为不好维护,有很多人喜欢把逻辑写在存储过程(其实我觉得如果存储过程中只写简单的sql,并且有统一的公司规范也是比较容易维护的,主要是有些人写在代码中,有些人写存储过程,而且逻辑写经常写在存储过程,所以直接禁止比较好)。
对于redis的lua来说,我觉得在有些情况下是很有必要的,因为redis单线程处理客户端命令的这一个特点,lua脚本的执行也是原子性的,在很多的需要事务的场景都可以采用lua来代替,比如:获取key a的值,如果a>10,那么设置b为0。采用lua可以减少网络的往返次数,在对性能有极致要求的情况下,采用lua提高几倍的吞吐量是非常有用的手段。
但是我认为在大多数情况下都不应该使用lua,尽量采用redis本身支持的事务和流水线等功能来提高吞吐量,理由如下:
- lua对集群支持不友好:一般场景下事务的性能已经足够了,如果采用lua之后又在达到瓶颈,那么势必要考虑集群来横向扩展了,lua如果涉及到多个键分布在不同的实例上,集群会直接返回失败,上面说过集群不支持多键命令,同样不支持key在不同实例的lua脚本。如果要支持这个lua脚本,那么你要保证键都在一个实例。详情参见:Keys hash tags
- lua可维护性:lua虽然很简单,但是不是团队中每个人都会,如果里面写了很多的逻辑,可维护性比较差
我的建议还是尽量不使用lua,如果有必要使用lua可以作为后期性能瓶颈的优化方案。
另外在说说事务和lua,事务在某些场景还是lua不能代替的,如:在事务watch之后,我需要检查下内存中的变量a,如果a大于0,那么执行事务。lua的上下文是在redis中,它能看到的只是整个redis,所以lua并不能完全代替事务。
七、关于redis的事务概念
很多人都知道redis的事务概念,也知道数据库中的事务,但是这两个事务是没有什么关联的。redis的事务保证了操作的原子性,在操作执行的过程中,操作不会被打断,例如操作 1,2,3 即使1失败了,2和3照样会执行。redis事务的原子性和数据库事务的原子性也没有太大关系,如果说非要对应那么更加对应的是数据库事务的隔离性,数据库事务的原子性和隔离性定义如下(摘自360百科):
原子性: 整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。
redis没有回滚,如果失败也继续执行
隔离性:隔离状态执行事务,使它们好像是系统在给定时间内执行的唯一操作。如果有两个事务,运行在相同的时间内,执行相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆,必须串行化或序列化请求,使得在同一时间仅有一个请求用于同一数据。
redis单线程保证了执行过程中不会被另一个事务打扰
八、redis的zset
在使用的过程中觉得zset的功能实在是强大,因为他有序性的特点,能提供很多功能。有必要好好学习底层的实现(hashmap+skiplist),非常有用的数据结构。
九、redis的key驱逐策略
memcache默认是基于lru驱逐,而redis的默认是关闭的,如果内存满了,redis会拒绝执行写命令。在编写程序的时候需要知道redis有没有配置lru,如果配置了那么你的key都是不可靠的,有可能会丢失,这就是缓存的典型场景。对于缓存来说,最好给你的每个key都设置上过期时间。
十、redis部署方案
关于redis的部署方案有很多,需要针对具体的应用场景,主要考虑的是成本,一个备份节点没用到,成本很高,另外如果作为数据库必须持久化
- 如果只是缓存,无高可用: 单机版,(如果做了持久化,要考虑数据的过期问题,主要是程序的考虑)
- 缓存+高可用:一主一从+哨兵(只在从节点持久化)
- 缓存+高可用+高性能:一主多从+哨兵 客户端做读写分离(所有的读写分离都需要考虑一致性的问题)
- 数据库,一个实例能放下所有数据:可以采用主从(双机持久化)+哨兵,
- 数据库+一个实例不能放下所有数据:集群+主从+双机持久化
方案无非是 持久化、主从高可用、读写分离、集群等组合,需要根据实际的业务来部署,建议在微服务架构下不要采用一套方案,而是多个方案部署,比如缓存部署一套,内存数据库方案部署一套,针对高并发场景单一致性要求不高的部署读写分离。每个服务之间用到的数据库在最好隔开,如果使用同一个缓存就选择不同的db,有利于后期迁移数据。关于部署方案可以参考阿里云redis文档,非常值得学习。
这里有一点疑问,我在网上看到很多都说主从采用链式复制,包括阿里云的读写分离也采用了这样一个方案,好处显而易见,但是不知道自己搭建的话怎么实现,我看官网文档的sentinel都是星式复制,找不到相关的部署方法,如果有知道的,望大神告知!