为什么不搞集群服务也能实现Redis高可用?

基于内存的Redis应该是目前各种Web开发业务中最为常用的Key-Value数据库了。

我们经常在业务中用其存储用户登陆态(Session存储),加速一些热数据的查询(相比较MySQL而言,速度有数量级的提升),做简单的消息队列(LPUSH和BRPOP)、订阅发布(PUB/SUB)系统等等。规模比较大的互联网公司,一般都会有专门的团队,将Redis存储以基础服务的形式提供给各个业务调用。

不过任何一个基础服务的提供方,都会被调用方问起的一个问题是:你的服务是否具有高可用性?

最好不要因为你的服务经常出问题,导致我这边的业务跟着遭殃。最近我所在的项目中也自己搭了一套小型的“高可用”Redis服务,在此做一下自己的总结和思考。

一、可能出现的异常

首先我们要定义一下对于Redis服务来说怎样才算是高可用,即在各种出现异常的情况下,依然可以正常提供服务。或者宽松一些,出现异常的情况下,只经过很短暂的时间即可恢复正常服务。所谓异常,应该至少包含了以下几种可能性:

【异常1】某个节点服务器的某个进程突然down掉(例如某开发手残,把一台服务器的Redis-Server进程kill了)。

【异常2】某台节点服务器down掉,相当于这个节点上所有进程都停了(例如某运维手残,把一个服务器的电源拔了;例如一些老旧机器出现硬件故障)。

【异常3】任意两个节点服务器之间的通信中断了(例如某临时工手残,把用于两个机房通信的光缆挖断了)。

其实以上任意一种异常都是小概率事件。

而做到高可用性的基本指导思想就是:多个小概率事件同时发生的概率可以忽略不计。只要我们设计的系统可以容忍短时间内的单点故障,即可实现高可用性。

对于搭建高可用Redis服务,网上已有了很多方案,例如Keepalived、Codis、Twemproxy、Redis Sentinel。其中Codis和Twemproxy主要是用于大规模的Redis集群中,也是在Redis官方发布Redis Sentinel之前twitter和豌豆荚提供的开源解决方案。

我的业务中数据量并不大,所以搞集群服务反而是浪费机器了。最终在Keepalived和Redis Sentinel之间做了个选择,选择了官方的解决方案Redis Sentinel。

Redis Sentinel可以理解为一个监控Redis Server服务是否正常的进程,并且一旦检测到不正常,可以自动地将备份(slave)Redis Server启用,使得外部用户对Redis服务内部出现的异常无感知。我们按照由简至繁的步骤,搭建一个最小型的高可用的Redis服务。

二、探索参考方案

方案1:单机版Redis Server,无Sentinel

未分类

一般情况下,我们搭的个人网站,或者平时做开发时,会起一个单实例的Redis Server。调用方直接连接Redis服务即可,甚至Client和Redis本身就处于同一台服务器上。

这种搭配仅适合个人学习娱乐,毕竟这种配置总会有单点故障的问题无法解决。一旦Redis服务进程挂了,或者服务器1停机了,那么服务就不可用了。并且如果没有配置Redis数据持久化的话,Redis内部已经存储的数据也会丢失。

方案2:主从同步Redis Server,单实例Sentinel

未分类

为了实现高可用,解决方案1中所述的单点故障问题,我们必须增加一个备份服务,即在两台服务器上分别各启动一个Redis Server进程,一般情况下由master提供服务,slave只负责同步和备份。

与此同时,再额外启动一个Sentinel进程,监控两个Redis Server实例的可用性,以便在master挂掉的时候,及时把slave提升到master的角色继续提供服务,这样就实现了Redis Server的高可用。

这基于一个高可用服务设计的依据,即单点故障本身就是个小概率事件,而多个单点同时故障(即master和slave同时挂掉),可以认为是(基本)不可能发生的事件。

对于Redis服务的调用方来说,现在要连接的是Redis Sentinel服务,而不是Redis Server了。常见的调用过程是,Client先连接Redis Sentinel并询问目前Redis Server中哪个服务是master,哪些是slave,然后再去连接相应的Redis Server进行操作。

当然目前的第三方库一般都已经实现了这一调用过程,不再需要我们手动去实现(例如Nodejs的ioredis,PHP的predis,Golang的go-redis/redis,JAVA的jedis等)。

然而,我们实现了Redis Server服务的主从切换之后,又引入了一个新的问题,即Redis Sentinel本身也是个单点服务,一旦Sentinel进程挂了,那么客户端就没办法链接Sentinel了。所以说,方案2的配置并无法实现高可用性。

方案3:主从同步Redis Server,双实例Sentinel

未分类

为了解决方案2的问题,我们把Redis Sentinel进程也额外启动一份,两个Sentinel进程同时为客户端提供服务发现的功能。对于客户端来说,它可以连接任何一个Redis Sentinel服务,来获取当前Redis Server实例的基本信息。

通常情况下,我们会在Client端配置多个Redis Sentinel的链接地址,Client一旦发现某个地址连接不上,会去试图连接其他的Sentinel实例,这当然也不需要我们手动实现,各个开发语言中比较热门的Redis连接库都帮我们实现了这个功能。

我们预期是:即使其中一个Redis Sentinel挂掉了,还有另外一个Sentinel可以提供服务。

然而,愿景是美好的,现实却是很残酷的。如此架构下,依然无法实现Redis服务的高可用。

方案3示意图中,红线部分是两台服务器之间的通信,而我们所设想的异常场景(【异常2】)是,某台服务器整体down机,不妨假设服务器1停机,此时,只剩下服务器2上面的Redis Sentinel和slave Redis Server进程。

这时,Sentinel其实是不会将仅剩的slave切换成master继续服务的,也就导致Redis服务不可用,因为Redis的设定是只有当超过50%的Sentinel进程可以连通并投票选取新的master时,才会真正发生主从切换。本例中两个Sentinel只有一个可以连通,等于50%并不在可以主从切换的场景中。

你可能会问,为什么Redis要有这个50%的设定?

假设我们允许小于等于50%的Sentinel连通的场景下也可以进行主从切换。试想一下【异常3】,即服务器1和服务器2之间的网络中断,但是服务器本身是可以运行的。如下图所示:

未分类

实际上对于服务器2来说,服务器1直接down掉和服务器1网络连不通是一样的效果,反正都是突然就无法进行任何通信了。

假设网络中断时我们允许服务器2的Sentinel把slave切换为master,结果就是你现在拥有了两个可以对外提供服务的Redis Server。

Client做任何的增删改操作,有可能落在服务器1的Redis上,也有可能落在服务器2的Redis上(取决于Client到底连通的是哪个Sentinel),造成数据混乱。即使后面服务器1和服务器2之间的网络又恢复了,那我们也无法把数据统一了(两份不一样的数据,到底该信任谁呢?),数据一致性完全被破坏。

方案4:主从同步Redis Server,三实例Sentinel

未分类

鉴于方案3并没有办法做到高可用,我们最终的版本就是上图所示的方案4了。

实际上这就是我们最终搭建的架构。我们引入了服务器3,并且在3上面又搭建起一个RedisSentinel进程,现在由三个Sentinel进程来管理两个Redis Server实例。这种场景下,不管是单一进程故障、还是单个机器故障、还是某两个机器网络通信故障,都可以继续对外提供Redis服务。

如果你的机器比较空闲,当然也可以把服务器3上面也开启一个Redis Server,形成1master+2slave的架构,每个数据都有两个备份,可用性会提升一些。当然也并不是slave越多越好,毕竟主从同步也是需要时间成本的。

在方案4中,一旦服务器1和其他服务器的通信完全中断,那么服务器2和3会将slave切换为master。对于客户端来说,在这么一瞬间会有2个master提供服务,并且一旦网络恢复了,那么所有在中断期间落在服务器1上的新数据都会丢失。

如果想要部分解决这个问题,可以配置Redis Server进程,让其在检测到自己网络有问题的时候,立即停止服务,避免在网络故障期间还有新数据进来(可以参考Redis的min-slaves-to-write和min-slaves-max-lag这两个配置项)。

至此,我们就用3台机器搭建了一个高可用的Redis服务。其实网上还有更加节省机器的办法,就是把一个Sentinel进程放在Client机器上,而不是服务提供方的机器上。

只不过在公司里面,一般服务的提供方和调用方并不来自同一个团队。两个团队共同操作同一个机器,很容易因为沟通问题导致一些误操作,所以出于这种人为因素的考虑,我们还是采用了方案4的架构。并且由于服务器3上面只跑了一个Sentinel进程,对服务器资源消耗并不多,还可以用服务器3来跑一些其他的服务。

易用性:像使用单机版Redis一样使用Redis Sentinel

作为服务的提供方,我们总是会讲到用户体验问题。在上述方案当中始终有一个让Client端用的不是那么舒服的地方。

对于单机版Redis,Client端直接连接Redis Server,我们只需要给一个IP和Port,Client就可以使用我们的服务了。

而改造成Sentinel模式之后,Client不得不采用一些支持Sentinel模式的外部依赖包,并且还要修改自己的Redis连接配置,这对于“矫情”的用户来讲显然是不能接收的。

有没有办法还是像在使用单机版的Redis那样,只给Client一个固定的IP和Port就可以提供服务呢?

答案当然是肯定的。这可能就要引入虚拟IP(Virtual IP、VIP),如下图所示:

未分类

我们可以把虚拟IP指向Redis Server master所在的服务器,在发生Redis主从切换的时候,会触发一个回调脚本,回调脚本中将VIP切换至slave所在的服务器。这样对于Client端来说,他仿佛在使用的依然是一个单机版的高可用Redis服务。

三、结语

搭建任何一个服务,做到“能用”其实是非常简单的,就像我们运行一个单机版的Redis。

不过一旦要做到“高可用”,事情就会变得复杂起来。业务中使用了额外的两台服务器,3个Sentinel进程+1个Slave进程,只是为了保证在那小概率的事故中依然做到服务可用。

在实际业务中我们还启用了supervisor做进程监控,一旦进程意外退出,会自动尝试重新启动。

Redis的概述、搭建及简单使用(基于CentOS)

1. 概述

   

Redis 简介

Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库。

Redis 与其他 key – value 缓存产品有以下三个特点:

  • Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
  • Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
  • Redis支持数据的备份,即master-slave模式的数据备份。

Redis 优势

  • 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
    丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
  • 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
  • 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。

Redis与其他key-value存储有什么不同?

  • Redis有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。Redis的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。

  • Redis运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。在内存数据库方面的另一个优点是,相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样Redis可以做很多内部复杂性很强的事情。同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。

2. 搭建

(1)获取想要版本的下载地址(可直接去官网查询想要的版本,右击复制地址) 

http://download.redis.io/releases/redis-4.0.12.tar.gz

(2)进入对应目录,执行下载命令

[root@ezreal thirdparty]# wget http://download.redis.io/releases/redis-4.0.12.tar.gz

未分类

(3)当前文件夹下解压 

[root@ezreal redis]# tar -zxvf redis-4.0.12.tar.gz 

(4)进入解压后的文件夹并make

[root@ezreal redis]# cd redis-4.0.12

[root@ezreal redis-4.0.12]# make

(5)启动

[root@ezreal redis-4.0.12]# cd src/

[root@ezreal src]# ./redis-server 

未分类

3. 简单使用 

(1) 配置后台启动

a) 编辑redis.conf文件 打开守护进程(文件位置在根目录下)

未分类

(2) 启动redis并应用redis.conf配置文件

[root@ezreal src]# ./redis-server ../redis.conf

未分类

(3) 打开客户端并进行简单测试

[root@ezreal src]# ./redis-cli 
127.0.0.1:6379> hset testkey testfiled testvalue
(integer) 1
127.0.0.1:6379> hget testkey testfiled
"testvalue"
127.0.0.1:6379> 

本篇到此结束,嘿嘿嘿嘿嘿嘿嘿!

Redis的7个应用场景

一:缓存——热数据

热点数据(经常会被查询,但是不经常被修改或者删除的数据),首选是使用redis缓存,毕竟强大到冒泡的QPS和极强的稳定性不是所有类似工具都有的,而且相比于memcached还提供了丰富的数据类型可以使用,另外,内存中的数据也提供了AOF和RDB等持久化机制可以选择,要冷、热的还是忽冷忽热的都可选。

结合具体应用需要注意一下:很多人用spring的AOP来构建redis缓存的自动生产和清除,过程可能如下:

  • Select 数据库前查询redis,有的话使用redis数据,放弃select 数据库,没有的话,select 数据库,然后将数据插入redis

  • update或者delete数据库钱,查询redis是否存在该数据,存在的话先删除redis中数据,然后再update或者delete数据库中的数据

上面这种操作,如果并发量很小的情况下基本没问题,但是高并发的情况请注意下面场景:

为了update先删掉了redis中的该数据,这时候另一个线程执行查询,发现redis中没有,瞬间执行了查询SQL,并且插入到redis中一条数据,回到刚才那个update语句,这个悲催的线程压根不知道刚才那个该死的select线程犯了一个弥天大错!于是这个redis中的错误数据就永远的存在了下去,直到下一个update或者delete。

二:计数器

诸如统计点击数等应用。由于单线程,可以避免并发问题,保证不会出错,而且100%毫秒级性能!爽。

命令:INCRBY

当然爽完了,别忘记持久化,毕竟是redis只是存了内存!

三:队列

相当于消息系统,ActiveMQ,RocketMQ等工具类似,但是个人觉得简单用一下还行,如果对于数据一致性要求高的话还是用RocketMQ等专业系统。

由于redis把数据添加到队列是返回添加元素在队列的第几位,所以可以做判断用户是第几个访问这种业务

队列不仅可以把并发请求变成串行,并且还可以做队列或者栈使用

四:位操作(大数据处理)

用于数据量上亿的场景下,例如几亿用户系统的签到,去重登录次数统计,某用户是否在线状态等等。

想想一下腾讯10亿用户,要几个毫秒内查询到某个用户是否在线,你能怎么做?千万别说给每个用户建立一个key,然后挨个记(你可以算一下需要的内存会很恐怖,而且这种类似的需求很多,腾讯光这个得多花多少钱。。)好吧。这里要用到位操作——使用setbit、getbit、bitcount命令。

原理是:

redis内构建一个足够长的数组,每个数组元素只能是0和1两个值,然后这个数组的下标index用来表示我们上面例子里面的用户id(必须是数字哈),那么很显然,这个几亿长的大数组就能通过下标和元素值(0和1)来构建一个记忆系统,上面我说的几个场景也就能够实现。用到的命令是:setbit、getbit、bitcount

五:分布式锁与单线程机制

  • 验证前端的重复请求(可以自由扩展类似情况),可以通过redis进行过滤:每次请求将request Ip、参数、接口等hash作为key存储redis(幂等性请求),设置多长时间有效期,然后下次请求过来的时候先在redis中检索有没有这个key,进而验证是不是一定时间内过来的重复提交

  • 秒杀系统,基于redis是单线程特征,防止出现数据库“爆破”

  • 全局增量ID生成,类似“秒杀”

六:最新列表

例如新闻列表页面最新的新闻列表,如果总数量很大的情况下,尽量不要使用select a from A limit 10这种low货,尝试redis的 LPUSH命令构建List,一个个顺序都塞进去就可以啦。不过万一内存清掉了咋办?也简单,查询不到存储key的话,用mysql查询并且初始化一个List到redis中就好了。

七:排行榜

谁得分高谁排名往上。命令:ZADD(有续集,sorted set)分数排行榜,高分的前100名。

Redis几个重要的健康指标

存活情况

所有指标中最重要的当然是检查redis是否还活着,可以通过命令PING的响应是否是PONG来判断。

连接数

连接的客户端数量,可通过命令src/redis-cli info Clients | grep connected_clients得到,这个值跟使用redis的服务的连接池配置关系比较大,所以在监控这个字段的值时需要注意。另外这个值也不能太大,建议不要超过5000,如果太大可能是redis处理太慢,那么需要排除问题找出原因。

未分类

另外还有一个拒绝连接数(rejected_connections)也需要关注,这个值理想状态是0。如果大于0,说明创建的连接数超过了maxclients,需要排查原因。是redis连接池配置不合理还是连接这个redis实例的服务过多等。

阻塞客户端数量

blocked_clients,一般是执行了list数据类型的BLPOP或者BRPOP命令引起的,可通过命令src/redis-cli info Clients | grep blocked_clients得到,很明显,这个值最好应该为0。

使用内存峰值

监控redis使用内存的峰值,我们都知道Redis可以通过命令config set maxmemory 10737418240设置允许使用的最大内存(强烈建议不要超过20G),为了防止发生swap导致Redis性能骤降,甚至由于使用内存超标导致被系统kill,建议used_memory_peak的值与maxmemory的值有个安全区间,例如1G,那么used_memory_peak的值不能超过9663676416(9G)。另外,我们还可以监控maxmemory不能少于多少G,比如5G。因为我们以前生产环境出过这样的问题,运维不小心把10G配置成了1G,从而导致服务器有足够内存却不能使用的悲剧。

内存碎片率

mem_fragmentation_ratio=used_memory_rss/used_memory,这也是一个非常需要关心的指标。如果是redis4.0之前的版本,这个问题除了重启也没什么很好的优化办法。而redis4.0有一个主要特性就是优化内存碎片率问题(Memory de-fragmentation)。在redis.conf配置文件中有介绍即ACTIVE DEFRAGMENTATION:碎片整理允许Redis压缩内存空间,从而回收内存。这个特性默认是关闭的,可以通过命令CONFIG SET activedefrag yes热启动这个特性。

  • 当这个值大于1时,表示分配的内存超过实际使用的内存,数值越大,碎片率越严重。
  • 当这个值小于1时,表示发生了swap,即可用内存不够。

另外需要注意的是,当内存使用量(used_memory)很小的时候,这个值参考价值不大。所以,建议used_memory至少1G以上才考虑对内存碎片率进行监控。

缓存命中率

keyspace_misses/keyspace_hits这两个指标用来统计缓存的命令率,keyspace_misses指未命中次数,keyspace_hits表示命中次数。keyspace_hits/(keyspace_hits+keyspace_misses)就是缓存命中率。视情况而定,建议0.9以上,即缓存命中率要超过90%。如果缓存命中率过低,那么要排查对缓存的用法是否有问题!

OPS

instantaneous_ops_per_sec这个指标表示缓存的OPS,如果业务比较平稳,那么这个值也不会波动很大,不过国内的业务比较特性,如果不是全球化的产品,夜间是基本上没有什么访问量的,所以这个字段的监控要结合自己的具体业务,不同时间段波动范围可能有所不同。

持久化

rdb_last_bgsave_status/aof_last_bgrewrite_status,即最近一次或者说最后一次RDB/AOF持久化是否有问题,这两个值都应该是”ok”。

另外,由于redis持久化时会fork子进程,且fork是一个完全阻塞的过程,所以可以监控fork耗时即latest_fork_usec,单位是微妙,如果这个值比较大会影响业务,甚至出现timeout。

失效KEY

如果把Redis当缓存使用,那么建议所有的key都设置了expire属性,通过命令src/redis-cli info Keyspace得到每个db中key的数量和设置了expire属性的key的属性,且expires需要等于keys:

# Keyspace 
db0:keys=30,expires=30,avg_ttl=0 
db0:keys=23,expires=22,avg_ttl=0 

慢日志

通过命令slowlog get得到Redis执行的slowlog集合,理想情况下,slowlog集合应该为空,即没有任何慢日志,不过,有时候由于网络波动等原因造成set key value这种命令执行也需要几毫秒,在监控的时候我们需要注意,而不能看到slowlog就想着去优化,简单的set/get可能也会出现在slowlog中。

使用过Redis,我竟然还不知道Rdb

使用过Redis,那就先说说使用过那些场景吧

字符串缓存

//举例

$redis->set();

$redis->get();

$redis->hset();

$redis->hget();

队列

//举例

$redis->rpush();

$redis->lpop();

$redis->lrange();

发布订阅

//举例

$redis->publish();

$redis->subscribe();

计数器

//举例

$redis->set();

$redis->incr();

排行榜

//举例

$redis->zadd();

$redis->zrevrange();

$redis->zrange();

集合间操作

//举例

$redis->sadd();

$redis->spop();

$redis->sinter();

$redis->sunion();

$redis->sdiff();

悲观锁

解释:悲观锁(Pessimistic Lock), 顾名思义,就是很悲观。

每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁。

场景:如果项目中使用了缓存且对缓存设置了超时时间。

当并发量比较大的时候,如果没有锁机制,那么缓存过期的瞬间,

大量并发请求会穿透缓存直接查询数据库,造成雪崩效应。

/**

 * 获取锁

 * @param  String  $key    锁标识

 * @param  Int     $expire 锁过期时间

 * @return Boolean

 */

public function lock($key = '', $expire = 5) {

    $is_lock = $this->_redis->setnx($key, time()+$expire);

    //不能获取锁

    if(!$is_lock){

        //判断锁是否过期

        $lock_time = $this->_redis->get($key);

        //锁已过期,删除锁,重新获取

        if (time() > $lock_time) {

            unlock($key);

            $is_lock = $this->_redis->setnx($key, time() + $expire);

        }

    }

    return $is_lock? true : false;

}

/**

 * 释放锁

 * @param  String  $key 锁标识

 * @return Boolean

 */

public function unlock($key = ''){

    return $this->_redis->del($key);

}

// 定义锁标识

$key = 'test_lock';

// 获取锁

$is_lock = lock($key, 10);

if ($is_lock) {

    echo 'get lock success<br>';

    echo 'do sth..<br>';

    sleep(5);

    echo 'success<br>';

    unlock($key);

} else { //获取锁失败

    echo 'request too frequently<br>';

}

乐观锁

解释:乐观锁(Optimistic Lock), 顾名思义,就是很乐观。

每次去拿数据的时候都认为别人不会修改,所以不会上锁。

watch命令会监视给定的key,当exec时候如果监视的key从调用watch后发生过变化,则整个事务会失败。

也可以调用watch多次监视多个key。这样就可以对指定的key加乐观锁了。

注意watch的key是对整个连接有效的,事务也一样。

如果连接断开,监视和事务都会被自动清除。

当然了exec,discard,unwatch命令都会清除连接中的所有监视。

$strKey = 'test_age';

$redis->set($strKey,10);

$age = $redis->get($strKey);

echo "---- Current Age:{$age} ---- <br/><br/>";

$redis->watch($strKey);

// 开启事务

$redis->multi();

//在这个时候新开了一个新会话执行

$redis->set($strKey,30);  //新会话

echo "---- Current Age:{$age} ---- <br/><br/>"; //30

$redis->set($strKey,20);

$redis->exec();

$age = $redis->get($strKey);

echo "---- Current Age:{$age} ---- <br/><br/>"; //30

//当exec时候如果监视的key从调用watch后发生过变化,则整个事务会失败

上面的一些场景,咱们大部分都使用过,却还没有提及到Rdb文件。

“对吧,使用过Redis,却不知道Rdb文件,你中枪了吗?”

Rdb文件是什么,它是干什么的

Redis 作为互联网产品开发中不可缺少的常备武器,它性能高、数据结构丰富、简单易用,但同时也是因为太容易用了,不管什么数据、不管这数据有多大、不管数据有多少,通通塞进去,最后导致的问题就是 Redis 内存使用持续上升,但是又不知道里面的数据是不是有用,是否可以拆分和清理,最可怕的是服务器发生宕机后,Redis 数据库里的所有数据将会全部丢失。

比如当内存上升,性能慢时,我们进行性能调优的时候,我们想知道:

  • 哪些Key占用了大量的内存?

  • 想知道每个Key的占用空间?

  • 想知道占用空间大的Key都存了啥?

  • 想知道占用空间大的Key的重要性,当性能慢的时候是否可以马上删除?

  • 更想知道这些Key是哪个业务方,哪个开发创建的?这样就可以找他沟通了。

尝试解决问题的思路

1、先通过 keys * 命令,拿到所有的 key,然后根据 key 再获取所有的内容。

  • 优点:可以不使用 Redis 机器的硬盘,直接网络传输。

  • 缺点:如果 key 数据特别多,keys 命令可能会直接导致 Redis 卡住,从而影响业务使用 或 对 Redis 请求太多次,资源消耗多,遍历数据太慢。

2、开启 aof,通过 aof 文件获取所有的数据。

  • 优点:无需影响 Redis 服务,完全离线操作,足够安全。

  • 缺点:有一些 Redis 实例写入频繁,不适合开启 aof,普适性不强;aof 文件有可能特别大,传输、解析起来太慢,效率低。

3、使用 bgsave,获取 rdb 文件,解析后获取数据。

  • 优点:机制成熟,可靠性好;文件相对小,传输、解析效率高。

  • 缺点:bgsave 虽然会 fork 子进程,但还是有可能导致主进程卡住一段时间,对业务有产生影响的风险。

综合评估后,决定采用低峰期在从节点做 bgsave 获取 rdb 文件,相对安全可靠,也可以覆盖所有业务的 Redis 集群。

也就是说每个实例每天在低峰期自动生成一个 .rdb 文件,即使报表数据有一天的延迟也是可以接受的。

“哦,原来.rdb文件是磁盘的缓存文件,那么如何开启持久化呢?”

下面简单的介绍下,Redis 的持久化

Redis 支持两种方式的持久化,一种是RDB方式,一种是AOF方式。

RDB 是 Redis 用来进行持久化的一种方式,是把当前内存中的数据集,快照写入磁盘。

RDB – 自动

RDB(Redis DataBase)方式是通过快照完成的,当符合一定条件时Redis会自动将内存中的所有数据进行快照,并且存储到硬盘上,RDB是Redis的默认持久化方式。

vim /usr/local/redis/conf/redis.conf

save 900 1    #15分钟内有至少1个键被更改

save 300 10   #5分钟内至少有10个键被更改

save 60 1000  #1分钟内至少有10000个键被更改

#以上条件都是或的关系,当满足其一就会进行快照。

dbfilename "dump.rdb"       #持久化文件名称

dir "/data/dbs/redis/6381"  #持久化数据文件存放的路径

#配置文件修改后,需要重启redis服务。

还可以通过命令行的方式进行配置:

CONFIG GET save    #查看redis持久化配置

CONFIG SET save "100 20" #修改redis持久化配置

#使用命令行的方式配置,即时生效,服务器重启后需要重新配置。

RDB – 手动

  • save

该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。

显然该命令对于内存比较大的实例会造成长时间阻塞,这是致命的缺陷。

  • bgsave

执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。

具体操作是Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段。

AOF

AOF(APPEND ONLY MODE)是通过保存对redis服务端的写命令(如set、sadd、rpush)来记录数据库状态的,即保存你对redis数据库的写操作。

配置日志文件如下:

vim /usr/local/redis/conf/redis.conf

dir "/data/dbs/redis/6381"           #AOF文件存放目录

appendonly yes                       #开启AOF持久化,默认关闭

appendfilename "appendonly.aof"      #AOF文件名称(默认)

appendfsync no                       #AOF持久化策略

auto-aof-rewrite-percentage 100      #触发AOF文件重写的条件(默认)

auto-aof-rewrite-min-size 64mb       #触发AOF文件重写的条件(默认)

#上面的每个参数,可以找资料了解下,不做多解释了。

RDB 与 AOF 的优缺点,见上面的即可。

至此,我们了解了 Redis 持久化的一些配置,里面的细节建议查询相关资料进行研究。

接下来继续,通过上一步我们拿到了 rdb 文件,就相当于拿到了Redis实例的数据。

  1. 解析 rdb 文件,获取key和value的值。

  2. 根据相应的数据结构及内容,估算内存消耗。

  3. 统计并生成报表。

分析工具

  • 雪球 rdr:
    https://github.com/xueqiu/rdr

  • redis-rdb-tools:
    https://github.com/sripathikrishnan/redis-rdb-tools

小结

  1. 讲解了工作中常用的 redis 使用场景。

  2. 讲解了 redis 持久化的两个方式(RDB、AOF)。

  3. 推荐了两个分析rdb的工具。

通过对 redis 的使用 到 了解到服务器上如何对redis数据做持久化快照,再到如何利用工具进行分析rdb文件,最后通过分析后的数据,可以反过来对 redis 的使用提出一些建议。

其他知识点也是这样,我们不能只停留在方法的简单调用,就觉得理解了这门技术!

联想

其实上面分析出来的数据,是不可能定位到这个key是哪个业务方的,哪个开发创建的,是否重要等等,那我们应该怎么做呢?

1.制定开发团队的Redis Key的使用规范,通过key的命名可以得到:

  • 属于什么业务(加域名表示)

  • 属于什么数据类型(加数据类型标示)

  • 是否设置过期时间

2.统一平台进行Redis Key的申请,只有申请了才能进行使用:

  • 填写申请人

  • 填写申请时间

  • 填写申请业务方

  • 填写数据类型

  • 填写Key的重要性

  • 填写Key是否存在过期时间

  • 根据填写项生成规范的key名称

  • …(等等需要标记的)

3.上面我们已经能分析出某个redis实例rdb文件的内容,通过分析出来的内容 与 统一平台申请的数据,进行整合,分析key的合格率、内存使用量、不同数据类型的分布、内存占用量Top 100的值 等等。

4.我们可以通过运维了解到,每个服务器与实例之间的配置关系,就可以了解到某台服务器(N个实例)上的 key的合格率、内存使用量、不同数据类型的分布、内存占用量Top 100的值等等。

这样,在后台系统中就可以看到哪台服务器,哪个实例的使用情况,解决了Redis滥用并无法进行监控的问题。

SeeRedis,一款实用的redis管理工具

SeeRedis下载安装地址:https://github.com/void9main

SeeRedis是一款基于php开发的web端的redis管理工具,其无论是从界面还是具体的功能,都展现出了简洁快速的设计思维,尤其非常是和快节奏的程序开发,能够对redis实现管理和对数据实现有效的查询与利用。
下面简单的介绍一下SeeRedis的使用方式
首先SeeRedis有一个简单的清爽的登陆页面

未分类

利用这种ip+端口号的登陆方式seeredis可以在你自己的电脑上面快速的访问任何线上或者线下的redis数据库
其次在登陆之后SeeRedis也能快速将整个redis服务器的具体信息展现出来

未分类

而且在侧边栏也可以清晰的看到整个数据的db详细和每个db中key的数量
在点击进入到db中之后能够清楚的看到分页下的每个key的排列(如下)和每个键的详情,包括:键的名称,键的类型,键的生存时间。

未分类

而在这个页面也可以实现对redis的复杂的操作,如:key键的模糊搜索,删除键值,查看键值,为键值设置生存时间,甚至于清空整个db等等

未分类

一篇文章入门Redis

Redis是目前最流行的NoSQL数据库,最重要的是它是运行在内存上的数据库。所以几乎所有高并发需求的产品都会考虑使用Redis作为数据库缓存。

不同于MongoDB的以硬盘存储为主、内存为辅,Redis是真·内存存储,即所有数据都存在内存中,只是偶尔间歇性的保存到硬盘上备份。

Redis特点:

  • NoSQL:采用类似JSON的Key-Value键值对存储。
  • 存储在内存中:几乎是计算机存储的最高读写速度(除CPU寄存器外)。
  • 可持久化:定期可以把内存中的数据备份到硬盘。
  • 轻量化:整个软件才1M,内存占用极少。
  • 单机多实例:同机器可以给多个应用配置多个Redis数据库,因为资源占用极少。

理解Redis

原子性

Redis处理高并发最强的就是其原子性。完全基于单线程,抛弃多进程、多线程等逻辑。

什么是“原子性”?参考:深入学习RedisAPI的原子性分析

原子性是数据库的事务中的特性。在数据库事务的情景下,原子性指的是:
一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。
对于Redis而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。

Redis vs. JSON

作为NoSQL初学者,我觉得所有的NoSQL不过是一个更复杂的JSON文件而已。

但是只是一个文本文件的JSON面临IO堵塞、文本解析等很大屏障,速度决定了它的天花板。即使是放在Ram Disk内存盘上的JSON,也解决不了高并发的问题
而Redis不光用了内存,还用了原子性逻辑来加速运行,同时还加入了一系列的备份、恢复、分布式多机器运行的功能。

所以真的没法再说和JSON一样了。

Redis vs. MongoDB

MongoDB是基于Documentation的,

….

Redis vs. Memcached

….

安装

参考:How To Install and Secure Redis on Ubuntu 18.04

整个软件大约1M左右。

Mac的Homebrew安装:

brew install redis

Ubuntu安装:

sudo apt install redis-server

Docker安装:
因为Redis实在是太轻量了,而且原生支持多实例运行,配置也是单文件配置,所以不太需要专门用docker来做隔离。如果真有docker需要的话,也不会难。参考hub.docker.com。

编译安装:
如果需要自己编译的话,就到Redis的下载最新的release,一般用tar.gz格式。
目前最新的稳定版5.0。中文官网地址:http://www.redis.cn/

wget http://download.redis.io/releases/redis-5.0.0.tar.gz
tar -xvzf redis-*.tar.gz
cd redis-*

make
make test
sudo make install

配置

Redis配置很好理解,只需一个redis.conf配置文件。

找到文件后,一般需要修改的地方只有以下几点:

# 绑定主机IP和端口,端口是redis默认
bind 127.0.0.1
port 6379

# (推荐)以守护进程方式运行,这样就不会进入命令行”前台堵塞模式“
daemonize yes

# 数据文件
dbfilename dump.db

# 数据存储的位置,运行前需手动创建文件夹,否则报错
dir /var/lib/redis/

# 日志文件
log /var/log/redis/redis-server.log

# 数据库数量,默认16个数据库
database 16

运行交互

启动redis服务器:

redis-server

# 或指定配置文件启动
redis-server /etc/redis/redis.conf

如果没有在配置中设置daemonize,那么这里就会在前端启动,即堵塞整个shell来运行这个程序。如果已经是守护进程了,那么就会在后台运行,可以用ps aux |grep redis看到。
如果要开机启动,直接在/etc/rc.local中加入启动redis的命令即可,不过这样的还不如设置系统service好管理。关闭服务器的方法就是直接kill掉进程即可:pkill redis-server

客户端:

# 客户端shell,与服务器进行命令交互
redis-cli 

# 关闭客户端
redis-cli shutdown

Redis的主从 “RAID 1”

以上是最简单的单机设置。然而,Redis的主从设置也不难,很简单。

记住:Redis的Master-Slave的结构,实际上只是一种备份关系!而不是数据分散在各地的那种。

Redis的Master-slave架构的作用:

  • 备份:slave从的最主要作用是备份,以弥补单机内存数据不稳固的缺点。
  • 读、写分离:写入是很耗时间的,而读很快。那么如果读和写分离,会加速很多。

未分类

设置方法:

  • 主从两台机器上,都安装了redis-server
  • 两台机子上都有一个配置文件redis.conf
  • 两台机子上的redis.conf中,互相指明自己是主还是从,主的IP是什么,以及权限等相关设置。
  • 两台机子同时启动redis-server

这个设置方法是最简单的主从设置,甚至有点像ssh-tunnel或frp内网穿越等设置。都是基于一个配置文件就能完成自动连接的。

主从可以在同一台机器(但是没有什么意义),只是注意端口号不要冲突。如果不是同一台机器,那么端口号就无所谓了。

Redis集群 “RAID 0”

如果说主从架构是硬盘组合的RAID 1模式,那么Redis集群就是RAID 0——数据是分布在各个机器上的。

如果只是简单的主从架构,那么主要的压力还是都集中在Master主机上,万百万级别的高并发肯定是扛不住的。所以要用到Redis集群。

Redis集群才是真正的分布式。

集群分为软件层面的和硬件层面的。
Redis在同一台机器可以启动多个服务,也就是在本机可以使用多个Redis数据库服务,这叫软件层面集群(没什么用)。因为一台机器死机,整个集群就没了。所以软件方面的只适合同一台机器给不同应用配置redis数据库,不适合集群。
硬件集群是每台机器上都有redis,用于分担数据。

集群有这几大特点:

  • Slots:代表Redis对数据自动切分(Split)的能力
  • Partition:代表数据的高可用性(Availability)

集群的槽 Slots

Redis怎么把全部数据分配个集群的每台机器?
它会先把数据分为16,384个slots槽,然后把这些槽平均分配个每台机器。比如机器A分了0-1000的槽用来存数据,机器B分了1001-2000的槽。。。每台机器都会知道自己会负责哪些槽。

如果一台机器接收到不是自己负责的slot的数据,就会把请求“转发”给该负责的机器。这个就叫转向 (Redirection)。

怎么确定新来的数据在哪台机器上写入呢?

Redis利用了Hash Table数据结构的基本原理,即通过一个Hash function把key映射为一个固定的整数number。通过number % 16384而得到一个固定的index整数值,根据这个index就能直到它所属的slot在哪个“负责人”位置了。

集群的分区 Partition

如果有partition分区,那么及时有些机器突然不可用、断线,集群也可以继续完成请求任务。

Redis于Python交互

Python需要安装redis包:pip install redis

基础交互代码test.py

import redis

Redis 简述

Redis 简介

Redis 是完全开源免费的,遵守 BSD 协议,是一个高性能的 key-value 数据库。

Redis 与其他 key – value 缓存产品有以下三个特点:

  • Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。
  • Redis不仅仅支持简单的 key-value 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。
  • Redis支持数据的备份,即 master-slave 模式的数据备份。

Redis 优势

  • 性能极高 – Redis 能读的速度是 110000 次/s,写的速度是 81000 次/s 。
  • 丰富的数据类型 – Redis 支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
  • 原子 – Redis 的所有操作都是原子性的,同时 Redis 还支持对几个操作全并后的原子性执行。
  • 丰富的特性 – Redis 还支持 publish/subscribe, 通知, key 过期等等特性。

Redis 与其他 key-value 存储有什么不同?

  • Redis 有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。Redis 的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。
  • Redis 运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,应为数据量不能大于硬件内存。在内存数据库方面的另一个优点是, 相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样 Redis 可以做很多内部复杂性很强的事情。 同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。

redis 的安装

下载安装包,博主采用的是 redis-3.0.5.tar.gz,可以在这里下载.

http://download.csdn.net/detail/u013256816/9487005

安装步骤:

tar -zvxf redis-3.0.5.tar.gz
cd redis-3.0.5
make

redis 服务端开启

./redis-server

开启之后如下图所示:

未分类

这种方式是启动的 redis 使用的是默认配置,也可以通过启动参数考古 redis 使用指定配置文件:

./redis-server [redis.conf 的路径]

如果修改 redis.conf,采用 redis 默认的 redis.conf 文件, redis 默认只能通过 127.0.0.1:6379 这个地址访问,这样就只能在本机上操作了,如果想要远程操作就不可行了。这里需要修改 redis.conf 这个配置文件,在配置文件中添加相应的 ip 地址,这里假如添加 ip 地址:10.10.195.112,只需在 redis.conf 这个配置文件中添加:

bind 10.10.195.112
bind 127.0.0.1

这样就可以远程访问 redis 服务器了(先配置,后开启 redis 服务)。

redis 客户端连接

  • ./redis-cli (本地)
  • ./redis-cli -h [host] -p [port] -a [password] (远程服务器)

Jedis 开发

使用 java 开发 redis,博主使用的是 jedis,需要引入 jedis 的 jar,可以在这里下载。还需要 commons-pool.jar 包

http://download.csdn.net/detail/u013256816/9487008

未分类

连接

Jedis jedis = new Jedis(“10.10.195.112”);
System.out.println(jedis.ping());

输出:PONG

操作字符串

jedis.set(“name”, “zzh”);
System.out.println(jedis.get(“name”));

输出:zzh

操作列表

jedis.lpush(“nblist”, “jj”);
jedis.lpush(“nblist”, “jj”);
jedis.lpush(“nblist”, “yy”);
jedis.lpush(“nblist”, “qq”);
List<String> list = jedis.lrange(“nblist”, 0, -1);
int length = list.size();
for(int i=0;i<length;i++)
{
    System.out.println(list.get(i));
}

输出:

qq
yy
jj
jj

获取 redis 中所有的键

Set<String> set = jedis.keys(“*”);
for(String key : set)
{
    System.out.println(key);
}

输出:

nam
name
tutorial
list
tutorial-list
nblist
tutoriallist
keyname
user
listt
zsetkey
hash-key

Redis 常用命令

1. 连接操作命令

  • quit:关闭连接(connection)
  • auth:简单密码认证
  • help cmd: 查看 cmd 帮助,例如:help quit

2 持久化

  • save:将数据同步保存到磁盘
  • bgsave:将数据异步保存到磁盘
  • lastsave:返回上次成功将数据保存到磁盘的 Unix 时戳
  • shutdown:将数据同步保存到磁盘,然后关闭服务

3. 远程服务控制

  • info:提供服务器的信息和统计
  • monitor:实时转储收到的请求
  • slaveof:改变复制策略设置
  • config:在运行时配置 Redis 服务器

4. 对 key 操作的命令

  • exists(key):确认一个 key 是否存在
  • del(key):删除一个 key
  • type(key):返回值的类型
  • keys(pattern):返回满足给定 pattern 的所有 key
  • randomkey:随机返回 key 空间的一个
  • keyrename(oldname, newname):重命名 key
  • dbsize:返回当前数据库中 key 的数目
  • expire:设定一个 key 的活动时间(s)
  • ttl:获得一个 key 的活动时间
  • select(index):按索引查询
  • move(key, dbindex):移动当前数据库中的 key 到 dbindex 数据库
  • flushdb:删除当前选择数据库中的所有 key
  • flushall:删除所有数据库中的所有 key

5. String

  • set(key, value):给数据库中名称为 key 的 string 赋予值 value
  • get(key):返回数据库中名称为 key 的 string 的 value
  • getset(key, value):给名称为 key 的 string 赋予上一次的 value
  • mget(key1, key2,…, key N):返回库中多个 string 的 value
  • setnx(key, value):添加 string,名称为 key,值为 value
  • setex(key, time, value):向库中添加 string,设定过期时间 time
  • mset(key N, value N):批量设置多个 string 的值
  • msetnx(key N, value N):如果所有名称为 key i 的 string 都不存在
  • incr(key):名称为 key 的 string 增 1 操作
  • incrby(key, integer):名称为 key 的 string 增加 integer
  • decr(key):名称为 key 的 string 减 1 操作
  • decrby(key, integer):名称为 key 的 string 减少 integer
  • append(key, value):名称为 key 的 string 的值附加 value
  • substr(key, start, end):返回名称为 key 的 string 的 value 的子串

6. List

  • rpush(key, value):在名称为 key 的 list 尾添加一个值为 value 的元素
  • lpush(key, value):在名称为 key 的 list 头添加一个值为 value 的 元素
  • llen(key):返回名称为 key 的 list 的长度
  • lrange(key, start, end):返回名称为 key 的 list 中 start 至 end 之间的元素
  • ltrim(key, start, end):截取名称为 key 的 list
  • lindex(key, index):返回名称为 key 的 list 中 index 位置的元素
  • lset(key, index, value):给名称为 key 的 list 中 index 位置的元素赋值
  • lrem(key, count, value):删除 count 个 key 的 list 中值为 value 的元素
  • lpop(key):返回并删除名称为 key 的 list 中的首元素
  • rpop(key):返回并删除名称为 key 的 list 中的尾元素
  • blpop(key1, key2,… key N, timeout):lpop 命令的 block 版本。
  • brpop(key1, key2,… key N, timeout):rpop 的 block 版本。
  • rpoplpush(srckey, dstkey):返回并删除名称为 srckey 的 list 的尾元素,并将该元素添加到名称为 dstkey 的 list 的头部

7. Set

  • sadd(key, member):向名称为 key 的 set 中添加元素 member
  • srem(key, member) :删除名称为 key 的 set 中的元素 member
  • spop(key) :随机返回并删除名称为 key 的 set 中一个元素
  • smove(srckey, dstkey, member) :移到集合元素
  • scard(key) :返回名称为 key 的 set 的基数
  • sismember(key, member) :member 是否是名称为 key 的 set 的元素
  • sinter(key1, key2,…key N) :求交集
  • sinterstore(dstkey, (keys)) :求交集并将交集保存到 dstkey 的集合
  • sunion(key1, (keys)) :求并集
  • sunionstore(dstkey, (keys)) :求并集并将并集保存到 dstkey 的集合
  • sdiff(key1, (keys)) :求差集
  • sdiffstore(dstkey, (keys)) :求差集并将差集保存到 dstkey 的集合
  • smembers(key) :返回名称为 key 的 set 的所有元素
  • srandmember(key) :随机返回名称为 key 的 set 的一个元素

8. Hash

  • hset(key, field, value):向名称为 key 的 hash 中添加元素 field
  • hget(key, field):返回名称为 key 的 hash 中 field 对应的 value
  • hmget(key, (fields)):返回名称为 key 的 hash 中 field i 对应的 value
  • hmset(key, (fields)):向名称为 key 的 hash 中添加元素 field
  • hincrby(key, field, integer):将名称为 key 的 hash 中 field 的 value 增加 integer
  • hexists(key, field):名称为 key 的 hash 中是否存在键为 field 的域
  • hdel(key, field):删除名称为 key 的 hash 中键为 field 的域
  • hlen(key):返回名称为 key 的 hash 中元素个数
  • hkeys(key):返回名称为 key 的 hash 中所有键
  • hvals(key):返回名称为 key 的 hash 中所有键对应的 value
  • hgetall(key):返回名称为 key 的 hash 中所有的键(field)及其对应的 value

使用 Redis 实现分布式锁的正确姿势

锁是我们在实现大多数系统时绕不过的话题。一旦有竞态条件出现,任何不经保护的操作,都可能带来问题。而现代系统大多为分布式系统,这就引入了分布式锁,要求具有在分布各处的服务上保护资源的能力。而实现分布式锁,目前大多有以下 3 种方式:

  1. 使用数据库实现
  2. 使用 Redis 等缓存系统实现
  3. 使用 ZooKeeper 等分布式协调系统实现

其中 Redis 简便灵活,高可用分布式,且支持持久化。本文即介绍基于 Redis 实现分布式锁。

SETNX 语义

使用 Redis 实现分布式锁,根本原理是 SETNX 指令。其语义如下:

SETNX key value

如果 key 不存在,则设置 key 值为 value(同 set);如果 key 已经存在,则不执行赋值操作。并使用不同的返回值标识。官方文档

还可以通过 SET 命令的 NX 选项使用:

SET key value [expiration EX seconds|PX milliseconds] [NX|XX]

NX – 仅在 key 不存在时执行赋值操作。官方文档
而如下文所述,通过 SET 的 NX 选项使用,可同时使用其它选项,如 EX/PX 设置超时时间,是更好的方式。

SETNX 实现分布式锁

下面我们对比下几种具体实现方式。

方案1:SETNX + Delete

伪代码如下:

setnx lock_a random_value
// do sth
delete lock_a

此实现方式的问题在于:一旦服务获取锁之后,因某种原因挂掉,则锁一直无法自动释放。从而导致死锁。

方案2:SETNX + SETEX

伪代码如下:

setnx lock_a random_value
setex lock_a 10 random_value // 10s超时
// do sth
delete lock_a

按需设置超时时间。此方案解决了方案 1 死锁的问题,但同时引入了新的死锁问题:如果 SETNX 之后、SETEX 之前服务挂掉,会陷入死锁。根本原因为 SETNX/SETEX 分为了两个步骤,非原子操作。

方案3:SET NX PX

伪代码如下:

SET lock_a random_value NX PX 10000 // 10s超时
// do sth
delete lock_a

此方案通过 SET 的 NX/PX 选项,将加锁、设置超时两个步骤合并为一个原子操作,从而解决方案 1、2 的问题。( PX 与 EX 选项的语义相同,差异仅在单位。)此方案目前大多数SDK、Redis 部署方案都支持,因此是推荐使用的方式。

但此方案也有如下问题:

  • 如果锁被错误的释放(如超时),或被错误的抢占,或因redis问题等导致锁丢失,无法很快的感知到。

方案4:SET Key RandomValue NX PX

方案 4 在 3 的基础上,增加对 value 的检查,只解除自己加的锁。类似于 CAS,不过是 compare-and-delete。此方案 Redis 原生命令不支持,为保证原子性,需要通过 Lua 脚本实现。

伪代码如下:

SET lock_a random_value NX PX 10000
// do sth
eval "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 lock_a random_value

此方案更严谨:即使因为某些异常导致锁被错误的抢占,也能部分保证锁的正确释放。并且在释放锁时能检测到锁是否被错误抢占、错误释放,从而进行特殊处理。

注意事项

超时时间

从上述描述可看出,超时时间是一个比较重要的变量:

  • 超时时间不能太短,否则在任务执行完成前就自动释放了锁,导致资源暴露在锁保护之外。
  • 超时时间不能太长,否则会导致意外死锁后长时间的等待。除非人为接入处理。

因此建议是根据任务内容,合理衡量超时时间,将超时时间设置为任务内容的几倍即可。如果实在无法确定而又要求比较严格,可以采用定期 SETEX/Expire 更新超时时间实现。

重试

如果拿不到锁,建议根据任务性质、业务形式进行轮询等待。等待次数需要参考任务执行时间。

与 Redis 事务的比较

SETNX 使用更为灵活方案。Multi/Exec 的事务实现形式更为复杂。且部分 Redis 集群方案 ( 如 Codis ),不支持 Multi/Exec 事务。

Golang Demo

基于 Redigo 简单实例代码如下:

package main

import (
    "fmt"
    "sync"
    "time"

    "github.com/garyburd/redigo/redis"
)

func getLock(redisAddr, lockKey string, ex uint, retry int) error {
    if retry <= 0 {
        retry = 10
    }
    conn, err := redis.DialTimeout("tcp", redisAddr, time.Minute, time.Minute, time.Minute)
    if err != nil {
        fmt.Println("conn to redis failed, err:%v", err)
        return err
    }
    defer conn.Close()
    ts := time.Now() // as random value
    for i := 1; i <= retry; i++ {
        if i > 1 { // sleep if not first time
            time.Sleep(time.Second)
        }
        v, err := conn.Do("SET", lockKey, ts, "EX", retry, "NX")
        if err == nil {
            if v == nil {
                fmt.Println("get lock failed, retry times:", i)
            } else {
                fmt.Println("get lock success")
                break
            }
        } else {
            fmt.Println("get lock failed with err:", err)
        }
        if i >= retry {
            err = fmt.Errorf("get lock failed with max retry times.")
            return err
        }
    }
    return nil
}

func unLock(redisAddr, lockKey string) error {
    conn, err := redis.DialTimeout("tcp", redisAddr, time.Minute, time.Minute, time.Minute)
    if err != nil {
        fmt.Println("conn to redis failed, err:%v", err)
        return err
    }
    defer conn.Close()
    v, err := redis.Bool(conn.Do("DEL", lockKey))
    if err == nil {
        if v {
            fmt.Println("unLock success")
        } else {
            fmt.Println("unLock failed")
            return fmt.Errorf("unLock failed")
        }
    } else {
        fmt.Println("unLock failed, err:", err)
        return err
    }
    return nil
}

const (
    RedisAddr = "127.0.0.1:3000"
)

func main() {
    var wg sync.WaitGroup

    key := "lock_demo"

    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            time.Sleep(time.Second)
            // getLock
            err := getLock(RedisAddr, key, 10, 10)
            if err != nil {
                fmt.Println(fmt.Sprintf("worker[%d] get lock failed:%v", id, err))
                return
            }
            // sleep for random
            for j := 0; j < 5; j++ {
                time.Sleep(time.Second)
                fmt.Println(fmt.Sprintf("worker[%d] hold lock for %ds", id, j+1))
            }
            // unLock
            err = unLock(RedisAddr, key)
            if err != nil {
                fmt.Println(fmt.Sprintf("worker[%d] unlock failed:%v", id, err))
            }
            fmt.Println(fmt.Sprintf("worker[%d] done", id))
        }(i)
    }

    wg.Wait()
    fmt.Println("demo is done!")
}

参考

SETEX 命令
SET 命令
Distributed locks with Redis

redis 禁用O(n) 复杂度方法

某公司技术部发生2起本年度PO级特大事故,造成公司资金损失400万,原因如下:

由于工程师直接操作上线redis,执行:

keys * wxdb(此处省略)cf8*

这样的命令,导致redis锁住,导致CPU飙升,引起所有支付链路卡住,等十几秒结束后,所有的请求流量全部挤压到了rds数据库中,使数据库产生了雪崩效应,发生了数据库宕机事件。

redis开发规范中有一条铁律如下所示:

线上Redis禁止使用Keys正则匹配操作!

线上执行正则匹配操作,引起缓存雪崩,最终数据库宕机的原因:

  1. redis是单线程的,其所有操作都是原子的,不会因并发产生数据异常;

  2. 使用高耗时的Redis命令是很危险的,会占用唯一的一个线程的大量处理时间,导致所有的请求都被拖慢。(例如时间复杂度为O(N)的KEYS命令,严格禁止在生产环境中使用);

有上面两句作铺垫,原因就显而易见了!

运维人员进行keys *操作,该操作比较耗时,又因为redis是单线程的,所以redis被锁住;

此时QPS比较高,又来了几万个对redis的读写请求,因为redis被锁住,所以全部Hang在那;

因为太多线程Hang在那,CPU严重飙升,造成redis所在的服务器宕机;

所有的线程在redis那取不到数据,一瞬间全去数据库取数据,数据库就宕机了;

需要注意的是,同样危险的命令不仅有keys *,还有以下几组:

FLUSHALL  清空整个 Redis 服务器的数据(删除所有数据库的所有 key )。
FLUSHDB   清空当前数据库中的所有 key。
CONFIG SET  调整 Redis 服务器的配置(configuration)而无须重启。

因此,一个合格的redis运维或者开发,应该懂得如何禁用上面的命令。所以我一直觉得出现新闻中那种情况的原因,一般是人员的水平问题。

如何禁用redis命令

就是在redis.conf中,在SECURITY这一项中,我们新增以下命令:

rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command KEYS ""
rename-command CONFIG ""

另外,对于FLUSHALL命令,需要设置配置文件中appendonly no,否则服务器是无法启动。

注意了,上面的这些命令可能有遗漏,大家可以查官方文档。除了Flushdb这类和redis安全隐患有关的命令意外,但凡发现时间复杂度为O(N)的命令,都要慎重,不要在生产上随便使用。例如hgetall、lrange、smembers、zrange、sinter等命令,它们并非不能使用,但这些命令的时间复杂度都为O(N),使用这些命令需要明确N的值,否则也会出现缓存宕机。

改良建议

业内建议使用scan命令来改良keys和SMEMBERS命令:

Redis2.8版本以后有了一个新命令scan,可以用来分批次扫描redis记录,这样肯定会导致整个查询消耗的总时间变大,但不会影响redis服务卡顿,影响服务使用。

具体使用,大家详情可以自己查阅下面这份文档:

http://doc.redisfans.com/key/scan.html

代码实例:

# php redis 扩展
$redis = new Redis();
$redis->connect(config('database.redis.default.host'), config('database.redis.default.port'));
$redis->auth(config('database.redis.default.password'));
$redis->select(config('database.redis.default.database'));
$redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY);
$it = NULL;
while($arr_keys = $redis->hScan($Key, $it)) {
    foreach($arr_keys as $str_field => $str_value) {
          //TODO  
    }
}

# laravel  predis 驱动

$it = NULL;
while($arr_keys = Redis::hScan($Key, $it)) {
    foreach($arr_keys[1] as $str_field => $str_value) {
        //TODO
        $it = $arr_keys[0];
        if($arr_keys[0] == 0){
            break;
        }
    }
}