工作,学习,生活,这里将会有一些记录. 备用域名:http://meisw.51099.com 注册 | 登陆
浏览模式: 标准 | 列表分类:redis

redis从db0迁移数据至db1

 

从redis db0中迁移’employeeInfo_'为前缀的key到db1中,db0删除该key

redis-cli -a redis123 -n 0 keys 'employeeInfo_*' | xargs -I '{}' redis-cli -a redis123 -n 0 move '{}' 1 
  • 1

redis-cli -n 0 keys ‘*’ 表示获取db0中所有的keys
-a redis123 表示密码redis123
xargs -I ‘{}’ 表示将上步操作的结果作为参数,保存在’{}'中, 在执行移动db1命令: redis-cli -a redis123 -n 0 move ‘{}’ 1

ps:若是Redis带密码,前后都要带上-a 密码

具体参考redis之间数据迁移 - 开始的第一步 - 博客园

redis-monitor

 

  1. 首先安装python库

    pip install redis-monitor

  2. 初始化配置和数据库

    redis-monitor init

  3. 启动 webserver

    redis-monitor start

然后访问 127.0.0.1:9527

https://github.com/NetEaseGame/redis-monitor

redigo的redis.Pool 配置参数调优

 

配置场景

再来看下主要参数

  • MaxIdle 
    • 表示连接池空闲连接列表的长度限制
    • 空闲列表是一个栈式的结构,先进后出
  • MaxActive 
    • 表示连接池中最大连接数限制
    • 主要考虑到服务端支持的连接数上限,以及应用之间”瓜分”连接数
  • IdleTimeout 
    • 空闲连接的超时设置,一旦超时,将会从空闲列表中摘除
    • 该超时时间时间应该小于服务端的连接超时设置

区分两种使用场景:

  1. 高频调用的场景,需要尽量压榨redis的性能: 
    • 调高MaxIdle的大小,该数目小于maxActive,由于作为一个缓冲区一样的存在,扩大缓冲区自然没有问题
    • 调高MaxActive,考虑到服务端的支持上限,尽量调高
    • IdleTimeout由于是高频使用场景,设置短一点也无所谓,需要注意的一点是MaxIdle设置的长了,队列中的过期连接可能会增多,这个时候IdleTimeout也要相应变化
  2. 低频调用的场景,调用量远未达到redis的负载,稳定性为重: 
    • MaxIdle可以设置的小一些
    • IdleTimeout相应地设置小一些
    • MaxActive随意,够用就好,容易检测到异常
  3. https://blog.csdn.net/xiaohu50/article/details/51606349

Redis 5种主要数据类型和命令

 https://www.cnblogs.com/mingtianct/p/6291593.html

https://www.cnblogs.com/youxin/p/3833817.html

Redis基础、高级特性与性能调优

 本文将从Redis的基本特性入手,通过讲述Redis的数据结构和主要命令对Redis的基本能力进行直观介绍。之后概览Redis提供的高级能力,并在部署、维护、性能调优等多个方面进行更深入的介绍和指导。

本文适合使用Redis的普通开发人员,以及对Redis进行选型、架构设计和性能调优的架构设计人员。

目录

  • 概述
  • Redis的数据结构和相关常用命令
  • 数据持久化
  • 内存管理与数据淘汰机制
  • Pipelining
  • 事务与Scripting
  • Redis性能调优
  • 主从复制与集群分片
  • Redis Java客户端的选择

概述

Redis是一个开源的,基于内存的结构化数据存储媒介,可以作为数据库、缓存服务或消息服务使用。
Redis支持多种数据结构,包括字符串、哈希表、链表、集合、有序集合、位图、Hyperloglogs等。
Redis具备LRU淘汰、事务实现、以及不同级别的硬盘持久化等能力,并且支持副本集和通过Redis Sentinel实现的高可用方案,同时还支持通过Redis Cluster实现的数据自动分片能力。

Redis的主要功能都基于单线程模型实现,也就是说Redis使用一个线程来服务所有的客户端请求,同时Redis采用了非阻塞式IO,并精细地优化各种命令的算法时间复杂度,这些信息意味着:

  • Redis是线程安全的(因为只有一个线程),其所有操作都是原子的,不会因并发产生数据异常
  • Redis的速度非常快(因为使用非阻塞式IO,且大部分命令的算法时间复杂度都是O(1))
  • 使用高耗时的Redis命令是很危险的,会占用唯一的一个线程的大量处理时间,导致所有的请求都被拖慢。(例如时间复杂度为O(N)的KEYS命令,严格禁止在生产环境中使用)

Redis的数据结构和相关常用命令

本节中将介绍Redis支持的主要数据结构,以及相关的常用Redis命令。本节只对Redis命令进行扼要的介绍,且只列出了较常用的命令。如果想要了解完整的Redis命令集,或了解某个命令的详细使用方法,请参考官方文档:https://redis.io/commands

Key

Redis采用Key-Value型的基本数据结构,任何二进制序列都可以作为Redis的Key使用(例如普通的字符串或一张JPEG图片)
关于Key的一些注意事项:

  • 不要使用过长的Key。例如使用一个1024字节的key就不是一个好主意,不仅会消耗更多的内存,还会导致查找的效率降低
  • Key短到缺失了可读性也是不好的,例如"u1000flw"比起"user:1000:followers"来说,节省了寥寥的存储空间,却引发了可读性和可维护性上的麻烦
  • 最好使用统一的规范来设计Key,比如"object-type:id:attr",以这一规范设计出的Key可能是"user:1000"或"comment:1234:reply-to"
  • Redis允许的最大Key长度是512MB(对Value的长度限制也是512MB)

String

String是Redis的基础数据类型,Redis没有Int、Float、Boolean等数据类型的概念,所有的基本类型在Redis中都以String体现。

与String相关的常用命令:

  • SET:为一个key设置value,可以配合EX/PX参数指定key的有效期,通过NX/XX参数针对key是否存在的情况进行区别操作,时间复杂度O(1)
  • GET:获取某个key对应的value,时间复杂度O(1)
  • GETSET:为一个key设置value,并返回该key的原value,时间复杂度O(1)
  • MSET:为多个key设置value,时间复杂度O(N)
  • MSETNX:同MSET,如果指定的key中有任意一个已存在,则不进行任何操作,时间复杂度O(N)
  • MGET:获取多个key对应的value,时间复杂度O(N)

上文提到过,Redis的基本数据类型只有String,但Redis可以把String作为整型或浮点型数字来使用,主要体现在INCR、DECR类的命令上:

  • INCR:将key对应的value值自增1,并返回自增后的值。只对可以转换为整型的String数据起作用。时间复杂度O(1)
  • INCRBY:将key对应的value值自增指定的整型数值,并返回自增后的值。只对可以转换为整型的String数据起作用。时间复杂度O(1)
  • DECR/DECRBY:同INCR/INCRBY,自增改为自减。

INCR/DECR系列命令要求操作的value类型为String,并可以转换为64位带符号的整型数字,否则会返回错误。
也就是说,进行INCR/DECR系列命令的value,必须在[-2^63 ~ 2^63 - 1]范围内。

前文提到过,Redis采用单线程模型,天然是线程安全的,这使得INCR/DECR命令可以非常便利的实现高并发场景下的精确控制。

例1:库存控制

在高并发场景下实现库存余量的精准校验,确保不出现超卖的情况。

设置库存总量:

SET inv:remain "100"

库存扣减+余量校验:

DECR inv:remain

当DECR命令返回值大于等于0时,说明库存余量校验通过,如果返回小于0的值,则说明库存已耗尽。

假设同时有300个并发请求进行库存扣减,Redis能够确保这300个请求分别得到99到-200的返回值,每个请求得到的返回值都是唯一的,绝对不会找出现两个请求得到一样的返回值的情况。

例2:自增序列生成

实现类似于RDBMS的Sequence功能,生成一系列唯一的序列号

设置序列起始值:

SET sequence "10000"

获取一个序列值:

INCR sequence

直接将返回值作为序列使用即可。

获取一批(如100个)序列值:

INCRBY sequence 100

假设返回值为N,那么[N - 99 ~ N]的数值都是可用的序列值。

当多个客户端同时向Redis申请自增序列时,Redis能够确保每个客户端得到的序列值或序列范围都是全局唯一的,绝对不会出现不同客户端得到了重复的序列值的情况。

List

Redis的List是链表型的数据结构,可以使用LPUSH/RPUSH/LPOP/RPOP等命令在List的两端执行插入元素和弹出元素的操作。虽然List也支持在特定index上插入和读取元素的功能,但其时间复杂度较高(O(N)),应小心使用。

与List相关的常用命令:

  • LPUSH:向指定List的左侧(即头部)插入1个或多个元素,返回插入后的List长度。时间复杂度O(N),N为插入元素的数量
  • RPUSH:同LPUSH,向指定List的右侧(即尾部)插入1或多个元素
  • LPOP:从指定List的左侧(即头部)移除一个元素并返回,时间复杂度O(1)
  • RPOP:同LPOP,从指定List的右侧(即尾部)移除1个元素并返回
  • LPUSHX/RPUSHX:与LPUSH/RPUSH类似,区别在于,LPUSHX/RPUSHX操作的key如果不存在,则不会进行任何操作
  • LLEN:返回指定List的长度,时间复杂度O(1)
  • LRANGE:返回指定List中指定范围的元素(双端包含,即LRANGE key 0 10会返回11个元素),时间复杂度O(N)。应尽可能控制一次获取的元素数量,一次获取过大范围的List元素会导致延迟,同时对长度不可预知的List,避免使用LRANGE key 0 -1这样的完整遍历操作。

应谨慎使用的List相关命令:

  • LINDEX:返回指定List指定index上的元素,如果index越界,返回nil。index数值是回环的,即-1代表List最后一个位置,-2代表List倒数第二个位置。时间复杂度O(N)
  • LSET:将指定List指定index上的元素设置为value,如果index越界则返回错误,时间复杂度O(N),如果操作的是头/尾部的元素,则时间复杂度为O(1)
  • LINSERT:向指定List中指定元素之前/之后插入一个新元素,并返回操作后的List长度。如果指定的元素不存在,返回-1。如果指定key不存在,不会进行任何操作,时间复杂度O(N)

由于Redis的List是链表结构的,上述的三个命令的算法效率较低,需要对List进行遍历,命令的耗时无法预估,在List长度大的情况下耗时会明显增加,应谨慎使用。

换句话说,Redis的List实际是设计来用于实现队列,而不是用于实现类似ArrayList这样的列表的。如果你不是想要实现一个双端出入的队列,那么请尽量不要使用Redis的List数据结构。

为了更好支持队列的特性,Redis还提供了一系列阻塞式的操作命令,如BLPOP/BRPOP等,能够实现类似于BlockingQueue的能力,即在List为空时,阻塞该连接,直到List中有对象可以出队时再返回。针对阻塞类的命令,此处不做详细探讨,请参考官方文档(https://redis.io/topics/data-types-intro) 中"Blocking operations on lists"一节。

Hash

Hash即哈希表,Redis的Hash和传统的哈希表一样,是一种field-value型的数据结构,可以理解成将HashMap搬入Redis。
Hash非常适合用于表现对象类型的数据,用Hash中的field对应对象的field即可。
Hash的优点包括:

  • 可以实现二元查找,如"查找ID为1000的用户的年龄"
  • 比起将整个对象序列化后作为String存储的方法,Hash能够有效地减少网络传输的消耗
  • 当使用Hash维护一个集合时,提供了比List效率高得多的随机访问命令

与Hash相关的常用命令:

  • HSET:将key对应的Hash中的field设置为value。如果该Hash不存在,会自动创建一个。时间复杂度O(1)
  • HGET:返回指定Hash中field字段的值,时间复杂度O(1)
  • HMSET/HMGET:同HSET和HGET,可以批量操作同一个key下的多个field,时间复杂度:O(N),N为一次操作的field数量
  • HSETNX:同HSET,但如field已经存在,HSETNX不会进行任何操作,时间复杂度O(1)
  • HEXISTS:判断指定Hash中field是否存在,存在返回1,不存在返回0,时间复杂度O(1)
  • HDEL:删除指定Hash中的field(1个或多个),时间复杂度:O(N),N为操作的field数量
  • HINCRBY:同INCRBY命令,对指定Hash中的一个field进行INCRBY,时间复杂度O(1)

应谨慎使用的Hash相关命令:

  • HGETALL:返回指定Hash中所有的field-value对。返回结果为数组,数组中field和value交替出现。时间复杂度O(N)
  • HKEYS/HVALS:返回指定Hash中所有的field/value,时间复杂度O(N)

上述三个命令都会对Hash进行完整遍历,Hash中的field数量与命令的耗时线性相关,对于尺寸不可预知的Hash,应严格避免使用上面三个命令,而改为使用HSCAN命令进行游标式的遍历,具体请见 https://redis.io/commands/scan

Set

Redis Set是无序的,不可重复的String集合。

与Set相关的常用命令:

  • SADD:向指定Set中添加1个或多个member,如果指定Set不存在,会自动创建一个。时间复杂度O(N),N为添加的member个数
  • SREM:从指定Set中移除1个或多个member,时间复杂度O(N),N为移除的member个数
  • SRANDMEMBER:从指定Set中随机返回1个或多个member,时间复杂度O(N),N为返回的member个数
  • SPOP:从指定Set中随机移除并返回count个member,时间复杂度O(N),N为移除的member个数
  • SCARD:返回指定Set中的member个数,时间复杂度O(1)
  • SISMEMBER:判断指定的value是否存在于指定Set中,时间复杂度O(1)
  • SMOVE:将指定member从一个Set移至另一个Set

慎用的Set相关命令:

  • SMEMBERS:返回指定Hash中所有的member,时间复杂度O(N)
  • SUNION/SUNIONSTORE:计算多个Set的并集并返回/存储至另一个Set中,时间复杂度O(N),N为参与计算的所有集合的总member数
  • SINTER/SINTERSTORE:计算多个Set的交集并返回/存储至另一个Set中,时间复杂度O(N),N为参与计算的所有集合的总member数
  • SDIFF/SDIFFSTORE:计算1个Set与1或多个Set的差集并返回/存储至另一个Set中,时间复杂度O(N),N为参与计算的所有集合的总member数

上述几个命令涉及的计算量大,应谨慎使用,特别是在参与计算的Set尺寸不可知的情况下,应严格避免使用。可以考虑通过SSCAN命令遍历获取相关Set的全部member(具体请见 https://redis.io/commands/scan ),如果需要做并集/交集/差集计算,可以在客户端进行,或在不服务实时查询请求的Slave上进行。

Sorted Set

Redis Sorted Set是有序的、不可重复的String集合。Sorted Set中的每个元素都需要指派一个分数(score),Sorted Set会根据score对元素进行升序排序。如果多个member拥有相同的score,则以字典序进行升序排序。

Sorted Set非常适合用于实现排名。

Sorted Set的主要命令:

  • ZADD:向指定Sorted Set中添加1个或多个member,时间复杂度O(Mlog(N)),M为添加的member数量,N为Sorted Set中的member数量
  • ZREM:从指定Sorted Set中删除1个或多个member,时间复杂度O(Mlog(N)),M为删除的member数量,N为Sorted Set中的member数量
  • ZCOUNT:返回指定Sorted Set中指定score范围内的member数量,时间复杂度:O(log(N))
  • ZCARD:返回指定Sorted Set中的member数量,时间复杂度O(1)
  • ZSCORE:返回指定Sorted Set中指定member的score,时间复杂度O(1)
  • ZRANK/ZREVRANK:返回指定member在Sorted Set中的排名,ZRANK返回按升序排序的排名,ZREVRANK则返回按降序排序的排名。时间复杂度O(log(N))
  • ZINCRBY:同INCRBY,对指定Sorted Set中的指定member的score进行自增,时间复杂度O(log(N))

慎用的Sorted Set相关命令:

  • ZRANGE/ZREVRANGE:返回指定Sorted Set中指定排名范围内的所有member,ZRANGE为按score升序排序,ZREVRANGE为按score降序排序,时间复杂度O(log(N)+M),M为本次返回的member数
  • ZRANGEBYSCORE/ZREVRANGEBYSCORE:返回指定Sorted Set中指定score范围内的所有member,返回结果以升序/降序排序,min和max可以指定为-inf和+inf,代表返回所有的member。时间复杂度O(log(N)+M)
  • ZREMRANGEBYRANK/ZREMRANGEBYSCORE:移除Sorted Set中指定排名范围/指定score范围内的所有member。时间复杂度O(log(N)+M)

上述几个命令,应尽量避免传递[0 -1]或[-inf +inf]这样的参数,来对Sorted Set做一次性的完整遍历,特别是在Sorted Set的尺寸不可预知的情况下。可以通过ZSCAN命令来进行游标式的遍历(具体请见 https://redis.io/commands/scan ),或通过LIMIT参数来限制返回member的数量(适用于ZRANGEBYSCORE和ZREVRANGEBYSCORE命令),以实现游标式的遍历。

Bitmap和HyperLogLog

Redis的这两种数据结构相较之前的并不常用,在本文中只做简要介绍,如想要详细了解这两种数据结构与其相关的命令,请参考官方文档https://redis.io/topics/data-types-intro 中的相关章节

Bitmap在Redis中不是一种实际的数据类型,而是一种将String作为Bitmap使用的方法。可以理解为将String转换为bit数组。使用Bitmap来存储true/false类型的简单数据极为节省空间。

HyperLogLogs是一种主要用于数量统计的数据结构,它和Set类似,维护一个不可重复的String集合,但是HyperLogLogs并不维护具体的member内容,只维护member的个数。也就是说,HyperLogLogs只能用于计算一个集合中不重复的元素数量,所以它比Set要节省很多内存空间。

其他常用命令

  • EXISTS:判断指定的key是否存在,返回1代表存在,0代表不存在,时间复杂度O(1)
  • DEL:删除指定的key及其对应的value,时间复杂度O(N),N为删除的key数量
  • EXPIRE/PEXPIRE:为一个key设置有效期,单位为秒或毫秒,时间复杂度O(1)
  • TTL/PTTL:返回一个key剩余的有效时间,单位为秒或毫秒,时间复杂度O(1)
  • RENAME/RENAMENX:将key重命名为newkey。使用RENAME时,如果newkey已经存在,其值会被覆盖;使用RENAMENX时,如果newkey已经存在,则不会进行任何操作,时间复杂度O(1)
  • TYPE:返回指定key的类型,string, list, set, zset, hash。时间复杂度O(1)
  • CONFIG GET:获得Redis某配置项的当前值,可以使用*通配符,时间复杂度O(1)
  • CONFIG SET:为Redis某个配置项设置新值,时间复杂度O(1)
  • CONFIG REWRITE:让Redis重新加载redis.conf中的配置

数据持久化

Redis提供了将数据定期自动持久化至硬盘的能力,包括RDB和AOF两种方案,两种方案分别有其长处和短板,可以配合起来同时运行,确保数据的稳定性。

必须使用数据持久化吗?

Redis的数据持久化机制是可以关闭的。如果你只把Redis作为缓存服务使用,Redis中存储的所有数据都不是该数据的主体而仅仅是同步过来的备份,那么可以关闭Redis的数据持久化机制。
但通常来说,仍然建议至少开启RDB方式的数据持久化,因为:

  • RDB方式的持久化几乎不损耗Redis本身的性能,在进行RDB持久化时,Redis主进程唯一需要做的事情就是fork出一个子进程,所有持久化工作都由子进程完成
  • Redis无论因为什么原因crash掉之后,重启时能够自动恢复到上一次RDB快照中记录的数据。这省去了手工从其他数据源(如DB)同步数据的过程,而且要比其他任何的数据恢复方式都要快
  • 现在硬盘那么大,真的不缺那一点地方

RDB

采用RDB持久方式,Redis会定期保存数据快照至一个rbd文件中,并在启动时自动加载rdb文件,恢复之前保存的数据。可以在配置文件中配置Redis进行快照保存的时机:

save [seconds] [changes]

意为在[seconds]秒内如果发生了[changes]次数据修改,则进行一次RDB快照保存,例如

save 60 100

会让Redis每60秒检查一次数据变更情况,如果发生了100次或以上的数据变更,则进行RDB快照保存。
可以配置多条save指令,让Redis执行多级的快照保存策略。
Redis默认开启RDB快照,默认的RDB策略如下:

save 900 1 save 300 10 save 60 10000

也可以通过BGSAVE命令手工触发RDB快照保存。

RDB的优点:

  • 对性能影响最小。如前文所述,Redis在保存RDB快照时会fork出子进程进行,几乎不影响Redis处理客户端请求的效率。
  • 每次快照会生成一个完整的数据快照文件,所以可以辅以其他手段保存多个时间点的快照(例如把每天0点的快照备份至其他存储媒介中),作为非常可靠的灾难恢复手段。
  • 使用RDB文件进行数据恢复比使用AOF要快很多。

RDB的缺点:

  • 快照是定期生成的,所以在Redis crash时或多或少会丢失一部分数据。
  • 如果数据集非常大且CPU不够强(比如单核CPU),Redis在fork子进程时可能会消耗相对较长的时间(长至1秒),影响这期间的客户端请求。

AOF

采用AOF持久方式时,Redis会把每一个写请求都记录在一个日志文件里。在Redis重启时,会把AOF文件中记录的所有写操作顺序执行一遍,确保数据恢复到最新。

AOF默认是关闭的,如要开启,进行如下配置:

appendonly yes

AOF提供了三种fsync配置,always/everysec/no,通过配置项[appendfsync]指定:

  • appendfsync no:不进行fsync,将flush文件的时机交给OS决定,速度最快
  • appendfsync always:每写入一条日志就进行一次fsync操作,数据安全性最高,但速度最慢
  • appendfsync everysec:折中的做法,交由后台线程每秒fsync一次

随着AOF不断地记录写操作日志,必定会出现一些无用的日志,例如某个时间点执行了命令SET key1 "abc",在之后某个时间点又执行了SET key1 "bcd",那么第一条命令很显然是没有用的。大量的无用日志会让AOF文件过大,也会让数据恢复的时间过长。
所以Redis提供了AOF rewrite功能,可以重写AOF文件,只保留能够把数据恢复到最新状态的最小写操作集。
AOF rewrite可以通过BGREWRITEAOF命令触发,也可以配置Redis定期自动进行:

auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb

上面两行配置的含义是,Redis在每次AOF rewrite时,会记录完成rewrite后的AOF日志大小,当AOF日志大小在该基础上增长了100%后,自动进行AOF rewrite。同时如果增长的大小没有达到64mb,则不会进行rewrite。

AOF的优点:

  • 最安全,在启用appendfsync always时,任何已写入的数据都不会丢失,使用在启用appendfsync everysec也至多只会丢失1秒的数据。
  • AOF文件在发生断电等问题时也不会损坏,即使出现了某条日志只写入了一半的情况,也可以使用redis-check-aof工具轻松修复。
  • AOF文件易读,可修改,在进行了某些错误的数据清除操作后,只要AOF文件没有rewrite,就可以把AOF文件备份出来,把错误的命令删除,然后恢复数据。

AOF的缺点:

  • AOF文件通常比RDB文件更大
  • 性能消耗比RDB高
  • 数据恢复速度比RDB慢

内存管理与数据淘汰机制

最大内存设置

默认情况下,在32位OS中,Redis最大使用3GB的内存,在64位OS中则没有限制。

在使用Redis时,应该对数据占用的最大空间有一个基本准确的预估,并为Redis设定最大使用的内存。否则在64位OS中Redis会无限制地占用内存(当物理内存被占满后会使用swap空间),容易引发各种各样的问题。

通过如下配置控制Redis使用的最大内存:

maxmemory 100mb

在内存占用达到了maxmemory后,再向Redis写入数据时,Redis会:

  • 根据配置的数据淘汰策略尝试淘汰数据,释放空间
  • 如果没有数据可以淘汰,或者没有配置数据淘汰策略,那么Redis会对所有写请求返回错误,但读请求仍然可以正常执行

在为Redis设置maxmemory时,需要注意:

  • 如果采用了Redis的主从同步,主节点向从节点同步数据时,会占用掉一部分内存空间,如果maxmemory过于接近主机的可用内存,导致数据同步时内存不足。所以设置的maxmemory不要过于接近主机可用的内存,留出一部分预留用作主从同步。

数据淘汰机制

Redis提供了5种数据淘汰策略:

  • volatile-lru:使用LRU算法进行数据淘汰(淘汰上次使用时间最早的,且使用次数最少的key),只淘汰设定了有效期的key
  • allkeys-lru:使用LRU算法进行数据淘汰,所有的key都可以被淘汰
  • volatile-random:随机淘汰数据,只淘汰设定了有效期的key
  • allkeys-random:随机淘汰数据,所有的key都可以被淘汰
  • volatile-ttl:淘汰剩余有效期最短的key

最好为Redis指定一种有效的数据淘汰策略以配合maxmemory设置,避免在内存使用满后发生写入失败的情况。

一般来说,推荐使用的策略是volatile-lru,并辨识Redis中保存的数据的重要性。对于那些重要的,绝对不能丢弃的数据(如配置类数据等),应不设置有效期,这样Redis就永远不会淘汰这些数据。对于那些相对不是那么重要的,并且能够热加载的数据(比如缓存最近登录的用户信息,当在Redis中找不到时,程序会去DB中读取),可以设置上有效期,这样在内存不够时Redis就会淘汰这部分数据。

配置方法:

maxmemory-policy volatile-lru   #默认是noeviction,即不进行数据淘汰

Pipelining

Pipelining

Redis提供许多批量操作的命令,如MSET/MGET/HMSET/HMGET等等,这些命令存在的意义是减少维护网络连接和传输数据所消耗的资源和时间。
例如连续使用5次SET命令设置5个不同的key,比起使用一次MSET命令设置5个不同的key,效果是一样的,但前者会消耗更多的RTT(Round Trip Time)时长,永远应优先使用后者。

然而,如果客户端要连续执行的多次操作无法通过Redis命令组合在一起,例如:

SET a "abc" INCR b HSET c name "hi"

此时便可以使用Redis提供的pipelining功能来实现在一次交互中执行多条命令。
使用pipelining时,只需要从客户端一次向Redis发送多条命令(以\r\n)分隔,Redis就会依次执行这些命令,并且把每个命令的返回按顺序组装在一起一次返回,比如:

$ (printf "PING\r\nPING\r\nPING\r\n"; sleep 1) | nc localhost 6379 +PONG +PONG +PONG

大部分的Redis客户端都对Pipelining提供支持,所以开发者通常并不需要自己手工拼装命令列表。

Pipelining的局限性

Pipelining只能用于执行连续且无相关性的命令,当某个命令的生成需要依赖于前一个命令的返回时,就无法使用Pipelining了。

通过Scripting功能,可以规避这一局限性

事务与Scripting

Pipelining能够让Redis在一次交互中处理多条命令,然而在一些场景下,我们可能需要在此基础上确保这一组命令是连续执行的。

比如获取当前累计的PV数并将其清0

> GET vCount 12384 > SET vCount 0 OK

如果在GET和SET命令之间插进来一个INCR vCount,就会使客户端拿到的vCount不准确。

Redis的事务可以确保复数命令执行时的原子性。也就是说Redis能够保证:一个事务中的一组命令是绝对连续执行的,在这些命令执行完成之前,绝对不会有来自于其他连接的其他命令插进去执行。

通过MULTI和EXEC命令来把这两个命令加入一个事务中:

> MULTI OK > GET vCount QUEUED > SET vCount 0 QUEUED > EXEC 1) 12384 2) OK

Redis在接收到MULTI命令后便会开启一个事务,这之后的所有读写命令都会保存在队列中但并不执行,直到接收到EXEC命令后,Redis会把队列中的所有命令连续顺序执行,并以数组形式返回每个命令的返回结果。

可以使用DISCARD命令放弃当前的事务,将保存的命令队列清空。

需要注意的是,Redis事务不支持回滚
如果一个事务中的命令出现了语法错误,大部分客户端驱动会返回错误,2.6.5版本以上的Redis也会在执行EXEC时检查队列中的命令是否存在语法错误,如果存在,则会自动放弃事务并返回错误。
但如果一个事务中的命令有非语法类的错误(比如对String执行HSET操作),无论客户端驱动还是Redis都无法在真正执行这条命令之前发现,所以事务中的所有命令仍然会被依次执行。在这种情况下,会出现一个事务中部分命令成功部分命令失败的情况,然而与RDBMS不同,Redis不提供事务回滚的功能,所以只能通过其他方法进行数据的回滚。

通过事务实现CAS

Redis提供了WATCH命令与事务搭配使用,实现CAS乐观锁的机制。

假设要实现将某个商品的状态改为已售:

if(exec(HGET stock:1001 state) == "in stock")     exec(HSET stock:1001 state "sold");

这一伪代码执行时,无法确保并发安全性,有可能多个客户端都获取到了"in stock"的状态,导致一个库存被售卖多次。

使用WATCH命令和事务可以解决这一问题:

exec(WATCH stock:1001); if(exec(HGET stock:1001 state) == "in stock") {     exec(MULTI);     exec(HSET stock:1001 state "sold");     exec(EXEC); }

WATCH的机制是:在事务EXEC命令执行时,Redis会检查被WATCH的key,只有被WATCH的key从WATCH起始时至今没有发生过变更,EXEC才会被执行。如果WATCH的key在WATCH命令到EXEC命令之间发生过变化,则EXEC命令会返回失败。

Scripting

通过EVAL与EVALSHA命令,可以让Redis执行LUA脚本。这就类似于RDBMS的存储过程一样,可以把客户端与Redis之间密集的读/写交互放在服务端进行,避免过多的数据交互,提升性能。

Scripting功能是作为事务功能的替代者诞生的,事务提供的所有能力Scripting都可以做到。Redis官方推荐使用LUA Script来代替事务,前者的效率和便利性都超过了事务。

关于Scripting的具体使用,本文不做详细介绍,请参考官方文档 https://redis.io/commands/eval

Redis性能调优

尽管Redis是一个非常快速的内存数据存储媒介,也并不代表Redis不会产生性能问题。
前文中提到过,Redis采用单线程模型,所有的命令都是由一个线程串行执行的,所以当某个命令执行耗时较长时,会拖慢其后的所有命令,这使得Redis对每个任务的执行效率更加敏感。

针对Redis的性能优化,主要从下面几个层面入手:

  • 最初的也是最重要的,确保没有让Redis执行耗时长的命令
  • 使用pipelining将连续执行的命令组合执行
  • 操作系统的Transparent huge pages功能必须关闭:
    echo never > /sys/kernel/mm/transparent_hugepage/enabled
  • 如果在虚拟机中运行Redis,可能天然就有虚拟机环境带来的固有延迟。可以通过./redis-cli --intrinsic-latency 100命令查看固有延迟。同时如果对Redis的性能有较高要求的话,应尽可能在物理机上直接部署Redis。
  • 检查数据持久化策略
  • 考虑引入读写分离机制

长耗时命令

Redis绝大多数读写命令的时间复杂度都在O(1)到O(N)之间,在文本和官方文档中均对每个命令的时间复杂度有说明。

通常来说,O(1)的命令是安全的,O(N)命令在使用时需要注意,如果N的数量级不可预知,则应避免使用。例如对一个field数未知的Hash数据执行HGETALL/HKEYS/HVALS命令,通常来说这些命令执行的很快,但如果这个Hash中的field数量极多,耗时就会成倍增长。
又如使用SUNION对两个Set执行Union操作,或使用SORT对List/Set执行排序操作等时,都应该严加注意。

避免在使用这些O(N)命令时发生问题主要有几个办法:

  • 不要把List当做列表使用,仅当做队列来使用
  • 通过机制严格控制Hash、Set、Sorted Set的大小
  • 可能的话,将排序、并集、交集等操作放在客户端执行
  • 绝对禁止使用KEYS命令
  • 避免一次性遍历集合类型的所有成员,而应使用SCAN类的命令进行分批的,游标式的遍历

Redis提供了SCAN命令,可以对Redis中存储的所有key进行游标式的遍历,避免使用KEYS命令带来的性能问题。同时还有SSCAN/HSCAN/ZSCAN等命令,分别用于对Set/Hash/Sorted Set中的元素进行游标式遍历。SCAN类命令的使用请参考官方文档:https://redis.io/commands/scan

Redis提供了Slow Log功能,可以自动记录耗时较长的命令。相关的配置参数有两个:

slowlog-log-slower-than xxxms  #执行时间慢于xxx毫秒的命令计入Slow Log slowlog-max-len xxx  #Slow Log的长度,即最大纪录多少条Slow Log

使用SLOWLOG GET [number]命令,可以输出最近进入Slow Log的number条命令。
使用SLOWLOG RESET命令,可以重置Slow Log

网络引发的延迟

  • 尽可能使用长连接或连接池,避免频繁创建销毁连接
  • 客户端进行的批量数据操作,应使用Pipeline特性在一次交互中完成。具体请参照本文的Pipelining章节

数据持久化引发的延迟

Redis的数据持久化工作本身就会带来延迟,需要根据数据的安全级别和性能要求制定合理的持久化策略:

  • AOF + fsync always的设置虽然能够绝对确保数据安全,但每个操作都会触发一次fsync,会对Redis的性能有比较明显的影响
  • AOF + fsync every second是比较好的折中方案,每秒fsync一次
  • AOF + fsync never会提供AOF持久化方案下的最优性能
  • 使用RDB持久化通常会提供比使用AOF更高的性能,但需要注意RDB的策略配置
  • 每一次RDB快照和AOF Rewrite都需要Redis主进程进行fork操作。fork操作本身可能会产生较高的耗时,与CPU和Redis占用的内存大小有关。根据具体的情况合理配置RDB快照和AOF Rewrite时机,避免过于频繁的fork带来的延迟

Redis在fork子进程时需要将内存分页表拷贝至子进程,以占用了24GB内存的Redis实例为例,共需要拷贝24GB / 4kB * 8 = 48MB的数据。在使用单Xeon 2.27Ghz的物理机上,这一fork操作耗时216ms。

可以通过INFO命令返回的latest_fork_usec字段查看上一次fork操作的耗时(微秒)

Swap引发的延迟

当Linux将Redis所用的内存分页移至swap空间时,将会阻塞Redis进程,导致Redis出现不正常的延迟。Swap通常在物理内存不足或一些进程在进行大量I/O操作时发生,应尽可能避免上述两种情况的出现。

/proc/<pid>/smaps文件中会保存进程的swap记录,通过查看这个文件,能够判断Redis的延迟是否由Swap产生。如果这个文件中记录了较大的Swap size,则说明延迟很有可能是Swap造成的。

数据淘汰引发的延迟

当同一秒内有大量key过期时,也会引发Redis的延迟。在使用时应尽量将key的失效时间错开。

引入读写分离机制

Redis的主从复制能力可以实现一主多从的多节点架构,在这一架构下,主节点接收所有写请求,并将数据同步给多个从节点。
在这一基础上,我们可以让从节点提供对实时性要求不高的读请求服务,以减小主节点的压力。
尤其是针对一些使用了长耗时命令的统计类任务,完全可以指定在一个或多个从节点上执行,避免这些长耗时命令影响其他请求的响应。

关于读写分离的具体说明,请参见后续章节

主从复制与集群分片

主从复制

Redis支持一主多从的主从复制架构。一个Master实例负责处理所有的写请求,Master将写操作同步至所有Slave。
借助Redis的主从复制,可以实现读写分离和高可用:

  • 实时性要求不是特别高的读请求,可以在Slave上完成,提升效率。特别是一些周期性执行的统计任务,这些任务可能需要执行一些长耗时的Redis命令,可以专门规划出1个或几个Slave用于服务这些统计任务
  • 借助Redis Sentinel可以实现高可用,当Master crash后,Redis Sentinel能够自动将一个Slave晋升为Master,继续提供服务

启用主从复制非常简单,只需要配置多个Redis实例,在作为Slave的Redis实例中配置:

slaveof 192.168.1.1 6379  #指定Master的IP和端口

当Slave启动后,会从Master进行一次冷启动数据同步,由Master触发BGSAVE生成RDB文件推送给Slave进行导入,导入完成后Master再将增量数据通过Redis Protocol同步给Slave。之后主从之间的数据便一直以Redis Protocol进行同步

使用Sentinel做自动failover

Redis的主从复制功能本身只是做数据同步,并不提供监控和自动failover能力,要通过主从复制功能来实现Redis的高可用,还需要引入一个组件:Redis Sentinel

Redis Sentinel是Redis官方开发的监控组件,可以监控Redis实例的状态,通过Master节点自动发现Slave节点,并在监测到Master节点失效时选举出一个新的Master,并向所有Redis实例推送新的主从配置。

Redis Sentinel需要至少部署3个实例才能形成选举关系。

关键配置:

sentinel monitor mymaster 127.0.0.1 6379 2  #Master实例的IP、端口,以及选举需要的赞成票数 sentinel down-after-milliseconds mymaster 60000  #多长时间没有响应视为Master失效 sentinel failover-timeout mymaster 180000  #两次failover尝试间的间隔时长 sentinel parallel-syncs mymaster 1  #如果有多个Slave,可以通过此配置指定同时从新Master进行数据同步的Slave数,避免所有Slave同时进行数据同步导致查询服务也不可用

另外需要注意的是,Redis Sentinel实现的自动failover不是在同一个IP和端口上完成的,也就是说自动failover产生的新Master提供服务的IP和端口与之前的Master是不一样的,所以要实现HA,还要求客户端必须支持Sentinel,能够与Sentinel交互获得新Master的信息才行。

集群分片

为何要做集群分片:

  • Redis中存储的数据量大,一台主机的物理内存已经无法容纳
  • Redis的写请求并发量大,一个Redis实例以无法承载

当上述两个问题出现时,就必须要对Redis进行分片了。
Redis的分片方案有很多种,例如很多Redis的客户端都自行实现了分片功能,也有向Twemproxy这样的以代理方式实现的Redis分片方案。然而首选的方案还应该是Redis官方在3.0版本中推出的Redis Cluster分片方案。

本文不会对Redis Cluster的具体安装和部署细节进行介绍,重点介绍Redis Cluster带来的好处与弊端。

Redis Cluster的能力

  • 能够自动将数据分散在多个节点上
  • 当访问的key不在当前分片上时,能够自动将请求转发至正确的分片
  • 当集群中部分节点失效时仍能提供服务

其中第三点是基于主从复制来实现的,Redis Cluster的每个数据分片都采用了主从复制的结构,原理和前文所述的主从复制完全一致,唯一的区别是省去了Redis Sentinel这一额外的组件,由Redis Cluster负责进行一个分片内部的节点监控和自动failover。

Redis Cluster分片原理

Redis Cluster中共有16384个hash slot,Redis会计算每个key的CRC16,将结果与16384取模,来决定该key存储在哪一个hash slot中,同时需要指定Redis Cluster中每个数据分片负责的Slot数。Slot的分配在任何时间点都可以进行重新分配。

客户端在对key进行读写操作时,可以连接Cluster中的任意一个分片,如果操作的key不在此分片负责的Slot范围内,Redis Cluster会自动将请求重定向到正确的分片上。

hash tags

在基础的分片原则上,Redis还支持hash tags功能,以hash tags要求的格式明明的key,将会确保进入同一个Slot中。例如:{uiv}user:1000和{uiv}user:1001拥有同样的hash tag {uiv},会保存在同一个Slot中。

使用Redis Cluster时,pipelining、事务和LUA Script功能涉及的key必须在同一个数据分片上,否则将会返回错误。如要在Redis Cluster中使用上述功能,就必须通过hash tags来确保一个pipeline或一个事务中操作的所有key都位于同一个Slot中。

有一些客户端(如Redisson)实现了集群化的pipelining操作,可以自动将一个pipeline里的命令按key所在的分片进行分组,分别发到不同的分片上执行。但是Redis不支持跨分片的事务,事务和LUA Script还是必须遵循所有key在一个分片上的规则要求。

主从复制 vs 集群分片

在设计软件架构时,要如何在主从复制和集群分片两种部署方案中取舍呢?

从各个方面看,Redis Cluster都是优于主从复制的方案

  • Redis Cluster能够解决单节点上数据量过大的问题
  • Redis Cluster能够解决单节点访问压力过大的问题
  • Redis Cluster包含了主从复制的能力

那是不是代表Redis Cluster永远是优于主从复制的选择呢?

并不是。

软件架构永远不是越复杂越好,复杂的架构在带来显著好处的同时,一定也会带来相应的弊端。采用Redis Cluster的弊端包括:

  • 维护难度增加。在使用Redis Cluster时,需要维护的Redis实例数倍增,需要监控的主机数量也相应增加,数据备份/持久化的复杂度也会增加。同时在进行分片的增减操作时,还需要进行reshard操作,远比主从模式下增加一个Slave的复杂度要高。
  • 客户端资源消耗增加。当客户端使用连接池时,需要为每一个数据分片维护一个连接池,客户端同时需要保持的连接数成倍增多,加大了客户端本身和操作系统资源的消耗。
  • 性能优化难度增加。你可能需要在多个分片上查看Slow Log和Swap日志才能定位性能问题。
  • 事务和LUA Script的使用成本增加。在Redis Cluster中使用事务和LUA Script特性有严格的限制条件,事务和Script中操作的key必须位于同一个分片上,这就使得在开发时必须对相应场景下涉及的key进行额外的规划和规范要求。如果应用的场景中大量涉及事务和Script的使用,如何在保证这两个功能的正常运作前提下把数据平均分到多个数据分片中就会成为难点。

所以说,在主从复制和集群分片两个方案中做出选择时,应该从应用软件的功能特性、数据和访问量级、未来发展规划等方面综合考虑,只在确实有必要引入数据分片时再使用Redis Cluster。
下面是一些建议:

  1. 需要在Redis中存储的数据有多大?未来2年内可能发展为多大?这些数据是否都需要长期保存?是否可以使用LRU算法进行非热点数据的淘汰?综合考虑前面几个因素,评估出Redis需要使用的物理内存。
  2. 用于部署Redis的主机物理内存有多大?有多少可以分配给Redis使用?对比(1)中的内存需求评估,是否足够用?
  3. Redis面临的并发写压力会有多大?在不使用pipelining时,Redis的写性能可以超过10万次/秒(更多的benchmark可以参考 https://redis.io/topics/benchmarks )
  4. 在使用Redis时,是否会使用到pipelining和事务功能?使用的场景多不多?

综合上面几点考虑,如果单台主机的可用物理内存完全足以支撑对Redis的容量需求,且Redis面临的并发写压力距离Benchmark值还尚有距离,建议采用主从复制的架构,可以省去很多不必要的麻烦。同时,如果应用中大量使用pipelining和事务,也建议尽可能选择主从复制架构,可以减少设计和开发时的复杂度。

Redis Java客户端的选择

Redis的Java客户端很多,官方推荐的有三种:Jedis、Redisson和lettuce。

在这里对Jedis和Redisson进行对比介绍

Jedis:

  • 轻量,简洁,便于集成和改造
  • 支持连接池
  • 支持pipelining、事务、LUA Scripting、Redis Sentinel、Redis Cluster
  • 不支持读写分离,需要自己实现
  • 文档差(真的很差,几乎没有……)

Redisson:

  • 基于Netty实现,采用非阻塞IO,性能高
  • 支持异步请求
  • 支持连接池
  • 支持pipelining、LUA Scripting、Redis Sentinel、Redis Cluster
  • 不支持事务,官方建议以LUA Scripting代替事务
  • 支持在Redis Cluster架构下使用pipelining
  • 支持读写分离,支持读负载均衡,在主从复制和Redis Cluster架构下都可以使用
  • 内建Tomcat Session Manager,为Tomcat 6/7/8提供了会话共享功能
  • 可以与Spring Session集成,实现基于Redis的会话共享
  • 文档较丰富,有中文文档

对于Jedis和Redisson的选择,同样应遵循前述的原理,尽管Jedis比起Redisson有各种各样的不足,但也应该在需要使用Redisson的高级特性时再选用Redisson,避免造成不必要的程序复杂度提升。

Jedis:
github:https://github.com/xetorthio/jedis
文档:https://github.com/xetorthio/jedis/wiki

Redisson:
github:https://github.com/redisson/redisson
文档:https://github.com/redisson/redisson/wiki

Redis应用场景-转载

 1.  MySql+Memcached架构的问题

 

  实际MySQL是适合进行海量数据存储的,通过Memcached将热点数据加载到cache,加速访问,很多公司都曾经使用过这样的架构,但随着业务数据量的不断增加,和访问量的持续增长,我们遇到了很多问题:

  1.MySQL需要不断进行拆库拆表,Memcached也需不断跟着扩容,扩容和维护工作占据大量开发时间。

  2.Memcached与MySQL数据库数据一致性问题。

  3.Memcached数据命中率低或down机,大量访问直接穿透到DB,MySQL无法支撑。

  4.跨机房cache同步问题。

  众多NoSQL百花齐放,如何选择

  最近几年,业界不断涌现出很多各种各样的NoSQL产品,那么如何才能正确地使用好这些产品,最大化地发挥其长处,是我们需要深入研究和思考的问题,实际归根结底最重要的是了解这些产品的定位,并且了解到每款产品的tradeoffs,在实际应用中做到扬长避短,总体上这些NoSQL主要用于解决以下几种问题

  1.少量数据存储,高速读写访问。此类产品通过数据全部in-momery 的方式来保证高速访问,同时提供数据落地的功能,实际这正是Redis最主要的适用场景。

  2.海量数据存储,分布式系统支持,数据一致性保证,方便的集群节点添加/删除。

  3.这方面最具代表性的是dynamo和bigtable 2篇论文所阐述的思路。前者是一个完全无中心的设计,节点之间通过gossip方式传递集群信息,数据保证最终一致性,后者是一个中心化的方案设计,通过类似一个分布式锁服务来保证强一致性,数据写入先写内存和redo log,然后定期compat归并到磁盘上,将随机写优化为顺序写,提高写入性能。

  4.Schema free,auto-sharding等。比如目前常见的一些文档数据库都是支持schema-free的,直接存储json格式数据,并且支持auto-sharding等功能,比如mongodb。

  面对这些不同类型的NoSQL产品,我们需要根据我们的业务场景选择最合适的产品。

       Redis最适合所有数据in-momory的场景,虽然Redis也提供持久化功能,但实际更多的是一个disk-backed的功能,跟传统意义上的持久化有比较大的差别,那么可能大家就会有疑问,似乎Redis更像一个加强版的Memcached,那么何时使用Memcached,何时使用Redis呢?

       如果简单地比较Redis与Memcached的区别,大多数都会得到以下观点:

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

 

 

2.  Redis常用数据类型

Redis最为常用的数据类型主要有以下:

  • String
  • Hash
  • List
  • Set
  • Sorted set
  • pub/sub
  • Transactions

在具体描述这几种数据类型之前,我们先通过一张图了解下Redis内部内存管理中是如何描述这些不同数据类型的:

         首先Redis内部使用一个redisObject对象来表示所有的key和value,redisObject最主要的信息如上图所示:

         type代表一个value对象具体是何种数据类型,

         encoding是不同数据类型在redis内部的存储方式,

         比如:type=string代表value存储的是一个普通字符串,那么对应的encoding可以是raw或者是int,如果是int则代表实际redis内部是按数值型类存储和表示这个字符串的,当然前提是这个字符串本身可以用数值表示,比如:"123" "456"这样的字符串。

       这里需要特殊说明一下vm字段,只有打开了Redis的虚拟内存功能,此字段才会真正的分配内存,该功能默认是关闭状态的,该功能会在后面具体描述。通过上图我们可以发现Redis使用redisObject来表示所有的key/value数据是比较浪费内存的,当然这些内存管理成本的付出主要也是为了给Redis不同数据类型提供一个统一的管理接口,实际作者也提供了多种方法帮助我们尽量节省内存使用,我们随后会具体讨论。

 

 

 

3.  各种数据类型应用和实现方式

下面我们先来逐一的分析下这7种数据类型的使用和内部实现方式:

  • String:
Strings 数据结构是简单的key-value类型,value其实不仅是String,也可以是数字.

常用命令:  set,get,decr,incr,mget 等。

 

应用场景:String是最常用的一种数据类型,普通的key/ value 存储都可以归为此类.即可以完全实现目前 Memcached 的功能,并且效率更高。还可以享受Redis的定时持久化,操作日志及 Replication等功能。除了提供与 Memcached 一样的get、set、incr、decr 等操作外,Redis还提供了下面一些操作:

 

 

    • 获取字符串长度
    • 往字符串append内容
    • 设置和获取字符串的某一段内容
    • 设置及获取字符串的某一位(bit)
    • 批量设置一系列字符串的内容

 

 

实现方式:String在redis内部存储默认就是一个字符串,被redisObject所引用,当遇到incr,decr等操作时会转成数值型进行计算,此时redisObject的encoding字段为int。

 

  • Hash

常用命令:hget,hset,hgetall 等。

应用场景:在Memcached中,我们经常将一些结构化的信息打包成HashMap,在客户端序列化后存储为一个字符串的值,比如用户的昵称、年龄、性别、积分等,这时候在需要修改其中某一项时,通常需要将所有值取出反序列化后,修改某一项的值,再序列化存储回去。这样不仅增大了开销,也不适用于一些可能并发操作的场合(比如两个并发的操作都需要修改积分)。而Redis的Hash结构可以使你像在数据库中Update一个属性一样只修改某一项属性值。

        我们简单举个实例来描述下Hash的应用场景,比如我们要存储一个用户信息对象数据,包含以下信息:

用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储,主要有以下2种存储方式:

 

第一种方式将用户ID作为查找key,把其他信息封装成一个对象以序列化的方式存储,这种方式的缺点是,增加了序列化/反序列化的开销,并且在需要修改其中一项信息时,需要把整个对象取回,并且修改操作需要对并发进行保护,引入CAS等复杂问题。

第二种方法是这个用户信息对象有多少成员就存成多少个key-value对儿,用用户ID+对应属性的名称作为唯一标识来取得对应属性的值,虽然省去了序列化开销和并发问题,但是用户ID为重复存储,如果存在大量这样的数据,内存浪费还是非常可观的。

那么Redis提供的Hash很好的解决了这个问题,Redis的Hash实际是内部存储的Value为一个HashMap,并提供了直接存取这个Map成员的接口,如下图:

也就是说,Key仍然是用户ID, value是一个Map,这个Map的key是成员的属性名,value是属性值,这样对数据的修改和存取都可以直接通过其内部Map的Key(Redis里称内部Map的key为field), 也就是通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。很好的解决了问题。

这里同时需要注意,Redis提供了接口(hgetall)可以直接取到全部的属性数据,但是如果内部Map的成员很多,那么涉及到遍历整个内部Map的操作,由于Redis单线程模型的缘故,这个遍历操作可能会比较耗时,而另其它客户端的请求完全不响应,这点需要格外注意。

实现方式:

上面已经说到Redis Hash对应Value内部实际就是一个HashMap,实际这里会有2种不同实现,这个Hash的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,对应的value redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为ht。

  • List

常用命令:lpush,rpush,lpop,rpop,lrange等。

应用场景:

Redis list的应用场景非常多,也是Redis最重要的数据结构之一,比如twitter的关注列表,粉丝列表等都可以用Redis的list结构来实现。

Lists 就是链表,相信略有数据结构知识的人都应该能理解其结构。使用Lists结构,我们可以轻松地实现最新消息排行等功能。Lists的另一个应用就是消息队列,
可以利用Lists的PUSH操作,将任务存在Lists中,然后工作线程再用POP操作将任务取出进行执行。Redis还提供了操作Lists中某一段的api,你可以直接查询,删除Lists中某一段的元素。

实现方式:

Redis list的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。

  • Set

常用命令:

sadd,spop,smembers,sunion 等。

应用场景:

Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。

Sets 集合的概念就是一堆不重复值的组合。利用Redis提供的Sets数据结构,可以存储一些集合性的数据,比如在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis还为集合提供了求交集、并集、差集等操作,可以非常方便的实现如共同关注、共同喜好、二度好友等功能,对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集合中。

实现方式:

set 的内部实现是一个 value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。

  • Sorted Set

常用命令:

zadd,zrange,zrem,zcard等

使用场景:

Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择sorted set数据结构,比如twitter 的public timeline可以以发表时间作为score来存储,这样获取时就是自动按时间排好序的。

另外还可以用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。

实现方式:

Redis sorted set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。

 

  • Pub/Sub

 

Pub/Sub 从字面上理解就是发布(Publish)与订阅(Subscribe),在Redis中,你可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。

 

  • Transactions

 

谁说NoSQL都不支持事务,虽然Redis的Transactions提供的并不是严格的ACID的事务(比如一串用EXEC提交执行的命令,在执行中服务器宕机,那么会有一部分命令执行了,剩下的没执行),但是这个Transactions还是提供了基本的命令打包执行的功能(在服务器不出问题的情况下,可以保证一连串的命令是顺序在一起执行的,中间有会有其它客户端命令插进来执行)。Redis还提供了一个Watch功能,你可以对一个key进行Watch,然后再执行Transactions,在这过程中,如果这个Watched的值进行了修改,那么这个Transactions会发现并拒绝执行。

 

 

 

4.  Redis实际应用场景

 

        Redis在很多方面与其他数据库解决方案不同:它使用内存提供主存储支持,而仅使用硬盘做持久性的存储;它的数据模型非常独特,用的是单线程。另一个大区别在于,你可以在开发环境中使用Redis的功能,但却不需要转到Redis。

转向Redis当然也是可取的,许多开发者从一开始就把Redis作为首选数据库;但设想如果你的开发环境已经搭建好,应用已经在上面运行了,那么更换数据库框架显然不那么容易。另外在一些需要大容量数据集的应用,Redis也并不适合,因为它的数据集不会超过系统可用的内存。所以如果你有大数据应用,而且主要是读取访问模式,那么Redis并不是正确的选择。

        然而我喜欢Redis的一点就是你可以把它融入到你的系统中来,这就能够解决很多问题,比如那些你现有的数据库处理起来感到缓慢的任务。这些你就可以通过Redis来进行优化,或者为应用创建些新的功能。在本文中,我就想探讨一些怎样将Redis加入到现有的环境中,并利用它的原语命令等功能来解决 传统环境中碰到的一些常见问题。在这些例子中,Redis都不是作为首选数据库。

1、显示最新的项目列表

下面这个语句常用来显示最新项目,随着数据多了,查询毫无疑问会越来越慢。

 

  1. SELECT * FROM foo WHERE ... ORDER BY time DESC LIMIT 10   

 

        在Web应用中,“列出最新的回复”之类的查询非常普遍,这通常会带来可扩展性问题。这令人沮丧,因为项目本来就是按这个顺序被创建的,但要输出这个顺序却不得不进行排序操作。

        类似的问题就可以用Redis来解决。比如说,我们的一个Web应用想要列出用户贴出的最新20条评论。在最新的评论边上我们有一个“显示全部”的链接,点击后就可以获得更多的评论。

        我们假设数据库中的每条评论都有一个唯一的递增的ID字段。

        我们可以使用分页来制作主页和评论页,使用Redis的模板,每次新评论发表时,我们会将它的ID添加到一个Redis列表:

 

  1. LPUSH latest.comments <ID>   

 

       我们将列表裁剪为指定长度,因此Redis只需要保存最新的5000条评论:

       LTRIM latest.comments 0 5000 

      每次我们需要获取最新评论的项目范围时,我们调用一个函数来完成(使用伪代码):

 

  1. FUNCTION get_latest_comments(start, num_items):  
  2.     id_list = redis.lrange("latest.comments",start,start+num_items - 1)  
  3.     IF id_list.length < num_items  
  4.         id_list = SQL_DB("SELECT ... ORDER BY time LIMIT ...")  
  5.     END  
  6.     RETURN id_list  
  7. END  

 

 

      这里我们做的很简单。在Redis中我们的最新ID使用了常驻缓存,这是一直更新的。但是我们做了限制不能超过5000个ID,因此我们的获取ID函数会一直询问Redis。只有在start/count参数超出了这个范围的时候,才需要去访问数据库。

        我们的系统不会像传统方式那样“刷新”缓存,Redis实例中的信息永远是一致的。SQL数据库(或是硬盘上的其他类型数据库)只是在用户需要获取“很远”的数据时才会被触发,而主页或第一个评论页是不会麻烦到硬盘上的数据库了。

2、删除与过滤

      我们可以使用LREM来删除评论。如果删除操作非常少,另一个选择是直接跳过评论条目的入口,报告说该评论已经不存在。

       有些时候你想要给不同的列表附加上不同的过滤器。如果过滤器的数量受到限制,你可以简单的为每个不同的过滤器使用不同的Redis列表。毕竟每个列表只有5000条项目,但Redis却能够使用非常少的内存来处理几百万条项目。

3、排行榜相关

      另一个很普遍的需求是各种数据库的数据并非存储在内存中,因此在按得分排序以及实时更新这些几乎每秒钟都需要更新的功能上数据库的性能不够理想。

      典型的比如那些在线游戏的排行榜,比如一个Facebook的游戏,根据得分你通常想要:

         - 列出前100名高分选手

         - 列出某用户当前的全球排名

      这些操作对于Redis来说小菜一碟,即使你有几百万个用户,每分钟都会有几百万个新的得分。

      模式是这样的,每次获得新得分时,我们用这样的代码:

      ZADD leaderboard  <score>  <username> 

     你可能用userID来取代username,这取决于你是怎么设计的。

      得到前100名高分用户很简单:ZREVRANGE leaderboard 0 99。

      用户的全球排名也相似,只需要:ZRANK leaderboard <username>。

 

4、按照用户投票和时间排序

      排行榜的一种常见变体模式就像Reddit或Hacker News用的那样,新闻按照类似下面的公式根据得分来排序:

       score = points / time^alpha 

      因此用户的投票会相应的把新闻挖出来,但时间会按照一定的指数将新闻埋下去。下面是我们的模式,当然算法由你决定。

      模式是这样的,开始时先观察那些可能是最新的项目,例如首页上的1000条新闻都是候选者,因此我们先忽视掉其他的,这实现起来很简单。

      每次新的新闻贴上来后,我们将ID添加到列表中,使用LPUSH + LTRIM,确保只取出最新的1000条项目。

      有一项后台任务获取这个列表,并且持续的计算这1000条新闻中每条新闻的最终得分。计算结果由ZADD命令按照新的顺序填充生成列表,老新闻则被清除。这里的关键思路是排序工作是由后台任务来完成的。

 

5、处理过期项目

      另一种常用的项目排序是按照时间排序。我们使用unix时间作为得分即可。

      模式如下:

       - 每次有新项目添加到我们的非Redis数据库时,我们把它加入到排序集合中。这时我们用的是时间属性,current_time和time_to_live。

       - 另一项后台任务使用ZRANGE…SCORES查询排序集合,取出最新的10个项目。如果发现unix时间已经过期,则在数据库中删除条目。

 

6、计数

       Redis是一个很好的计数器,这要感谢INCRBY和其他相似命令。

       我相信你曾许多次想要给数据库加上新的计数器,用来获取统计或显示新信息,但是最后却由于写入敏感而不得不放弃它们。

       好了,现在使用Redis就不需要再担心了。有了原子递增(atomic increment),你可以放心的加上各种计数,用GETSET重置,或者是让它们过期。

       例如这样操作:

         INCR user:<id> EXPIRE 

         user:<id> 60 

       你可以计算出最近用户在页面间停顿不超过60秒的页面浏览量,当计数达到比如20时,就可以显示出某些条幅提示,或是其它你想显示的东西。

7、特定时间内的特定项目

        另一项对于其他数据库很难,但Redis做起来却轻而易举的事就是统计在某段特点时间里有多少特定用户访问了某个特定资源。比如我想要知道某些特定的注册用户或IP地址,他们到底有多少访问了某篇文章。

      每次我获得一次新的页面浏览时我只需要这样做:

       SADD page:day1:<page_id> <user_id> 

      当然你可能想用unix时间替换day1,比如time()-(time()%3600*24)等等。

      想知道特定用户的数量吗?只需要使用SCARD page:day1:<page_id>。

       需要测试某个特定用户是否访问了这个页面?SISMEMBER page:day1:<page_id>。

 

8、实时分析正在发生的情况,用于数据统计与防止垃圾邮件等

        我们只做了几个例子,但如果你研究Redis的命令集,并且组合一下,就能获得大量的实时分析方法,有效而且非常省力。使用Redis原语命令,更容易实施垃圾邮件过滤系统或其他实时跟踪系统。

 

9、Pub/Sub

       Redis的Pub/Sub非常非常简单,运行稳定并且快速。支持模式匹配,能够实时订阅与取消频道。

10、队列

        你应该已经注意到像list push和list pop这样的Redis命令能够很方便的执行队列操作了,但能做的可不止这些:比如Redis还有list pop的变体命令,能够在列表为空时阻塞队列。

       现代的互联网应用大量地使用了消息队列(Messaging)。消息队列不仅被用于系统内部组件之间的通信,同时也被用于系统跟其它服务之间的交互。消息队列的使用可以增加系统的可扩展性、灵活性和用户体验。非基于消息队列的系统,其运行速度取决于系统中最慢的组件的速度(注:短板效应)。而基于消息队列可以将系统中各组件解除耦合,这样系统就不再受最慢组件的束缚,各组件可以异步运行从而得以更快的速度完成各自的工作。

    此外,当服务器处在高并发操作的时候,比如频繁地写入日志文件。可以利用消息队列实现异步处理。从而实现高性能的并发操作。

 

11、缓存

        Redis的缓存部分值得写一篇新文章,我这里只是简单的说一下。Redis能够替代memcached,让你的缓存从只能存储数据变得能够更新数据,因此你不再需要每次都重新生成数据了。

此部分内容的原文地址:http://antirez.com/post/take-advantage-of-redis-adding-it-to-your-stack.html

 

5.  国内外三个不同领域巨头分享的Redis实战经验及使用场景

 

   

     随着应用对高性能需求的增加,NoSQL逐渐在各大名企的系统架构中生根发芽。这里我们将为大家分享社交巨头新浪微博、传媒巨头Viacom及图片分享领域佼佼者Pinterest带来的Redis实践,首先我们看新浪微博 @启盼cobain的Redis实战经验分享:

一、新浪微博:史上最大的Redis集群

Tape is Dead,Disk is Tape,Flash is Disk,RAM Locality is King. — Jim Gray

Redis不是比较成熟的memcache或者Mysql的替代品,是对于大型互联网类应用在架构上很好的补充。现在有越来越多的应用也在纷纷基于Redis做架构的改造。首先简单公布一下Redis平台实际情况:

 

  • 2200+亿 commands/day 5000亿Read/day 500亿Write/day
  • 18TB+ Memory
  • 500+ Servers in 6 IDC 2000+instances

 

应该是国内外比较大的Redis使用平台,今天主要从应用角度谈谈Redis服务平台。

Redis使用场景

1.Counting(计数)

计数的应用在另外一篇文章里较详细的描述,计数场景的优化 http://www.xdata.me/?p=262这里就不多加描述了。

可以预见的是,有很多同学认为把计数全部存在内存中成本非常高,我在这里用个图表来表达下我的观点:

很多情况大家都会设想纯使用内存的方案会很有很高成本,但实际情况往往会有一些不一样:

 

  • COST,对于有一定吞吐需求的应用来说,肯定会单独申请DB、Cache资源,很多担心DB写入性能的同学还会主动将DB更新记入异步队列,而这三块的资源的利用率一般都不会太高。资源算下来,你惊异的发现:反而纯内存的方案会更精简!
  • KISS原则,这对于开发是非常友好的,我只需要建立一套连接池,不用担心数据一致性的维护,不用维护异步队列。
  • Cache穿透风险,如果后端使用DB,肯定不会提供很高的吞吐能力,cache宕机如果没有妥善处理,那就悲剧了。
  • 大多数的起始存储需求,容量较小。

 

2.Reverse cache(反向cache)

面对微博常常出现的热点,如最近出现了较为火爆的短链,短时间有数以万计的人点击、跳转,而这里会常常涌现一些需求,比如我们向快速在跳转时判定用户等级,是否有一些账号绑定,性别爱好什么的,已给其展示不同的内容或者信息。

普通采用memcache+Mysql的解决方案,当调用id合法的情况下,可支撑较大的吞吐。但当调用id不可控,有较多垃圾用户调用时,由于memcache未有命中,会大量的穿透至Mysql服务器,瞬间造成连接数疯长,整体吞吐量降低,响应时间变慢。

这里我们可以用redis记录全量的用户判定信息,如string key:uid int:type,做一次反向的cache,当用户在redis快速获取自己等级等信息后,再去Mc+Mysql层去获取全量信息。如图:

当然这也不是最优化的场景,如用Redis做bloomfilter,可能更加省用内存。

3.Top 10 list

产品运营总会让你展示最近、最热、点击率最高、活跃度最高等等条件的top list。很多更新较频繁的列表如果使用MC+MySQL维护的话缓存失效的可能性会比较大,鉴于占用内存较小的情况,使用Redis做存储也是相当不错的。

4.Last Index

用户最近访问记录也是redis list的很好应用场景,lpush lpop自动过期老的登陆记录,对于开发来说还是非常友好的。

5.Relation List/Message Queue

这里把两个功能放在最后,因为这两个功能在现实问题当中遇到了一些困难,但在一定阶段也确实解决了我们很多的问题,故在这里只做说明。

Message Queue就是通过list的lpop及lpush接口进行队列的写入和消费,由于本身性能较好也能解决大部分问题。

6.Fast transaction with Lua

Redis 的Lua的功能扩展实际给Redis带来了更多的应用场景,你可以编写若干command组合作为一个小型的非阻塞事务或者更新逻辑,如:在收到message推送时,同时1.给自己的增加一个未读的对话 2.给自己的私信增加一个未读消息 3.最后给发送人回执一个完成推送消息,这一层逻辑完全可以在Redis Server端实现。

但是,需要注意的是Redis会将lua script的全部内容记录在aof和传送给slave,这也将是对磁盘,网卡一个不小的开销。

7.Instead of Memcache

 

  1. 很多测试和应用均已证明,
  2. 在性能方面Redis并没有落后memcache多少,而单线程的模型给Redis反而带来了很强的扩展性。
  3. 在很多场景下,Redis对同一份数据的内存开销是小于memcache的slab分配的。
  4. Redis提供的数据同步功能,其实是对cache的一个强有力功能扩展。

 

Redis使用的重要点

1.rdb/aof Backup!

我们线上的Redis 95%以上是承担后端存储功能的,我们不仅用作cache,而更为一种k-v存储,他完全替代了后端的存储服务(MySQL),故其数据是非常重要的,如果出现数据污染和丢失,误操作等情况,将是难以恢复的。所以备份是非常必要的!为此,我们有共享的hdfs资源作为我们的备份池,希望能随时可以还原业务所需数据。

2.Small item & Small instance!

由于Redis单线程(严格意义上不是单线程,但认为对request的处理是单线程的)的模型,大的数据结构list,sorted set,hash set的批量处理就意味着其他请求的等待,故使用Redis的复杂数据结构一定要控制其单key-struct的大小。

另外,Redis单实例的内存容量也应该有严格的限制。单实例内存容量较大后,直接带来的问题就是故障恢复或者Rebuild从库的时候时间较长,而更糟糕的是,Redis rewrite aof和save rdb时,将会带来非常大且长的系统压力,并占用额外内存,很可能导致系统内存不足等严重影响性能的线上故障。我们线上96G/128G内存服务器不建议单实例容量大于20/30G。

3.Been Available!

业界资料和使用比较多的是Redis sentinel(哨兵)

http://www.huangz.me/en/latest/storage/redis_code_analysis/sentinel.html

http://qiita.com/wellflat/items/8935016fdee25d4866d9

2000行C实现了服务器状态检测,自动故障转移等功能。

但由于自身实际架构往往会复杂,或者考虑的角度比较多,为此 @许琦eryk和我一同做了hypnos项目。

hypnos是神话中的睡神,字面意思也是希望我们工程师无需在休息时间处理任何故障。:-)

其工作原理示意如下:

Talk is cheap, show me your code! 稍后将单独写篇博客细致讲下Hypnos的实现。

4.In Memory or not?

发现一种情况,开发在沟通后端资源设计的时候,常常因为习惯使用和错误了解产品定位等原因,而忽视了对真实使用用户的评估。也许这是一份历史数据,只有最近一天的数据才有人进行访问,而把历史数据的容量和最近一天请求量都抛给内存类的存储现实是非常不合理的。

所以当你在究竟使用什么样的数据结构存储的时候,请务必先进行成本衡量,有多少数据是需要存储在内存中的?有多少数据是对用户真正有意义的。因为这其实对后端资源的设计是至关重要的,1G的数据容量和1T的数据容量对于设计思路是完全不一样的

Plans in future?

1.slave sync改造

全部改造线上master-slave数据同步机制,这一点我们借鉴了MySQL Replication的思路,使用rdb+aof+pos作为数据同步的依据,这里简要说明为什么官方提供的psync没有很好的满足我们的需求:

假设A有两个从库B及C,及 A `— B&C,这时我们发现master A服务器有宕机隐患需要重启或者A节点直接宕机,需要切换B为新的主库,如果A、B、C不共享rdb及aof信息,C在作为B的从库时,仍会清除自身数据,因为C节点只记录了和A节点的同步状况。

故我们需要有一种将A`–B&C 结构切换切换为A`–B`–C结构的同步机制,psync虽然支持断点续传,但仍无法支持master故障的平滑切换。

实际上我们已经在我们定制的Redis计数服务上使用了如上功能的同步,效果非常好,解决了运维负担,但仍需向所有Redis服务推广,如果可能我们也会向官方Redis提出相关sync slave的改进。

2.更适合redis的name-system Or proxy

细心的同学发现我们除了使用DNS作为命名系统,也在zookeeper中有一份记录,为什么不让用户直接访问一个系统,zk或者DNS选择其一呢?

其实还是很简单,命名系统是个非常重要的组件,而dns是一套比较完善的命名系统,我们为此做了很多改进和试错,zk的实现还是相对复杂,我们还没有较强的把控粒度。我们也在思考用什么做命名系统更符合我们需求。

3.后端数据存储

大内存的使用肯定是一个重要的成本优化方向,flash盘及分布式的存储也在我们未来计划之中。(原文链接: Largest Redis Clusters Ever

二、Pinterest:Reids维护上百亿的相关性

      Pinterest已经成为硅谷最疯故事之一,在2012年,他们基于PC的业务增加1047%,移动端采用增加1698%, 该年3月其独立访问数量更飙升至533亿。在Pinterest,人们关注的事物以百亿记——每个用户界面都会查询某个board或者是用户是否关注的行为促成了异常复杂的工程问题。这也让Redis获得了用武之地。经过数年的发展,Pinterest已经成为媒体、社交等多个领域的佼佼者,其辉煌战绩如下:

 

  • 获得的推荐流量高于Google+、YouTube及LinkedIn三者的总和
  • 与Facebook及Twitter一起成为最流行的三大社交网络
  • 参考Pinterest进行购买的用户比其它网站更高( 更多详情

 

如您所想,基于其独立访问数,Pinterest的高规模促成了一个非常高的IT基础设施需求。

 

通过缓存来优化用户体验

近日,Pinterest工程经理Abhi Khune对其公司的用户体验需求及Redis的使用经验 进行了分享。即使是滋生的应用程序打造者,在分析网站的细节之前也不会理解这些特性,因此先大致的理解一下使用场景:首先,为每个粉丝进行提及到的预检查;其次,UI将准确的显示用户的粉丝及关注列表分页。高效的执行这些操作,每次点击都需要非常高的性能架构。

不能免俗,Pinterest的软件工程师及架构师已经使用了MySQL及memcache,但是缓存解决方案仍然达到了他们的瓶颈;因此为了拥有更好的用户体验,缓存必须被扩充。而在实际操作过程中,工程团队已然发现缓存只有当用户sub-graph已经在缓存中时才会起到作用。因此。任何使用这个系统的人都需要被缓存,这就导致了整个图的缓存。同时,最常见的查询“用户A是否关注了用户B”的答案经常是否定的,然而这却被作为了缓存丢失,从而促成一个数据库查询,因此他们需要一个新的方法来扩展缓存。最终,他们团队决定使用Redis来存储整个图,用以服务众多的列表。

使用Redis存储大量的Pinterest列表

Pinterest使用了Redis作为解决方案,并将性能推至了内存数据库等级,为用户保存多种类型列表:

 

  • 关注者列表
  • 你所关注的board列表
  • 粉丝列表
  • 关注你board的用户列表
  • 某个用户中board中你没有关注的列表
  • 每个board的关注者及非关注者

 

Redis为其7000万用户存储了以上的所有列表,本质上讲可以说是储存了所有粉丝图,通过用户ID分片。鉴于你可以通过类型来查看以上列表的数据,分析概要信息被用看起来更像事务的系统储存及访问。Pinterest当下的用户like被限制为10万,初略进行统计:如果每个用户关注25个board,将会在用户及board间产生17.5亿的关系。同时更加重要的是,这些关系随着系统的使用每天都会增加。

Pinterest的Reids架构及运营

通过Pinterest的一个创始人了解到,Pinterest开始使用Python及订制的Django编写应用程序,并一直持续到其拥有1800万用户级日410TB用户数据的时候。虽然使用了多个存储对数据进行储存,工程师根据用户id使用了8192个虚拟分片,每个分片都运行在一个Redis DB之上,同时1个Redis实例将运行多个Redis DB。为了对CPU核心的充分使用,同一台主机上同时使用多线程和单线程Redis实例。

鉴于整个数据集运行在内存当中,Redis在Amazon EBS上对每秒传输进来的写入都会进行持久化。扩展主要通过两个方面进行:第一,保持50%的利用率,通过主从转换,机器上运行的Redis实例一半会转译到一个新机器上;第二,扩展节点和分片。整个Redis集群都会使用一个主从配置,从部分将被当做一个热备份。一旦主节点失败,从部分会立刻完成主的转换,同时一个新的从部分将会被添加,ZooKeeper将完成整个过程。同时他们每个小时都会在Amazon S3上运行BGsave做更持久的储存——这项Reids操作会在后端进行,之后Pinterest会使用这些数据做MapReduce和分析作业。(更多内容见原文)

三、Viacom:Redis在系统中的用例盘点

Viacom是全球最大的传媒集体之一,同时也遭遇了当下最大的数据难题之一:如何处理日益剧增的动态视频内容。

着眼这一挑战的上升趋势,我们会发现:2010年世界上所有数据体积达到ZB级,而单单2012这一年,互联网产生的数据就增加了2.8个ZB,其中大部分的数据都是非结构化的,包括了视频和图片。

覆盖MVN(以前称为MTV Networks、Paramount及BET),Viacom是个名副其实的传媒巨头,支持众多人气站点,其中包括The Daily Show、osh.0、South Park Studios、GameTrailers.com等。作为媒体公司,这些网站上的文档、图片、视频短片都在无时无刻的更新。长话短说,下面就进入Viacom高级架构师Michael Venezia 分享的Redis实践:

Viacom的网站架构背景

对于Viacom,横跨多个站点传播内容让必须专注于规模的需求,同时为了将内容竟可能快的传播到相应用户,他们还必须聚焦内容之间的关系。然而即使The Daily Show、Nickelodeon、Spike或者是VH1 这些单独的网站上,日平均PV都可以达到千万,峰值时流量更会达到平均值的20-30倍。同时基于对实时的需求,动态的规模及速度已成为架构的基础之一。

除去动态规模之外,服务还必须基于用户正在浏览的视频或者是地理位置来推测用户的喜好。比如说,某个页面可能会将一个独立的视频片段与本地的促销,视频系列的额外部分,甚至是相关视频联系起来。为了能让用户能在网站上停留更长的时间,他们建立了一个能基于详细元数据自动建立页面的软件引擎,这个引擎可以根据用户当下兴趣推荐额外的内容。鉴于用于兴趣的随时改变,数据的类型非常广泛——类似graph-like,实际上做的是大量的join。

这样做有利于减少类似视频的大体积文件副本数,比如数据存储中一个独立的记录是Southpark片段“Cartman gets an Anal Probe”,这个片段可能也会出现在德语的网站上。虽然视频是一样的,但是英语用户搜索的可能就是另一个不同的词语。元数据的副本转换成搜索结果,并指向相同的视频。因此在美国用户搜索真实标题的情况下,德国浏览者可能会使用转译的标题——德国网站上的“Cartman und die Analsonde”。

这些元数据覆盖了其它记录或者是对象,同时还可以根据使用环境来改变内容,通过不同的规则集来限制不同地理位置或者是设备请求的内容。

Viacom的实现方法

尽管许多机构通过使用ORM及传统关系型数据库来解决这个问题,Viacom却使用了一个迥然不同的方法。

本质上,他们完全承担不了对数据库的直接访问。首先,他们处理的大部分都是流数据,他们偏向于使用Akamai从地理上来分配内容。其次,基于页面的复杂性可能会取上万个对象。取如此多的数据显然会影响到性能,因此JSON在1个数据服务中投入了使用。当然,这些JSON对象的缓存将直接影响到网站性能。同时,当内容或者是内容之间的关系发生改变时,缓存还需要动态的进行更新。

Viacom依靠对象基元和超类解决这个问题,继续以South Park为例:一个私有的“episode”类包含了所有该片段相关信息,一个“super object”将有助于发现实际的视频对象。超类这个思想确实非常有益于建设低延迟页面的自动建设,这些超类可以帮助到基元对象到缓存的映射及保存。

Viacom为什么要使用Redis

每当Viacom上传一个视频片段,系统将建立一个私有的对象,并于1个超类关联。每一次修改,他们都需要重估私有对象的每个改变,并更新所有复合对象。同时,系统还需要无效Akamail中的URL请求。系统现有架构的组合及更敏捷的管理方法需求将Viacom推向了Redis。

基于Viacom主要基于PHP,所以这个解决方案必须支持PHP。他们首先选择了memcached做对象存储,但是它并不能很好的支持hashmap;同时他们还需要一个更有效的进行无效步骤的重估,即更好的理解内容的依赖性。本质上说,他们需要时刻跟进无效步骤中的依赖性改变。因此他们选择了Redis及Predis的组合来解决这个问题。

他们团队使用Redis给southparkstudios.com和thedailyshow.com两个网站建设依赖性图,在取得了很大的成功后他们开始着眼Redis其它适合场景。

Redis的其它使用场景

显而易见,如果有人使用Redis来建设依赖性图,那么使用它来做对象处理也是说得通的。同样,这也成了架构团队为Redis选择的第二使用场景。Redis的复制及持久化特性同时也征服了Viacom的运营团队,因此在几个开发周期后,Redis成为他们网站的主要数据及依赖性储存。

后两个用例则是行为追踪及浏览计数的缓冲,改变后的架构是Redis每几分钟向MySQL中储存一次,而浏览计数则通过Redis进行存储及计数。同时Redis还被用来做人气的计算,一个基于访问数及访问时间的得分系统——如果某个视频最近被访问的次数越多,它的人气就越高。在如此多内容上每隔10-15分钟做一次计算绝对不是类似MySQL这样传统关系型数据库的强项,Viacom使用Redis的理由也非常简单——在1个存储浏览信息的Redis实例上运行Lua批处理作业,计算出所有的得分表。信息被拷贝到另一个Redis实例上,用以支持相关的产品查询。同时还在MySQL上做了另一个备份,用以以后的分析,这种组合会将这个过程耗费的时间降低60倍。

Viacom还使用Redis存储一步作业信息,这些信息被插入一个列表中,工作人员则使用BLPOP命令行在队列中抓取顶端的任务。同时zsets被用于从众多社交网络(比如Twitter及Tumblr)上综合内容,Viacom通过Brightcove视频播放器来同步多个内容管理系统。

横跨这些用例,几乎所有的Redis命令都被使用——sets、lists、zlists、hashmaps、scripts、counters等。同时,Redis也成为Viacom可扩展架构中不可或缺的一环。

redis 五种数据结构详解(string,list,set,zset,hash)

 Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。

String——字符串
Hash——字典
List——列表
Set——集合
Sorted Set——有序集合

下面我们就来简单说明一下它们各自的使用场景:

一、Redis String类型

string类型的数据存储是最简单的key-value存储;

1.string字符串读写实现方式:

public function testRedis(){         //string类型的数据结构         app()->redis->set('1', 'aa');
    //根据key取出value值
    $string = app()->redis->get('1');
    }

2.redis客户端查看结果:

3.string字符串的其他redis操作方法:

//普通set/get操作
$redis->set('library', 'predis');
$retval = $redis->get('library');
echo $retval; //显示 'predis'

//setex set一个存储时效
$redis->setex('str', 10, 'bar'); //表示存储有效期为10秒

//setnx/msetnx相当于add操作,不会覆盖已有值
$redis->setnx('foo',12); //true
$redis->setnx('foo',34); //false

//getset操作,set的变种,结果返回替换前的值
$redis->getset('foo',56);//返回34

// incrby/incr/decrby/decr 对值的递增和递减
$redis->incr('foo'); //foo为57
$redis->incrby('foo',2); //foo为59

//exists检测是否存在某值
$redis->exists('foo');//true

//del 删除
$redis->del('foo');//true

//type 类型检测,字符串返回string,列表返回 list,set表返回set/zset,hash表返回hash
$redis->type('foo');//不存在,返回none
$redis->set('str','test');
$redis->type('str'); //字符串,返回string

//append 连接到已存在字符串
$redis->append('str','_123'); //返回累加后的字符串长度8,此进str为 'test_123'

//setrange 部分替换操作
$redis->setrange('str',0,'abc'); //返回3,参数2为0时等同于set操作
$redis->setrange('str',2,'cd');//返回4,表示从第2个字符后替换,这时'str'为'abcd'

//substr 部分获取操作
$redis->substr('str',0,2);//表示从第0个起,取到第2个字符,共3个,返回'abc'

//strlen 获取字符串长度
$redis->strlen('str'); //返回4

//setbit/getbit 位存储和获取
$redis->setbit('binary',31,1);&nbsp; //表示在第31位存入1,这边可能会有大小端问题?不过没关系,getbit 应该不会有问题
$redis->getbit('binary',31);&nbsp;&nbsp;&nbsp; //返回1

//keys 模糊查找功能,支持*号以及?号(匹配一个字符)
$redis->set('foo1',123);
$redis->set('foo2',456);
$redis->keys('foo*'); //返回foo1和foo2的array
$redis->keys('f?o?');&nbsp; //同上

//randomkey 随机返回一个key
$redis->randomkey(); //可能是返回 'foo1'或者是'foo2'及其它任何一存在redis的key

//rename/renamenx 对key进行改名,所不同的是renamenx不允许改成已存在的key
$redis->rename('str','str2'); //把原先命名为'str'的key改成了'str2'

//expire 设置key-value的时效性,ttl 获取剩余有效期,persist 重新设置为永久存储
$redis->expire('foo', 1); //设置有效期为1秒
$redis->ttl('foo'); //返回有效期值1s
$redis->expire('foo'); //取消expire行为

//dbsize 返回redis当前数据库的记录总数
$redis->dbsize();

二、Redis Hash表

 redis中hash表存储数据,比较类似数据库中表的一条记录;

1.hash读写实现方式:

复制代码
public function testRedis()     {//存储 hash类型 数据结构         app()->redis->hset('goods', 'apple', '苹果');          //取出 hash表中的数据         $hash = app()->redis->hget('goods', 'apple');         print_r($hash);         echo "\n";                  die();     }
复制代码

2.redis客户端查看结果:

 

 3.关于hash表其他redis操作方法:

//hset/hget 存取hash表的数据
$redis->hset('hash1','key1','v1'); //将key为'key1' value为'v1'的元素存入hash1表
$redis->hset('hash1','key2','v2');
$redis->hget('hash1','key1');&nbsp; //取出表'hash1'中的key 'key1'的值,返回'v1'

//hexists 返回hash表中的指定key是否存在
$redis->hexists ('hash1','key1'); //true or false

//hdel 删除hash表中指定key的元素
$redis->hdel('hash1','key2'); //true or false

//hlen 返回hash表元素个数
$redis->hlen('hash1'); //1

//hsetnx 增加一个元素,但不能重复
$redis->hsetnx('hash1','key1','v2'); //false
$redis->hsetnx('hash1','key2','v2'); //true

//hmset/hmget 存取多个元素到hash表
$redis->hmset('hash1',array('key3'=>'v3','key4'=>'v4'));
$redis->hmget('hash1',array('key3','key4')); //返回相应的值 array('v3','v4')

//hincrby 对指定key进行累加
$redis->hincrby('hash1','key5',3); //返回3
$redis->hincrby('hash1','key5',10); //返回13

//hkeys 返回hash表中的所有key
$redis->hkeys('hash1'); //返回array('key1','key2','key3','key4','key5')

//hvals 返回hash表中的所有value
$redis->hvals('hash1'); //返回array('v1','v2','v3','v4',13)

//hgetall 返回整个hash表元素
$redis->hgetall('hash1'); //返回array('key1'=>'v1','key2'=>'v2','key3'=>'v3','key4'=>'v4','key5'=>13)

三、Redis list列表

List数据结构是链表结构,是双向的,可以在链表左,右两边分别操作;

也可以把list看成一种队列,所以在很多时候可以用redis用作消息队列,这个时候它的作用就类似于activeMq啦;

应用案例有时间轴数据,评论列表,消息传递等等,它可以提供简便的分页,读写操作。

1.list 读写实现方式:

复制代码
public function testRedis()     {//存储 list         app()->redis->lpush('news', 'cc'); //从队列前面插入元素         app()->redis->lpush('news', 'ee'); //从队列前面插入元素         app()->redis->rpush('news', 'dd');//从队列后面插入元素         $list = app()->redis->lrange('news', 0, -1);//取出list所有元素         print_r($list);         echo "\n";          die();     }
复制代码

2.在redis客户端中直观查看list的存储结果:

3.list列表的其他redis操作方法:

//rpush/rpushx 有序列表操作,从队列后插入元素
//lpush/lpushx 和rpush/rpushx的区别是插入到队列的头部,同上,'x'含义是只对已存在的key进行操作
$redis->rpush('fooList', 'bar1'); //返回一个列表的长度1
$redis->lpush('fooList', 'bar0'); //返回一个列表的长度2
$redis->rpushx('fooList', 'bar2'); //返回3,rpushx只对已存在的队列做添加,否则返回0
//llen返回当前列表长度
$redis->llen('fooList');//3

//lrange 返回队列中一个区间的元素
$redis->lrange('fooList',0,1); //返回数组包含第0个至第1个共2个元素
$redis->lrange('fooList',0,-1);//返回第0个至倒数第一个,相当于返回所有元素,注意redis中很多时候会用到负数,下同

//lindex 返回指定顺序位置的list元素
$redis->lindex('fooList',1); //返回'bar1'

//lset 修改队列中指定位置的value
$redis->lset('fooList',1,'123');//修改位置1的元素,返回true

//lrem 删除队列中左起指定数量的字符
$redis->lrem('fooList',1,'_'); //删除队列中左起(右起使用-1)1个字符'_'(若有)

//lpop/rpop 类似栈结构地弹出(并删除)最左或最右的一个元素
$redis->lpop('fooList'); //'bar0'
$redis->rpop('fooList'); //'bar2'

//ltrim 队列修改,保留左边起若干元素,其余删除
$redis->ltrim('fooList', 0,1); //保留左边起第0个至第1个元素

//rpoplpush 从一个队列中pop出元素并push到另一个队列
$redis->rpush('list1','ab0');
$redis->rpush('list1','ab1');
$redis->rpush('list2','ab2');
$redis->rpush('list2','ab3');
$redis->rpoplpush('list1','list2');//结果list1 =>array('ab0'),list2 =>array('ab1','ab2','ab3')
$redis->rpoplpush('list2','list2');//也适用于同一个队列,把最后一个元素移到头部list2 =>array('ab3','ab1','ab2')

//linsert 在队列的中间指定元素前或后插入元素
$redis->linsert('list2', 'before','ab1','123'); //表示在元素'ab1'之前插入'123'
$redis->linsert('list2', 'after','ab1','456');&nbsp;&nbsp; //表示在元素'ab1'之后插入'456'

//blpop/brpop 阻塞并等待一个列队不为空时,再pop出最左或最右的一个元素(这个功能在php以外可以说非常好用)
//brpoplpush 同样是阻塞并等待操作,结果同rpoplpush一样
$redis->blpop('list3',10); //如果list3为空则一直等待,直到不为空时将第一元素弹出,10秒后超时;

四、Redis Set集合

Set 就是一个集合,集合的概念就是一堆不重复值的组合。利用 Redis 提供的 Set 数据结构,可以存储一些集合性的数据。

比如在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。

因为 Redis 非常人性化的为集合提供了求交集、并集、差集等操作,那么就可以非常方便的实现如共同关注、共同喜好、二度好友等功能,对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集合中。

1.共同好友、二度好友
2.利用唯一性,可以统计访问网站的所有独立 IP
3.好友推荐的时候,根据 tag 求交集,大于某个 threshold 就可以推荐

1.set 读写实现方式:

复制代码
public function testRedis()     {//存储 set         $fans = app()->redis->sadd('fans', 'ff');         if($fans){             print_r('set add ff success');         }else{             print_r('set add ff fail');         }         $fans = app()->redis->sadd('fans', 'gg'); //不存在返回true         if($fans){             print_r('set add gg success');         }else{             print_r('set add gg fail');         }         $fans = app()->redis->sadd('fans', 'gg'); //不存在返回false         if($fans){             print_r('set add gg success');         }else{             print_r('set add gg fail');         }         //取出set         $fans = app()->redis->smembers('fans');         print_r($fans);         echo "\n";     }
复制代码

2.在redis客户端中直观查看set集合的存储结果:

3.set集合的其他redis操作方法:

//sadd 增加元素,返回true,重复返回false
$redis->sadd('set1','ab');
$redis->sadd('set1','cd');
$redis->sadd('set1','ef');

//srem 移除指定元素
$redis->srem('set1','cd'); //删除'cd'元素

//spop 弹出首元素
$redis->spop('set1');

//smove 移动当前set表的指定元素到另一个set表
$redis->sadd('set2','123');
$redis->smove('set1','set2','ab');//移动'set1'中的'ab'到'set2',返回true or false

//scard 返回当前set表元素个数
$redis->scard('set2');//2

//sismember 判断元素是否属于当前表
$redis->sismember('set2','123'); //true or false

//smembers 返回当前表的所有元素
$redis->smembers('set2'); //array('123','ab');

//sinter/sunion/sdiff&nbsp; 返回两个表中元素的交集/并集/补集
$redis->sadd('set1','ab');
$redis->sinter('set2','set1'); //返回array('ab')

//sinterstore/sunionstore/sdiffstore 将两个表交集/并集/补集元素copy到第三个表中
$redis->set('foo',0);
$redis->sinterstore('foo','set1'); //这边等同于将'set1'的内容copy到'foo'中,并将'foo'转为set表
$redis->sinterstore('foo',array('set1','set2')); //将'set1'和'set2'中相同的元素copy到'foo'表中,覆盖'foo'原有内容

//srandmember 返回表中一个随机元素
$redis->srandmember('set1');

五、Redis Zset集合(Sorted Sets)

zset是set的一个升级版本,他在set的基础上增加了一个顺序属性,这一属性在添加修改元素的时候可以指定,每次指定后,zset会自动重新按新的值调整顺序。 可以对指定键的值进行排序权重的设定,它应用排名模块比较多。

比如一个存储全班同学成绩的 Sorted Sets,其集合 value 可以是同学的学号,而 score 就可以是其考试得分,这样在数据插入集合的时候,就已经进行了天然的排序。另外还可以用 Sorted Sets 来做带权重的队列,比如普通消息的 score 为1,重要消息的 score 为2,然后工作线程可以选择按 score 的倒序来获取工作任务,让重要的任务优先执行。

zset集合可以完成有序执行、按照优先级执行的情况;

1.zset 读写实现方式:

复制代码
public function testRedis()     {//zset 添加元素         app()->redis->zadd('students', '1', '90');         app()->redis->zadd('students', '2', '80');         app()->redis->zadd('students', '3', '95');         app()->redis->zadd('students', '7', '75');         app()->redis->zadd('students', '5', '55');         //取出 zset         $zset = app()->redis->zrange('students', 0, -1);         print_r($zset);         echo "\n";     }
复制代码

2.在redis客户端中直观查看zset集合的存储结果:

3.zset集合的其他redis操作方法:

//sadd 增加元素,并设置序号,返回true,重复返回false
$redis->zadd('zset1',1,'ab');
$redis->zadd('zset1',2,'cd');
$redis->zadd('zset1',3,'ef');

//zincrby 对指定元素索引值的增减,改变元素排列次序
$redis->zincrby('zset1',10,'ab');//返回11

//zrem 移除指定元素
$redis->zrem('zset1','ef'); //true or false

//zrange 按位置次序返回表中指定区间的元素
$redis->zrange('zset1',0,1); //返回位置0和1之间(两个)的元素
$redis->zrange('zset1',0,-1);//返回位置0和倒数第一个元素之间的元素(相当于所有元素)

//zrevrange 同上,返回表中指定区间的元素,按次序倒排
$redis->zrevrange('zset1',0,-1); //元素顺序和zrange相反

//zrangebyscore/zrevrangebyscore 按顺序/降序返回表中指定索引区间的元素
$redis->zadd('zset1',3,'ef');
$redis->zadd('zset1',5,'gh');
$redis->zrangebyscore('zset1',2,9); //返回索引值2-9之间的元素 array('ef','gh')
//参数形式
$redis->zrangebyscore('zset1',2,9,'withscores'); //返回索引值2-9之间的元素并包含索引值 array(array('ef',3),array('gh',5))
$redis->zrangebyscore('zset1',2,9,array('withscores' =>true,'limit'=>array(1, 2))); //返回索引值2-9之间的元素,'withscores' =>true表示包含索引值; 'limit'=>array(1, 2),表示最多返回2条,结果为array(array('ef',3),array('gh',5))

//zunionstore/zinterstore 将多个表的并集/交集存入另一个表中
$redis->zunionstore('zset3',array('zset1','zset2','zset0')); //将'zset1','zset2','zset0'的并集存入'zset3'
//其它参数
$redis->zunionstore('zset3',array('zset1','zset2'),array('weights' => array(5,0)));//weights参数表示权重,其中表示并集后值大于5的元素排在前,大于0的排在后
$redis->zunionstore('zset3',array('zset1','zset2'),array('aggregate' => 'max'));//'aggregate' => 'max'或'min'表示并集后相同的元素是取大值或是取小值

//zcount 统计一个索引区间的元素个数
$redis->zcount('zset1',3,5);//2
$redis->zcount('zset1','(3',5)); //'(3'表示索引值在3-5之间但不含3,同理也可以使用'(5'表示上限为5但不含5

//zcard 统计元素个数
$redis->zcard('zset1');//4

//zscore 查询元素的索引
$redis->zscore('zset1','ef');//3

//zremrangebyscore 删除一个索引区间的元素
$redis->zremrangebyscore('zset1',0,2); //删除索引在0-2之间的元素('ab','cd'),返回删除元素个数2

//zrank/zrevrank 返回元素所在表顺序/降序的位置(不是索引)
$redis->zrank('zset1','ef');//返回0,因为它是第一个元素;zrevrank则返回1(最后一个)

//zremrangebyrank 删除表中指定位置区间的元素
$redis->zremrangebyrank('zset1',0,10); //删除位置为0-10的元素,返回删除的元素个数2

Redis作者谈Redis应用场景(转)

 https://www.cnblogs.com/ajianbeyourself/p/4395146.html

Records:17123