本文共 54443 字,大约阅读时间需要 181 分钟。
目录
REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统,是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库。一款高性能的NOSQL系列的非关系型数据库,非关系型数据库还有MongoDB,Membase等。关系型有 mysql 和oracle等。c语言开发。
特征:
基于内存的、非关系型的缓存和数据库,用的是epoll机制。每次客户端调用都经历了发送命令、执行命令、返回结果三个过程,所有命令都会进入一个队列中。
work线程是单线程,相对于mysql的串行化数据隔离级别相比,中间加一层redis 相对会变快,比如下面的从c1和c2客户端两个客户端同事请求数据,两者是串行的。
新版:增加了多线程的io操作(io threads),就是把read和write放在多线程处理,而redis的work线程只是负责计算操作来保证顺序。
二进制安全的:不会改变你的数值,你要提供字节数组,这在设置key value的时候redis自己做了。
弊端:单线程工作,不能发挥硬件优势,对于一些冷数据(热数据是指频繁操作的数据),如果放在内存中基本上是对内存的一种浪费。单线程会有一个问题:对于每个命令的执行时间是有要求的。如果 某个命令执行过长,会造成其他命令的阻塞,对于Redis这种高性能的服务 来说是致命的,所以Redis是面向快速执行场景的数据库。
那么为什么Redis使用单线程模型会达到每秒万级别的处理能力呢?可以将其归结为三点:
第一,纯内存访问,Redis将所有数据放在内存中,内存的响应时长大 约为100纳秒,这是Redis达到每秒万级别访问的重要基础。第二,非阻塞I/O,Redis使用epoll作为I/O多路复用技术的实现,再加上 Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。
第三,单线程避免了线程切换和竞态产生的消耗。
与memcached相比:
memcached的值为String, 可以通过Json来序列化反序列化对象。数据向计算移动(就是把整个数据取出来发送过去,然后解析自己想要的)。memcache服务器在编译安装的时候默认是单线程应用,如果你想变成支持多线程的,那么编译的时候指定./configure --enable-threads就可以编译成支撑多线程的应用程序了。主要是为了现在的多核系统准备的。但是官方文档也明说了,你的机器必须支持多线程编程,否则还是用不了那个memcache的多线程模式
Redis有5种数据类型,而且还有本地方法,比如list的index方法,不用返回整个数据,导致IO减小,计算向数据移动(先解析想要的,然后把需要的一小部分数据发送过去),redis的计算发生在redis内部。
关系型数据库与非关系型数据库对比:两者互补,通常情况下,使用关系型数据库,在合适的时候使用Nosql数据库,一般会将数据存储在关系型数据库,在Nosql数据库中备份存储关系型数据库的数据。
Redis使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
它通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Hash), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。
Redis 与其他 key - value 缓存产品有以下特点:
https://www.cnblogs.com/arxive/p/9301512.html
命令操作
https://www.redis.net.cn/tutorial/3509.html
1.Redis的数据结构:redis存储的是key,value格式的数据,其中key都是字符串,value有5中不同的数据结构
value的数据结构:object encoding key 查看key的编码
1)字符串类型string :value最大512M
set key value [ex seconds] [px milliseconds] [nx|xx]
·ex seconds:为键设置秒级过期时间。
·px milliseconds:为键设置毫秒级过期时间。 ·nx:键必须不存在,才可以设置成功,用于添加。 ·xx:与nx相反,键必须存在,才可以设置成功,用于更新。get key
getset key value 设置并返回原值
del key 成功返回1 失败返回0
STRLEN key 返回 是 value的字节数。字节数跟编码有关。
APPEND Key value 扩展value
bittop and key3 key1 key2 :二进制的与操作。把key1和key2按位与后的结果放在key3里。 or是或操作。
psetex key milliseconds value 以毫秒为单位设置过期时间
setex key seconds value ,将值 value 关联到 key ,并将 key 的过期时间设为 seconds (以秒为单位) ,不但是原子执行,同时减少了一次网络通讯的时间。
setnx 只有在 key 不存在时设置 key 的值
mset key value [key value …] 批量设置值 mset a 1 b 2 c 3 d 4
mget key [key …] 批量获取值 mget a b c d 如果有些键不存在,那么它的值为nil(空),mget 为 一次get多个 ,要比get n次 更有效率,但如果数量过多可能造成Redis阻塞或者网络拥塞。mget和mset虽然是批量的,但是是一个命令,是一个原子命令。
INCR 可以对 数值进行 数值操作
incr 会有三种情况
·值不是整数,返回错误。
·值是整数,返回自增后的结果。 ·键不存在,按照值为0自增,返回结果为1。Redis还提供了decr(自减)、incrby(自增指定数字)、 decrby(自减指定数字)、incrbyfloat(自增浮点数):
decr key ;incrby key increment ;decrby key decrement ;incrbyfloat key increment
setbit key offset value(0或1) 设置key的二进制从左往右偏移offset 位 并设置为 value
bitcount key start end (start 和end 为开始和结束的字节下标索引) : 统计key的value 的 start 到end 的 1 的个数 。end 可以为-1 。 0 到 -1 为统计所有字节。
但其编码方式可以是int、raw或者embstr,区别在于内存结构的不同
**int:**字符串保存的是整数值,并且这个正式可以用long类型来表示,那么其就会直接保存在redisObject的ptr属性里,并将编码设置为int
**raw:**字符串保存的大于32字节的字符串值,则使用简单动态字符串(SDS)结构,并将编码设置为raw,此时内存结构与SDS结构一致,内存分配次数为两次,创建redisObject对象和sdshdr结构
**embstr:**字符串保存的小于等于32字节的字符串值,使用的也是简单的动态字符串(SDS结构),但是内存结构做了优化,用于保存顿消的字符串;内存分配也只需要一次就可完成,分配一块连续的空间即可,
2)哈希类型 hash map格式 :ziplist和hashtable编码
hset key field value: field value 为 hashmap的键值。
hget key field
hgetall key 获取指定key的所有filed和value。 如果哈希元素个数比较多,会存在阻塞Redis的可能。
hdel key field [field …] hdel会删除一个或多个field,返回结果为成功删除field的个数。
hlen key 计算field个数
批量设置或获取field-value
hmget key field [field …]
hmset key field value [field value …]
hexists key field 判断field是否存
hkeys key 获取所有field hvals key 获取所有value
hincrby和hincrbyfloat,就像incrby和incrbyfloat命令一样,但是它们的作 用域是filed。
hincrby key field increment
hincrbyfloat key field increment
hstrlen key field 计算value的字符串长度(需要Redis3.2以上)
ziplist编码的哈希对象底层实现是压缩列表,在ziplist编码的哈希对象中,key-value键值对是以紧密相连的方式放入压缩链表的,先把key放入表尾,再放入value;键值对总是向表尾添加。
hashtable编码的哈希对象底层实现是字典,哈希对象中的每个key-value对都使用一个字典键值对来保存。
哈希对象使用ziplist编码需要满足两个条件:当哈希类型元素个数小于hash-max-ziplist-entries 配置(默认512个)、同时所有值都小于hash-max-ziplist-value配置(默认64 字节)
不满足任意一个都使用hashtable编码。而 hashtable的读写时间复杂度为O(1)。3)列表类型 list :列表对象的编码可以是ziplist和linkedlist之一。一个列表最多可以存储2^32-1个元素
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素导列表的头部(左边)或者尾部(右边)
lpush(从列表左边添加)lpush key value [value …]
rpush (从列表右边添加) rpush key value [value …]
llen key 获取列表长度
lset key index newValue 修改指定索引下标的元素
lrange key start end 范围获取, 查看所有 范围从0 到 -1
lpop 删除列表最左边的元素并返回, lpop key
rpop 删除列表最又边的元素并返回, lpop key
lrem key count value ,lrem命令会从列表中找到等于value的元素进行删除,根据count的不同分为三种情况:
·count>0,从左到右,删除最多count个元素。 ·count<0,从右到左,删除最多count绝对值个元素。 ·count=0,删除所有ltrim key start end 按照索引范围修剪列表 比如 ltrim listkey 1 操作会只保留列表listkey第2个到第4个元素。
lindex key index 获取列表对应索引的元素。 lindex listkey -1 当前列表最后一个元素
linsert key before|after pivot value linsert 命令会从列表中找到等于pivot的元素,在其前(before)或者后 (after)插入一个新的元素value. 返回结果为list的长度,
阻塞式弹出如下:blpop和brpop是lpop和rpop的阻塞版本,它们除了弹出方向不同,使用 方法基本相同。key[key…]:多个列表的键,·timeout:阻塞时间(单位:秒)。
blpop key [key …] timeout
brpop key [key …] timeout
1)列表为空:如果timeout=3,那么客户端要等到3秒后返回,如果 timeout=0,那么客户端一直阻塞等下去:如果此期间添加了数据element1,客户端立即返回。
2)列表不为空:客户端会立即返回。
在使用brpop时,有两点需要注意:
第一点,如果是多个键,那么brpop会从左至右遍历键,一旦有一个键能弹出元素,客户端立即返回。
第二点,如果多个客户端对同一个键执行brpop,那么最先执行brpop命令的客户端可以获取到弹出的值,其他顺序执行。
linkedlist编码底层采用双端链表实现,每个双端链表节点都保存了一个字符串对象,在每个字符串对象内保存了一个列表元素。
列表对象使用ziplist编码需要满足两个条件:一是所有字符串长度都小于64字节,二是元素数量小于512,不满足任意一个都会使用linkedlist编码。
两个条件的数字可以在Redis的配置文件中修改,list-max-ziplist-value选项和list-max-ziplist-entries选项。
使用场景:lpush+brpop命令组合即可实现阻塞队列,生产者客户端使用lrpush从列表左侧插入元素,多个消费者客户端使用brpop命令 阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用
性。·lpush+lpop=Stack(栈)
·lpush+rpop=Queue(队列) ·lpsh+ltrim=Capped Collection(有限集合) ·lpush+brpop=Message Queue(消息队列)4)集合类型 set 不允许重复值(不推荐使用),需要用数据太多,比如下面的srandmember操作需要随机 ,阻塞带宽。
intset和hashtable编码。
sadd key element [element …] 存储 成功返回1 失败返回0
smembers key 获取set中的所有元素,smembers和lrange、hgetall都属于比较重的命令,如果元素过多存在阻 塞Redis的可能性
srem key element [element …] 删除集合中的元素
scard key 计算元素个数 ,scard的时间复杂度为O(1),它不会遍历集合所有元素,而是直接用 Redis内部的变量。
sismember key element 判断元素是否在集合, 在集合内返回1,反之返回0
srandmember key n: 如果n为整数(不写 默认1),随机获取n个不重复元素,如果大于集合个数,那就返回所有元素。如果n小于0.那么返回可以重复元素,如果abs(n)大于元素个数仍然可以返回随机的n个可以重复元素。
spop key ,spop操作可以从集合中随机弹出一个元素 ,spop也支持[count]参数。srandmember和spop都是随机从集合选出元素,两者不同的是spop命令 执行后,元素会从集合中删除,而srandmember不会。
sinter key [key …] 交集
suinon key [key …] 并集
sdiff key [key …] 差集 key1 key2谁在前取谁的差集
集合间的运算在元素较多的情况下会比较耗时,所以Redis提供了上面 三个命令(原命令+store)将集合间交集、并集、差集的结果保存在 destination 这个 key中
sinterstore destination key [key …]
suionstore destination key [key …]
sdiffstore destination key [key …]
intset编码的集合对象底层实现是整数集合,所有元素都保存在整数集合中
集合对象使用intset编码需要满足两个条件:一是所有元素都是整数值;二是元素个数小于等于512个;不满足任意一条都将使用hashtable编码。
以上第二个条件可以在Redis配置文件中修改et-max-intset-entries选项
5)有序集合类型sortedset :不允许重复元素,且元素有序,ziplist编码 和skiplist编码
Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为 2^32 - 1 (4294967295, 每个集合可存储40多亿个成员)。
存储:zadd key score member [score member …] 重复zadd会更新score 小的靠前 ,返回结果代表成功添加成员的个数
为zadd命令添加了nx、xx、ch、incr四个选项:
·nx:member必须不存在,才可以设置成功,用于添加。 ·xx:member必须存在,才可以设置成功,用于更新。 ·ch:返回此次操作后,有序集合元素和分数发生变化的个数 ·incr:对score做增加,相当于后面介绍的zincrby。zrem key member [member …] 删除成员 ,返回结果为成功删除的个数。
zcard key 计算成员个数
zscore key member 计算某个成员的分数
计算成员的排名: zrank是从分数从低到高返回排名,zrevrank反之
zrank key member
zrevrank key member
有序集合相比集合提供了排序字段,但是也产生了代价,zadd的时间 复杂度为O(log(n)),sadd的时间复杂度为O(1)。
zincrby key increment(增加的值) member 增加成员的分数
获取:zrange key start end 0到-1为所有 zrange key 0 -1 withscores 并列出soce . 从小到大。
zrevrange key start end 从大到小 zrevrange key 0 1 倒序取两个
返回指定分数范围的成员:其中zrangebyscore按照分数从低到高返回,zrevrangebyscore反之,withscores选项会同时返回每个 成员的分数。[limit offset count]选项可以限制输出的起始位置和个数,同时min和max还支持开区间(小括号)和闭区间(中括号),-inf和 +inf分别代表无限小和无限大:
zrangebyscore key min max [withscores] [limit offset count]
zrevrangebyscore key max min [withscores] [limit offset count]
zrevrangebyscore user:ranking 221 200 withscores
zrangebyscore user:ranking (200 +inf withscores
zcount key min max 返回指定分数范围成员个数
zremrangebyrank key start end 删除指定排名内的升序元素 zremrangebyrank user:ranking 0 2
zremrangebyscore key min max 删除指定分数范围的成员 zremrangebyscore user:ranking (250 +inf
交集:
zinterstore destination numkeys key [key …] [weights weight [weight …]] [aggregate sum|min|max]
这个命令参数较多,下面分别进行说明: ·destination:交集计算结果保存到这个键。 ·numkeys:需要做交集计算键的个数。 ·key[key…]:需要做交集计算的键。·weights weight[weight…]:每个键的权重,在做交集计算时,每个键中 的每个member会将自己分数乘以这个权重,每个键的权重默认是1。
·aggregate sum|min|max:计算成员交集后,分值可以按照sum(和)、 min(最小值)、max(最大值)做汇总,默认值是sum并集:
zunionstore destination numkeys key [key …] [weights weight [weight …]] [aggregate sum|min|max]
该命令的所有参数和zinterstore是一致的,只不过是做并集计算,
skiplist编码的有序集合对象底层实现是跳跃表和字典两种;
每个跳跃表节点都保存一个集合元素,并按分值从小到大排列;节点的object属性保存了元素的成员,score属性保存分值;
字典的每个键值对保存一个集合元素,字典的键保存元素的成员,字典的值保存分值。
为何skiplist编码要同时使用跳跃表和字典实现?
有序集合对象使用ziplist编码需要满足两个条件:一是所有元素长度小于64字节;二是元素个数小于128个;不满足任意一条件将使用skiplist编码。
以上两个条件可以在Redis配置文件中修改zset-max-ziplist-entries选项和zset-max-ziplist-value选项
总结:
在Redis的五大数据对象中,string对象是唯一个可以被其他四种数据对象作为内嵌对象的;
列表(list)、哈希(hash)、集合(set)、有序集合(zset)底层实现都用到了压缩列表结构,并且使用压缩列表结构的条件都是在元素个数比较少、字节长度较短的情况下;
四种数据对象使用压缩列表的优点:
(1)节约内存,减少内存开销,Redis是内存型数据库,所以一定情况下减少内存开销是非常有必要的。
(2)减少内存碎片,压缩列表的内存块是连续的,并分配内存的次数一次即可。
(3)压缩列表的新增、删除、查找操作的平均时间复杂度是O(N),在N再一定的范围内,这个时间几乎是可以忽略的,并且N的上限值是可以配置的。
(4)四种数据对象都有两种编码结构,灵活性增加。
通用命令:
1.keys * 遍历查询所有的键 ,所以它的时间复杂度是O(n),当Redis保存了大量键时,线上环境禁止使用。支持pattern匹配,
*代表匹配任意字符
·代表匹配一个字符
[]代表匹配部分字符,例如[1,3]代表匹配1,3,[1-10]代表匹配1到10
的任意数字。·\x用来做转义,例如要匹配星号、问号需要进行转义。
和keys命令执行时会遍历所有键不同,scan采用渐进式遍历 的方式来解决keys命令可能带来的阻塞问题,每次scan命令的时间复杂度是 O(1),但是要真正实现keys的功能,需要执行多次scan。那么每次执行scan,可以想象成只扫描一个字典中的一部分键,直到将 字典中的所有键遍历完毕。scan的使用方法如下:
scan cursor [match pattern] [count number] ·cursor是必需参数,实际上cursor是一个游标,第一次遍历从0开始,每 次scan遍历完都会返回当前游标的值,直到游标值为0,表示遍历结束。 ·match pattern是可选参数,它的作用的是做模式的匹配,这点和keys的 模式匹配很像。 ·count number是可选参数,它的作用是表明每次要遍历的键个数,默认 值是10,此参数可以适当增大。 现有一个Redis有26个键(英文26个字母),现在要遍历所有的键,使 用scan命令效果的操作如下。第一次执行scan0,返回结果分为两个部分:第 一个部分6就是下次scan需要的cursor,第二个部分是10个键: 127.0.0.1:6379> scan 0“6”
使用新的cursor=“6”,执行scan6
127.0.0.1:6379> scan 6
“11”
这次得到的cursor=“11”,继续执行scan11得到结果cursor变为0,说明所有的键已经被遍历过了:
127.0.0.1:6379> scan 11
“0”
除了scan以外,Redis提供了面向哈希类型、集合类型、有序集合的扫 描遍历命令,解决诸如hgetall、smembers、zrange可能产生的阻塞问题,对 应的命令分别是hscan、sscan、zscan,它们的用法和scan基本类似。渐进式遍历可以有效的解决keys命令可能产生的阻塞问题,但是scan并 非完美无瑕,如果在scan的过程中如果有键的变化(增加、删除、修改),那么遍历效果可能会碰到如下问题:新增的键可能没有遍历到,遍历出了重 复的键等情况,也就是说scan并不能保证完整的遍历出来所有的键,这些是我们在开发时需要考虑的。
2.dbsize命令会返回当前数据库中键的总数,dbsize命令在计算键总数时不会遍历所有键,而是直接获取Redis内置的 键总数变量,所以dbsize命令的时间复杂度是O(1)。
3.type key 获取键对应的value的类型type命令实际返回的就是当前键的数据结构类型,它们分别是: string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集 合),但这些只是Redis对外的数据结构。如果键不存在,则返回none。
4.del key [key …] 删除指定的key ,不论value为什么类型,返回结果为成功删除键的个数,假设删除一个不存在的键,就会返回
0。5.exists key 检查键是否存在 如果键存在则返回1,不存在则返回0。
6.expire key seconds 键过期 Redis支持对键添加过期时间,当超过过期时间后,会自动删除键,例 如为键hello设置了10秒过期时间: expire hello 10,
expireat key timestamp 键在秒级时间戳timestamp后过期。
pexpire key milliseconds:键在milliseconds毫秒后过期。
pexpireat key milliseconds-timestamp键在毫秒级时间戳timestamp后过期。
但无论是使用过期时间还是时间戳,秒级还是毫秒级,在Redis内部最 终使用的都是pexpireat
在使用Redis相关过期命令时,需要注意以下几点。
1)如果expire key的键不存在,返回结果为0:
2)如果过期时间为负值,键会立即被删除,犹如使用del命令一样
3)persist命令可以将键的过期时间清除: persist key
7.ttl命令会返回键的剩余过期时间,它有3种返回值:大于等于0的整数:键剩余的过期时间。-1:键没设置过期时间。-2:键不存在。pttl也可以查询键的剩余过期时间,但是pttl精度更高可以达到 毫秒级别。
8.object encoding命令查询内部编码:每个类型有两种内部编码。
9.rename key newkey 键重命名 ,如果在rename之前,键java已经存在,那么它的值也将被覆盖,为了防止被强行rename,Redis提供了renamenx命令,确保只有newKey 不存在时候才被覆盖,由于重命名键期间会执行del命令删除旧的键,如果键对应的值比较 大,会存在阻塞Redis的可能性,这点不要忽视
migrate命令:migrate命令也是用于在Redis实例间进行数据迁移的,实际上migrate命 令就是将dump、restore、del三个命令进行组合,从而简化了操作流程。 migrate命令具有原子性,而且从Redis3.0.6版本以后已经支持迁移多个键的 功能,有效地提高了迁移效率,migrate在10.4节水平扩容中起到重要作用。
整个过程如图2-28所示,实现过程和dump+restore基本类似,但是有3点 不太相同:第一,整个过程是原子执行的,不需要在多个Redis实例上开启 客户端的,只需要在源Redis上执行migrate命令即可。第二,migrate命令的 数据传输直接在源Redis和目标Redis上完成的。第三,目标Redis完成restore 后会发送OK给源Redis,源Redis接收后会根据migrate对应的选项来决定是否 在源Redis上删除对应的键。下面对migrate的参数进行逐个说明:
host:目标Redis的IP地址。 port:目标Redis的端口。 key|"":在Redis3.0.6版本之前,migrate只支持迁移一个键,所以此处是 要迁移的键,但Redis3.0.6版本之后支持迁移多个键,如果当前需要迁移多 个键,此处为空字符串""。 destination-db:目标Redis的数据库索引,例如要迁移到0号数据库,这里就写0。 timeout:迁移的超时时间(单位为毫秒)。 [copy]:如果添加此选项,迁移后并不删除源键。 [replace]:如果添加此选项,migrate不管目标Redis是否存在该键都会正常迁移进行数据覆盖。 [keys key[key…]]:迁移多个键,例如要迁移key1、key2、key3,此处填 写“keys key1 key2 key3”比如:migrate 127.0.0.1 6380 “” 0 5000 keys key1 key2 key3
move命令用于在Redis内部进行数据迁移,Redis内部可以有多个数据库,Redis内部可以有多个数据库,彼此在数据上是相互隔离的,move key db就是把指定的键从源数据库移动到目标数据库中,不建议在生产环境使用。
建议使用migrate命令进行键值迁移。
Redis提供了几个面向Redis数据库的操作,它们分别是dbsize、select、 flushdb/flushall命令,本节将通过具体的使用场景介绍这些命令。
select dbIndex 切换数据库 , Redis默认配置中是有16个数据库,select 0操作将切换到第一个数据库,select 15选择最 后一个数据库,但是0号数据库和15号数据库之间的数据没有任何关联,甚至可以存在相同的键。当使用rediscli-h{ip}-p{port}连接Redis时,默认使用的就是0号数据库,当选择其他数据 库时,会有[index]的前缀标识,其中index就是数据库的索引下标。Redis3.0中已经逐渐弱化这个功能,例如Redis的分布式实现Redis Cluster只允许使用0号数据库,只不过为了向下兼容老版本的数据库功能, 为什么要废弃掉这个“优秀”的功能呢?总结起来有三点:Redis是单线程的。如果使用多个数据库,那么这些数据库仍然是使用 一个CPU,彼此之间还是会受到影响的。
多数据库的使用方式,会让调试和运维不同业务的数据库变的困难,假如有一个慢查询存在,依然会影响其他数据库,这样会使得别的业务方定位问题非常的困难
部分Redis的客户端根本就不支持这种方式。即使支持,在开发的时候来回切换数字形式的数据库,很容易弄乱。
flushdb/flushall命令用于清除数据库,两者的区别的是flushdb只清除当 前数据库,flushall会清除所有数据库。flushdb/flushall命令会将所有数据清除,一旦误操作后果不堪设想。
Bitmaps
Redis提供了Bitmaps这个“数据结构”可以实现对位的操作。。把数据结构加上引号主要因为:Bitmaps本身不是一种数据结构,实际上它就是字符串但是它可以对字符串的位进行操作。Bitmaps单独提供了一套命令,所以在Redis中使用Bitmaps和使用字符 串的方法不太相同。可以把Bitmaps想象成一个以位为单位的数组,数组的 每个单元只能存储0和1,数组的下标在Bitmaps中叫做偏移量.
1.设置值 setbit key offset value 设置键的第offset个位的值(从0算起)
很多应用的用户id以一个指定数字(例如10000)开头,直接将用户id 和Bitmaps的偏移量对应势必会造成一定的浪费,通常的做法是每次做setbit 操作时将用户id减去这个指定数字。在第一次初始化Bitmaps时,假如偏移 量非常大,那么整个初始化过程执行会比较慢,可能会造成Redis的阻塞。
2.获取值 gitbit key offset 获取键的第offset位的值(从0开始算).3.获取Bitmaps指定范围值为1的个数 bitcount [start][end] [start]和[end]代表起始和结束字节数,不写就返回整个key的1的个数。
bitcount key 1 34.Bitmaps间的运算
bitop op destkey key[key…] bitop是一个复合操作,它可以做多个Bitmaps的and(交集)、or(并 集)、not(非)、xor(异或)操作并将结果保存在destkey中。5.计算Bitmaps中第一个值为targetBit的偏移量 bitpos key targetBit [start] [end] bitops有两个选项[start]和[end],分别代表起始字节和结束字 节,例如计算第0个字节到第1个字节之间,第一个值为0的偏移量
bitpos key 0 0 1 但Bitmaps并不是万金油,假如该网站每天的独立访问用户很少,例如 只有10万(大量的僵尸用户),那么两者的对比如表3-5所示,很显然,这 时候使用Bitmaps就不太合适了,因为基本上大部分位都是0.HyperLogLog
HyperLogLog并不是一种新的数据结构(实际类型为字符串类型),而 是一种基数算法,通过HyperLogLog可以利用极小的内存空间完成独立总数 的统计,数据集可以是IP、Email、ID等。HyperLogLog提供了3个命令: pfadd、pfcount、pfmerge。例如2016-03-06的访问用户是uuid-1、uuid-2、 uuid-3、uuid-4,2016-03-05的访问用户是uuid-4、uuid-5、uuid-6、uuid-7。
1.添加 pfadd key element [element …]
2.计算独立用户数 pfcount key [key …] pfcount用于计算一个或多个HyperLogLog的独立总数。,例如 2016_03_06:unique:ids的独立总数为4:如果此时向2016_03_06:unique:ids插入uuid-1、uuid-2、uuid-3、uuid90,结果是5(新增uuid-90): HyperLogLog内存占用量小得惊人,但是用如此小空间来估 算如此巨大的数据,必然不是100%的正确,其中一定存在误差率。Redis官 方给出的数字是0.81%的失误率。 3.合并 pfmerge destkey sourcekey [sourcekey …] pfmerge可以求出多个HyperLogLog的并集并赋值给destkey。所谓慢查询日志就是系统在命令执行前后计算每条命令的执行时间,当超过预设阀值,就将这条命令的相关信息(例如:发生时 间,耗时,命令的详细信息)记录下来,Redis也提供了类似的功。mysql的是explain命令。
Redis提供了slowlog-log-slower-than和slowlog-max-len配置来解决这两个 问题。从字面意思就可以看出,slowlog-log-slower-than就是那个预设阀值,它的单位是微秒(1秒=1000毫秒=1000000微秒),默认值是10000,假如执 行了一条“很慢”的命令(例如keys*),如果它的执行时间超过了10000微秒,那么它将被记录在慢查询日志中。如果slowlog-log-slower-than=0会记录所有的命令,slowlog-log-slowerthan<0对于任何命令都不会进行记录。
从字面意思看,slowlog-max-len只是说明了慢查询日志最多存储多少 条,并没有说明存放在哪里?实际上Redis使用了一个列表来存储慢查询日 志,slowlog-max-len就是列表的最大长度,。一个新的命令满足慢查询条件时被插入到这个列表中,当慢查询日志列表已处于其最大长度时,最早插入的 一个命令将从列表中移出。
在Redis中有两种修改配置的方法,一种是修改配置文件,另一种是使用config set命令动态修改。例如下面使用config set命令将slowlog-log-slowerthan设置为20000微秒,slowlog-max-len设置为1000:如果要Redis将配置持久化到本地配置文件,需要执行config rewrite命令
config set slowlog-log-slower-than 20000
config set slowlog-max-len 1000
config rewrite
虽然慢查询日志是存放在Redis内存列表中的,但是Redis并没有暴露这个列表的键,而是通过一组命令来实现对慢查询日志的访问和管理。
slowlog get [n] 获取慢查询日志,,参数n可以指定条数:可以看到每个慢查询日志有4个属性组成,分别是慢查询日志的标识 id、发生时间戳、命令耗时、执行命令和参数。
(integer) 665
(integer) 1456718400
(integer) 12006
slowlog len 获取慢查询日志列表当前的长度
slowlog reset 慢查询日志重置,实际是对列表做清理操作
建议:·slowlog-max-len配置建议:线上建议调大慢查询列表,记录慢查询时 Redis会对长命令做截断操作,并不会占用大量内存。增大慢查询列表可以 减缓慢查询被剔除的可能,例如线上可设置为1000以上。
slowlog-log-slower-than配置建议:默认值超过10毫秒判定为慢查询, 需要根据Redis并发量调整该值。由于Redis采用单线程响应命令,对于高流 量的场景,如果命令执行时间在1毫秒以上,那么Redis最多可支撑OPS不到 1000。因此对于高OPS场景的Redis建议设置为1毫秒。
·慢查询只记录命令执行时间,并不包括命令排队和网络传输时间。因此客户端执行命令的时间会大于命令实际执行时间。因为命令执行排队机制,慢查询会导致其他命令级联阻塞,因此当客户端出现请求超时,需要检查该时间点是否有对应的慢查询,从而分析出是否为慢查询导致的命令级联阻塞。由于慢查询日志是一个先进先出的队列,也就是说如果慢查询比较多的情况下,可能会丢失部分慢查询命令,为了防止这种情况发生,可以定期 执行slow get命令将慢查询日志持久化到其他存储中(例如MySQL),然后 可以制作可视化界面进行查询。
redis-cli-help 查看帮助。
redis-cli–v查看Redis的版本。 redis-cli -h 127.0.0.1 -p 6379 redis-cli -h 127.0.0.1 -p 6379 get hello 直接得到命令的返回结果 redis-cli shutdown 停止Redis服务 Redis关闭的过程:断开与客户端的连接、持久化文件生成,是一种相对优雅的关闭方式。shutdown还有一个参数,代表是否在关闭Redis前,生成持久化文件: redis-cli shutdown nosave|save 除了可以通过shutdown命令关闭Redis服务以外,还可以通过kill进程 号的方式关闭掉Redis,但是不要粗暴地使用kill-9强制杀死Redis服务,不但不会做持久化操作,还会造成缓冲区等资源不能被优雅关闭,极端情况会造 成AOF和复制丢失数据的情况。redis-cli 参数:
-r : -r(repeat)选项代表将命令执行多次,例如下面操作将会执行三次ping命令 :redis-cli -r 3 ping -i(interval)选项代表每隔几秒执行一次命令,但是-i选项必须和-r选 项一起使用,redis-cli -r 5 -i 1 ping ,注意-i的单位是秒,不支持毫秒为单位,但是如果想以每隔10毫秒执行 一次,可以用-i 0.01。 redis-cli -r 100 -i 1 info | grep used_memory_human 每隔1秒输出内存的使用量,一共输出 100次: -x选项代表从标准输入(stdin)读取数据作为redis-cli的最后一个参 数,例如下面的操作会将字符串world作为set hello的值, echo “world” | redis-cli -x set hello -c(cluster)选项是连接Redis Cluster节点时需要使用的,-c选项可以防 止moved和ask异常 -a 如果Redis配置了密码,可以用-a(auth)选项,有了这个选项就不需要 手动输入auth命令 –scan选项和–pattern选项用于扫描指定模式的键,相当于使用scan命令。 –slave选项是把当前客户端模拟成当前Redis节点的从节点,可以用来 获取当前Redis节点的更新操作,合理的利用这个选项可以记录当前连接Redis节点的一些更新操作,这些更新操作很可能是实际开发业务时需要的数据。 –rdb选项会请求Redis实例生成并发送RDB持久化文件,保存在本地。 可使用它做持久化文件的定期备份。 –pipe选项用于将命令封装成Redis通信协议定义的数据格式,批量发送 给Redis执行, –bigkeys选项使用scan命令对Redis的键进行采样,从中找到内存占用比较大的键值,这些键可能是系统的瓶颈。 –eval选项用于执行指定Lua脚本 latency有三个选项,分别是–latency、–latency-history、–latency-dist。 它们都可以检测网络延迟,对于Redis的开发和运维非常有帮助。 –latency 该选项可以测试客户端到目标Redis的网络延迟, –latency-history :–latency的执行结果只有一条,如果想以分时段的形式了解延迟信息, 可以使用–latency-history选项:可以看到延时信息每15秒输出一次,可以通过-i参数控制间隔时间。redis-cli -h 10.10.xx.xx --latency-history –latency-dist 该选项会使用统计图表的形式从控制台输出延迟统计信息。 –stat选项可以实时获取Redis的重要统计信息,虽然info命令中的统计信 息更全,但是能实时看到一些增量的数据(例如requests) –no-raw选项是要求命令的返回结果必须是原始的格式,–raw恰恰相反,返回格式化后的结果。client kill ip:port 此命令用于杀掉指定IP地址和端口的客户。
client setName和client getName ,client setName用于给客户端设置名字,这样比较容易标识出客户端的来源。
client pause timeout(毫秒)client pause命令用于阻塞客户端timeout毫秒数,在此期间客户端连接将被阻塞。
·client pause只对普通和发布订阅客户端有效,对于主从复制(从节点内部伪装了一个客户端)是无效的,也就是此期间主从复制是正常进行的,所以此命令可以用来让主从复制保持一致。
·client pause可以用一种可控的方式将客户端连接从一个Redis节点切换 到另一个Redis节点。 需要注意的是在生产环境中,暂停客户端成本非常高monitor: monitor命令用于监控Redis正在执行的命令,如果开发和运维人员想监听Redis正在执行的命令,就可以用monitor命令,但事实并非如此美好,每个客户端都有自己的 输出缓冲区,既然monitor能监听到所有的命令,一旦Redis的并发量过大, monitor客户端的输出缓冲会暴涨,可能瞬间会占用大量内存。
tcp-keepalive :检测TCP连接活性的周期,默认值为0,也就是不进行 检测,如果需要设置,建议为60,那么Redis会每隔60秒对它创建的TCP连接进行活性检测,防止大量死连接占用系统资源。
tcp-backlog:TCP三次握手后,会将接受的连接放入队列中,tcpbacklog就是队列的大小,它在Redis中的默认值是511。通常来讲这个参数不 需要调整,但是这个参数会受到操作系统的影响,例如在Linux操作系统 中,如果/proc/sys/net/core/somaxconn小于tcp-backlog,那么在Redis启动时会 看到如下日志,并建议将/proc/sys/net/core/somaxconn设置更大。
client list 列出与Redis服务端相连的所有客户端连接信息.
id=2 addr=127.0.0.1:64395 fd=6 name= age=9 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
(1)标识:id、addr、fd、name
·id:客户端连接的唯一标识,这个id是随着Redis的连接自增的,重启 Redis后会重置为0。
·addr:客户端连接的ip和端口. ·fd:socket的文件描述符,与lsof命令结果中的fd是同一个,如果fd=-1 代表当前客户端不是外部客户端,而是Redis内部的伪装客户端 ·name:客户端的名字。(2)输入缓冲区:qbuf、qbuf-free
Redis为每个客户端分配了输入缓冲区,它的作用是将客户端发送的命 令临时保存,同时Redis从会输入缓冲区拉取命令并执行,输入缓冲区为客 户端发送命令到Redis执行命令提供了缓冲功能。client list中qbuf和qbuf-free分别代表这个缓冲区的总容量和剩余容量, Redis没有提供相应的配置来规定每个缓冲区的大小,输入缓冲区会根据输 入内容大小的不同动态调整,只是要求每个客户端缓冲区的大小不能超过 1G,超过后客户端将被关闭。 /* Protocol and I/O related defines / #define REDIS_MAX_QUERYBUF_LEN (102410241024) / 1GB max query buffer. */ 输入缓冲使用不当会产生两个问题: ·一旦某个客户端的输入缓冲区超过1G,客户端将会被关闭。 输入缓冲区不受maxmemory控制,假设一个Redis实例设置了 maxmemory为4G,已经存储了2G数据,但是如果此时输入缓冲区使用了 3G,已经超过maxmemory限制,可能会产生数据丢失、键值淘汰、OOM等 情况。那么如何快速发现和监控呢?监控输入缓冲区异常的方法有两种:
·通过定期执行client list命令,收集qbuf和qbuf-free找到异常的连接记录并分析,最终找到可能出问题的客户端。·通过info命令的info clients模块,找到最大的输入缓冲区,例如下面命 令中的其中client_biggest_input_buf代表最大的输入缓冲区,例如可以设置超 过10M就进行报警:
127.0.0.1:6379> info clients # Clients connected_clients:1414 client_longest_output_list:0 client_biggest_input_buf:2097152 blocked_clients:0(3)输出缓冲区:obl、oll、omem
client list中的obl代表固定缓冲区的长度,oll代表动态缓冲区列表的长 度,omem代表使用的字节数。
Redis为每个客户端分配了输出缓冲区,它的作用是保存命令执行的结 果返回给客户端,为Redis和客户端交互返回结果提供缓冲,与输入缓冲区不同的是,输出缓冲区的容量可以通过参数client-outputbuffer-limit来进行设置,并且输出缓冲区做得更加细致,按照客户端的不同 分为三种:普通客户端、发布订阅客户端、slave客户端,client-output-buffer-limit
·:客户端类型,分为三种。a)normal:普通客户端;b) slave:slave客户端,用于复制;c)pubsub:发布订阅客户端。 ·:如果客户端使用的输出缓冲区大于,客户端会被立即关闭。 ·和:如果客户端使用的输出缓冲区超过了并且持续了秒,客户端会被立即关闭。默认配置
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60 lient-output-buffer-limit pubsub 32mb 8mb 60和输入缓冲区相同的是,输出缓冲区也不会受到maxmemory的限制,如 果使用不当同样会造成maxmemory用满产生的数据丢失、键值淘汰、OOM等情况。实际上输出缓冲区由两部分组成:固定缓冲区(16KB)和动态缓冲区,其中固定缓冲区返回比较小的执行结果,而动态缓冲区返回比较大的结 果,例如大的字符串、hgetall、smembers命令的结果等。固定缓冲区使用的是字节数组,动态缓冲区使用的是列表。当固定缓冲 区存满后会将Redis新的返回结果存放在动态缓冲区的队列中,队列中的每 个对象就是每个返回结果。
监控输出缓冲区的方法依然有两种:
通过定期执行client list命令,收集obl、oll、omem找到异常的连接记录并分析,最终找到可能出问题的客户端。 ·通过info命令的info clients模块,找到输出缓冲区列表最大对象数,例如:127.0.0.1:6379> info clients
# Clients connected_clients:502 client_longest_output_list:4869 client_biggest_input_buf:0 blocked_clients:0其中,client_longest_output_list代表输出缓冲区列表最大对象数,这两种统计方法的优劣势和输入缓冲区是一样的。相比于输
入缓冲区,输出缓冲区出现异常的概率相对会比较大,那么如何预防呢?方法如下: ·进行上述监控,设置阀值,超过阀值及时处理。 ·限制普通客户端的输出缓冲区,把错误扼杀在摇篮中,例如可以进行如下设置:client-output-buffer-limit normal 20mb 10mb 120
·适当增大slave的输出缓冲区的,如果master节点写入较大,slave客户 端的输出缓冲区可能会比较大,一旦slave客户端连接因为输出缓冲区溢出 被kill,会造成复制重连.
·限制容易让输出缓冲区增大的命令,,例如,高并发下的monitor命令就是一个危险的命令。
·及时监控内存,一旦发现内存抖动频繁,可能就是输出缓冲区过.
(4)客户端的存活状态
client list中的age和idle分别代表当前客户端已经连接的时间和最近一次的空闲时间
(5)客户端的限制maxclients和timeout
Redis提供了maxclients参数来限制最大客户端连接数,一旦连接数超过 maxclients,新的连接将被拒绝。maxclients默认值是10000,可以通过info clients来查询当前Redis的连接数:可以通过config set maxclients对最大客户端连接数进行动态设置 :config set maxclients 50
timeout(单位为秒)参数来限制连接的最大空闲时间,一 旦客户端连接的idle时间超过了timeout,连接将会被关闭:
config set timeout 30
如果超时关闭,在Jedis代码中抛出了异常,因为此时客户端 已经被关闭,所以抛出的异常是JedisConnectionException
Redis的默认配置给出的timeout=0,
(6)客户端类型
client list中的flag是用于标识当前客户端的类型,例如flag=S代表当前客 户端是slave客户端、flag=N代表当前是普通客户端,flag=O代表当前客户端 正在执行monitor命令,表4-4列出了11种客户端类型
客户端统计片段
例如下面就是一次info clients的执行结果: 127.0.0.1:6379> info clients # Clients connected_clients:1414 client_longest_output_list:0 client_biggest_input_buf:2097152 blocked_clients:01)connected_clients:代表当前Redis节点的客户端连接数,需要重点监 控,一旦超过maxclients,新的客户端连接将被拒绝。
2)client_longest_output_list:当前所有输出缓冲区中队列对象个数的最 大值。 3)client_biggest_input_buf:当前所有输入缓冲区中占用的最大容量。 4)blocked_clients:正在执行阻塞命令(例如blpop、brpop、 brpoplpush)的客户端个数。除此之外info stats中还包含了两个客户端相关的统计指标,
·total_connections_received:Redis自启动以来处理的客户端连接数总数。
·rejected_connections:Redis自启动以来拒绝的客户端连接数,需要重点监控。客户端常见异常:
1.无法从连接池获取到连接
JedisPool中的Jedis对象个数是有限的,默认是8个。这里假设使用的默 认配置,如果有8个Jedis对象被占用,并且没有归还,此时调用者还要从 JedisPool中借用Jedis,就需要进行等待(例如设置了maxWaitMillis>0),如 果在maxWaitMillis时间内仍然无法获取到Jedis对象就会抛出如下异常:JedisConnectionException,还有一种情况,就是设置了blockWhenExhausted=false,那么调用者发现池子中没有资源时,会立即抛出异常不进行等待,下面的异常就是 blockWhenExhausted=false时的效果, JedisConnectionException。
可能的原因:
客户端:高并发下连接池设置过小,出现供不应求,所以会出现上面 的错误,但是正常情况下只要比默认的最大连接数(8个)多一些即可,因 为正常情况下JedisPool以及Jedis的处理效率足够高。
·客户端:没有正确使用连接池,比如没有进行释放,例如下面代码所示。·客户端:存在慢查询操作,这些慢查询持有的Jedis对象归还速度会比较慢,造成池子满了。
·服务端:客户端是正常的,但是Redis服务端由于一些原因造成了客户端命令执行过程的阻塞,也会使得客户端抛出这种异常。
2.客户端读写超时:.SocketTimeoutException:
造成该异常的原因也有以下几种:
·读写超时间设置得过短。
·命令本身就比较慢。 ·客户端与服务端网络不正常。 ·Redis自身发生阻塞3.客户端连接超时 :SocketTimeoutException
造成该异常的原因也有以下几种:
1)连接超时设置得过短,可以通过下面代码进行设置: 2)Redis发生阻塞,造成tcp-backlog已满,造成新的连接失败。 3)客户端与服务端网络不正常。4.客户端缓冲区异常
造成这个异常的原因可能有如下几种:
1)输出缓冲区满。例如将普通客户端的输出缓冲区设置为1M1M60。如果使用get命令获取一个bigkey(例如3M),就会出现这个异常。2)长时间闲置连接被服务端主动断开,上节已经详细分析了这个问题。
3)不正常并发读写:Jedis对象同时被多个线程并发操作,可能会出现上述异常。
5.Lua脚本正在执行
如果Redis当前正在执行Lua脚本,并且超过了lua-time-limit,此时Jedis调用Redis时,会收到下面的异常。
6.Redis正在加载持久化文件
Jedis调用Redis时,如果Redis正在加载持久化文件,那么会收到下面的异常。
7.Redis使用的内存超过maxmemory配置
Jedis执行写操作时,如果Redis的使用内存大于maxmemory的设置,会 收到下面的异常,此时应该调整maxmemory并找到造成内存增长的原因
8.客户端连接数过大
如果客户端连接数超过了maxclients,新申请的连接就会出现如下异常:这个问题可能会比较棘手,因为此时无法执行Redis命令进行问题修复,一般来说可以从两个方面进行着手解决:
·客户端:如果maxclients参数不是很小的话,应用方的客户端连接数基 本不会超过maxclients,通常来看是由于应用方对于Redis客户端使用不当造成的。此时如果应用方是分布式结构的话,可以通过下线部分应用节点(例 如占用连接较多的节点),使得Redis的连接数先降下来。从而让绝大部分 节点可以正常运行,此时再通过查找程序bug或者调整maxclients进行问题的 修复。·服务端:如果此时客户端无法处理,而当前Redis为高可用模式(例如 Redis Sentinel和Redis Cluster),可以考虑将当前Redis做故障转移。
案例:
案例1.Redis内存陡增,
服务端现象:Redis主节点内存陡增,几乎用满maxmemory,而从节点 内存并没有变化。
客户端现象:客户端产生了OOM异常,也就是Redis主节点使用的内存 已经超过了maxmemory的设置,无法写入新的数据。
2.分析原因
1)确实有大量写入,但是主从复制出现问题:查询了Redis复制的相关信息,复制是正常的,主从数据基本一致。2)其他原因造成主节点内存使用过大:排查是否由客户端缓冲区造成 主节点内存陡增,使用info clients命令查询相关信息。
127.0.0.1:6379> info clients
# Clients connected_clients:1891 client_longest_output_list:225698 client_biggest_input_buf:0 blocked_clients:0 很明显输出缓冲区不太正常,最大的客户端输出缓冲区队列已经超过了 20万个对象,于是需要通过client list命令找到omem不正常的连接,一般来 说大部分客户端的omem为0(因为处理速度会足够快),于是执行如下代 码,找到omem非零的客户端连接: redis-cli client list | grep -v “omem=0” id=7 addr=10.10.xx.78:56358 fd=6 name= age=91 idle=0 flags=O db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=224869 omem=2129300608 events=rw cmd=] 已经很明显是因为有客户端在执行monitor命令造成的。 3.处理方法和后期处理 对这个问题处理的方法相对简单,只要使用client kill命令杀掉这个连接,让其他客户端恢复正常写数据即可。但是更为重要的是在日后如何及时发现和避免这种问题的发生,基本有三点。·从运维层面禁止monitor命令,例如使用rename-command命令重置 monitor命令为一个随机字符串,除此之外,如果monitor没有做renamecommand,也可以对monitor命令进行相应的监控(例如client list)。
·从开发层面进行培训,禁止在生产环境中使用monitor命令,因为有时 候monitor命令在测试的时候还是比较有用的,完全禁止也不太现实。·限制输出缓冲区的大小
案例2:客户端周期性的超时
客户端现象:客户端出现大量超时,经过分析发现超时是周期性出现的。
服务端现象:服务端并没有明显的异常,只是有一些慢查询操作。
原因:·网络原因:服务端和客户端之间的网络出现周期性问题,经过观察网络是正常的。
·Redis本身:观察Redis日志统计,发现是否有异常。
结果:客户端:由于是周期性出现问题,就和慢查询日志的历史记录对应了一下时间,发现只要慢查询出现,客户端就会产生大量连接超时,两个时间 点基本一致。最终找到问题是慢查询操作造成的,通过执行hlen发现有200万个元 素,这种操作必然会造成Redis阻塞,通过与应用方沟通了解到他们有个定 时任务,每5分钟执行一次hgetall操作。
3.处理方法和后期处理
·从运维层面,监控慢查询,一旦超过阀值,就发出报警。
·从开发层面,加强对于Redis的理解,避免不正确的使用方式 ·使用专业的Redis运维工具redis-server加上要修改配置名和值(可以是多对),没有设置的配置将使用默认配置:# redis-server --port 6380
如果要用6380作为端口启动Redis。redis-server /opt/redis/redis.conf 配置文件启动。 –test-memory 可以用来检测当前操作系统能否稳定地分配指定容量的内存给 Redis,例如下面 操作检测当前操作系统能否提供1G的内存给Redis: redis-server --test-memory 1024,整个内存检测的时间比较长。当输出passed this test时说明内存检测完 毕,最后会提示–test-memory只是简单检测,如果有质疑可以使用更加专业的内存检测工具:通常无需每次开启Redis实例时都执行–test-memory选项,该功能更偏向于调试和测试,例如,想快速占满机器内存做一些极端条件的测试,这个功能是一个不错的选择。redis-benchmark可以为Redis做基准性能测试,
-c(clients)选项代表客户端的并发数量(默认是50)。
-n(num)选项代表客户端请求总量(默认是100000)。 例如redis-benchmark-c100-n20000代表100各个客户端同时请求Redis,一 共执行20000次。redis-benchmark会对各类数据结构的命令进行测试,并给出性能指标。====== GET ======
20000 requests completed in 0.27 seconds
100 parallel clients 3 bytes payload keep alive: 1 99.11% <= 1 milliseconds 100.00% <= 1 milliseconds 73529.41 requests per second 例如上面一共执行了20000次get操作,在0.27秒完成,每个请求数据量 是3个字节,99.11%的命令执行时间小于1毫秒,Redis每秒可以处理 73529.41次get请求-q选项仅仅显示redis-benchmark的requests per second信息,例如:
$redis-benchmark -c 100 -n 20000 -q PING_INLINE: 74349.45 requests per second PING_BULK: 68728.52 requests per second SET: 71174.38 requests per second… LRANGE_500 (first 450 elements): 11299.44 requests per second LRANGE_600 (first 600 elements): 9319.67 requests per second MSET (10 keys): 70671.38 requests per second -r 如果在一个空的Redis上执行了redis-benchmark会发现只有3个键,127.0.0.1:6379> dbsize
(integer) 3 127.0.0.1:6379> keys *127.0.0.1:6379> dbsize
(integer) 18641 127.0.0.1:6379> scan 0redis是一个内存数据库,为了防止数据丢失,可以将内存中的数据持久化到硬盘的文件中。
机制:
1)RDB:默认机制,不需要配置。在一定的间隔内,检测key的变化情况,然后持久化数据。配置在 redis.windows.conf文件中
save 900 1 ->900s 只要只要有1个key改变就持久化一次。
save 300 10 ->300s后 只要只要有10个key改变就持久化一次。 save 60 10000 -> 60s后 只要只要有一个10000改变就持久化一次。修改 redis.windows.conf文件后 需要用命令行进入到 redis.windows.conf目录 然后用 redis-server.exe redis.windows.conf 命令指定配置文件启动。 然后10秒内改变5次key 会触发持久化数据。 会在配置文件目录内生成一个dump.rdb 文件。
·bgsave命令:Redis进程执行fork操作创建子进程,RDB持久化过程由子 进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。运行 bgsave命令对应的Redis日志如下: Background saving started by pid 3151 * DB saved on disk * RDB: 0 MB of memory used by copy-on-write * Background saving terminated with success 显然bgsave命令是针对save阻塞问题做的优化。因此Redis内部所有的涉 及RDB的操作都采用bgsave的方式,3)执行debug reload命令重新加载Redis时,也会自动触发save操作
4)默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则 自动执行bgsave。
bgsave是主流的触发RDB持久化方式:1)执行bgsave命令,Redis父进程判断当前是否存在正在执行的子进 程,如RDB/AOF子进程,如果存在bgsave命令直接返回。
2)父进程执行fork操作创建子进程,fork操作过程中父进程会阻塞,通 过info stats命令查看latest_fork_usec选项,可以获取最近一个fork操作的耗时,单位为微秒。3)父进程fork完成后,bgsave命令返回“Background saving started”信息并不再阻塞父进程,可以继续响应其他命令.
4)子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后 对原有文件进行原子替换。执行lastsave命令可以获取最后一次生成RDB的 时间,对应info统计的rdb_last_save_time选项。
5)进程发送信号给父进程表示完成,父进程更新统计信息.
保存:RDB文件保存在dir配置指定的目录下,文件名通过dbfilename配 置指定。可以通过执行config set dir{newDir}和config set dbfilename{newFileName}运行期动态执行,当下次运行时RDB文件会保存到新目录。同样适用 于AOF持久化文件。
压缩:Redis默认采用LZF算法对生成的RDB文件做压缩处理,压缩后的 文件远远小于内存大小,默认开启,可以通过参数config set rdbcompression{yes|no}动态修改。虽然压缩RDB会消耗CPU,但可大幅降低文件的体积,方便保存到硬盘或通过网络发送给从节点,因此线上建议开启。
校验:如果Redis加载损坏的RDB文件时拒绝启动,并打印如下日志:# Short read or OOM loading DB. Unrecoverable error, aborting now.这时可以使用Redis提供的redis-check-dump工具检测RDB文件并获取对应的错误报告。
RDB的优点:
·RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的数据 快照。非常适用于备份,全量复制等场景。比如每6小时执行bgsave备份, 并把RDB文件拷贝到远程机器或者文件系统中(如hdfs),用于灾难恢复。 ·Redis加载RDB恢复数据远远快于AOF的方式。RDB的缺点:
·RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运 行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高。 ·RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式 的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题。2)AOF:日志记录的方式,可以记录每一条命令的操作,可以每一次命令后,持久化数据。AOF的主要作用 是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式.
1。修改redis.windows.conf文件中appendonly no -》appendonly yes 。修改后仍然要命令行形式指定配置文件,AOF文件名 通过appendfilename配置设置,默认文件名是appendonly.aof。保存路径同 RDB持久化方式一致,通过dir配置指定。AOF的工作流程操作:命令写入 (append)、文件同步(sync)、文件重写(rewrite)、重启加载 (load)
1)所有的写入命令会追加到aof_buf(缓冲区)中。
2)AOF缓冲区根据对应的策略向硬盘做同步操作。 3)随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的。 4)当Redis服务器重启时,可以加载AOF文件进行数据恢复。命令写入
AOF命令写入的内容直接是文本协议格式。例如set hello world这条命 令,在AOF缓冲区会追加如下文本: *3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n1)AOF为什么直接采用文本协议格式?可能的理由如下:
·文本协议具有很好的兼容性。 ·开启AOF后,所有写入命令都包含追加操作,直接采用协议格式,避免了二次处理开销。 ·文本协议具有可读性,方便直接修改和处理2)AOF为什么把命令追加到aof_buf中?Redis使用单线程响应命令,如 果每次写AOF文件命令都直接追加到硬盘,那么性能完全取决于当前硬盘负 载。先写入缓冲区aof_buf中,还有另一个好处,Redis可以提供多种缓冲区同步硬盘的策略,在性能和安全性方面做出平衡。
文件同步: Redis提供了多种AOF缓冲区同步文件策略,由参数appendfsync控制# appendfsync always 每一次操作都进行持久化。
appendfsync everysec 每隔1秒进行一次持久化。 (建议)# appendfsync no 依赖操作系统内核,一直write,内核决定什么时候flush。(出问题丢的数据很多)
系统调用write和fsync说明:
·write操作会触发延迟写(delayed write)机制。Linux在内核提供页缓 冲区用来提高硬盘IO性能。write操作在写入系统缓冲区后直接返回。同步硬盘操作依赖于系统调度机制,例如:缓冲区页空间写满或达到特定时间周期。同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失。 ·fsync针对单个文件操作(比如AOF文件),做强制硬盘同步,fsync将阻塞直到写入硬盘完成后返回,保证了数据持久化重写机制
随着命令不断写入AOF,文件会越来越大,为了解决这个问题,Redis 引入AOF重写机制压缩文件体积。AOF文件重写是把Redis进程内的数据转 化为写命令同步到新AOF文件的过程。除此之外,另一个目的是:更小的AOF 文件可以更快地被Redis加载。重写后的AOF文件为什么可以变小?有如下原因:
1)进程内已经超时的数据不再写入文件。
2)旧的AOF文件含有无效命令,如del key1、hdel key2、srem keys、set a111、set a222等。重写使用进程内数据直接生成,这样新的AOF文件只保留最终数据的写入命令。 3)多条写命令可以合并为一个,如:lpush list a、lpush list b、lpush list c可以转化为:lpush list a b c。为了防止单条命令过大造成客户端缓冲区溢 出,对于list、set、hash、zset等类型操作,以64个元素为界拆分为多条。AOF重写过程可以手动触发和自动触发:
·手动触发:直接调用bgrewriteaof命令·自动触发:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数确定自动触发时机。
auto-aof-rewrite-minsize:表示运行AOF重写时文件最小体积,默认 为64MB。 auto-aof-rewrite-percentage:代表当前AOF文件空间 (aof_current_size)和上一次重写后AOF文件空间(aof_base_size)的比值。 自动触发时机 = aof_current_size > auto-aof-rewrite-minsize &&(aof_current_size - aof_base_size)/ aof_base_size >= auto-aof-rewritepercentage其中aof_current_size和aof_base_size可以在info Persistence统计信息中查看。
当触发AOF重写时,内部做了哪些事呢?
1)执行AOF重写请求 :如果当前进程正在执行AOF重写,请求不执行并返回如下响应: ERR Background append only file rewriting already in progress如果当前进程正在执行bgsave操作,重写命令延迟到bgsave完成之后再执行,返回如下响应:
Background append only file rewriting scheduled2)父进程执行fork创建子进程,开销等同于bgsave过程。
3.1)主进程fork操作完成后,继续响应其他命令。所有修改命令依然写 入AOF缓冲区并根据appendfsync策略同步到硬盘,保证原有AOF机制正确性。3.2)由于fork操作运用写时复制技术,子进程只能共享fork操作时的内 存数据。由于父进程依然响应命令,Redis使用“AOF重写缓冲区”保存这部 分新数据,防止新AOF文件生成期间丢失这部分数据.
4)子进程根据内存快照,按照命令合并规则写入到新的AOF文件。每次批量写入硬盘数据量由配置aof-rewrite-incremental-fsync控制,默认为 32MB,防止单次刷盘数据过多造成硬盘阻塞。
5.1)新AOF文件写入完成后,子进程发送信号给父进程,父进程更新 统计信息,具体见info persistence下的aof_*相关统计5.2)父进程把AOF重写缓冲区的数据写入到新的AOF文件。
5.3)使用新AOF文件替换老文件,完成AOF重写.重启加载
AOF和RDB文件都可以用于服务器重启时的数据恢复。1)AOF持久化开启且存在AOF文件时,优先加载AOF文件,打印如下日志:
2)AOF关闭或者AOF文件不存在时,加载RDB文件,打印如下日志:3)加载AOF/RDB文件成功后,Redis启动成功。
4)AOF/RDB文件存在错误时,Redis启动失败并打印错误信.
文件校验: 加载损坏的AOF文件时会拒绝启动,并打印如下日志:
# Bad file format reading the append only file: make a backup of your AOF file, then use ./redis-check-aof --fix 对于错误格式的AOF文件,先进行备份,然后采用redis-check-aof–fix命 令进行修复,修复后使用diff-u对比数据的差异,找出丢失的数据,有些可以人工修改补全。 AOF文件可能存在结尾不完整的情况,比如机器突然掉电导致AOF尾部 文件命令写入不全。Redis为我们提供了aof-load-truncated配置来兼容这种情 况,默认开启。加载AOF时,当遇到此问题时会忽略并继续启动,同时打印如下警告日志: # !!! Warning: short read while loading the AOF file !!! # !!! Truncating the AOF at offset 397856725 !!! # AOF loaded anyway because aof-load-truncated is enabled 问题定位与优化 :Redis持久化功能一直是影响Redis性能的高发地fork操作
当Redis做RDB或AOF重写时,一个必不可少的操作就是执行fork操作创 建子进程,对于大多数操作系统来说fork是个重量级错误。虽然fork创建的子进程不需要拷贝父进程的物理内存空间,但是会复制父进程的空间内存页 表。例如对于10GB的Redis进程,需要复制大约20MB的内存页表,因此fork 操作耗时跟进程总内存量息息相关,如果使用虚拟化技术,特别是Xen虚拟 机,fork操作会更耗时。fork耗时问题定位:对于高流量的Redis实例OPS可达5万以上,如果fork 操作耗时在秒级别将拖慢Redis几万条命令执行,对线上应用延迟影响非常 明显。正常情况下fork耗时应该是每GB消耗20毫秒左右。可以在info stats统 计中查latest_fork_usec指标获取最近一次fork操作耗时,单位微秒。
如何改善fork操作的耗时: 1)优先使用物理机或者高效支持fork操作的虚拟化技术,避免使用 Xen。2)控制Redis实例最大可用内存,fork耗时跟内存量成正比,线上建议 每个Redis实例内存控制在10GB以内.
3)合理配置Linux内存分配策略,避免物理内存不足导致fork失败.
4)降低fork操作的频率,如适度放宽AOF自动触发时机,避免不必要的全量复制等。
子进程开销监控和优化
子进程负责AOF或者RDB文件的重写,它的运行过程主要涉及CPU、内存、硬盘三部分的消耗。 1.CPU ·CPU开销分析。子进程负责把进程内的数据分批写入文件,这个过程 属于CPU密集操作,通常子进程对单核CPU利用率接近90%. CPU消耗优化:Redis是CPU密集型服务,不要做绑定单核CPU操作。 由于子进程非常消耗CPU,会和父进程产生单核资源竞争。 不要和其他CPU密集型服务部署在一起,造成CPU过度竞争。如果部署多个Redis实例,尽量保证同一时刻只有一个子进程执行重写工作,可以参考redis多实例部署”。内存消耗优化:
1)同CPU优化一样,如果部署多个Redis实例,尽量保证同一时刻只有一个子进程在工作。2)避免在大量写入时做子进程重写操作,这样将导致父进程维护大量页副本,造成内存消耗。
3) Linux kernel在2.6.38内核增加了Transparent Huge Pages(THP),支持 huge page(2MB)的页分配,默认开启。当开启时可以降低fork创建子进程 的速度,但执行fork之后,如果开启THP,复制页单位从原来4KB变为 2MB,会大幅增加重写期间父进程内存消耗。建议设置“sudo echo never>/sys/kernel/mm/transparent_hugepage/enabled”关闭THP。
2.内存
·内存消耗分析。子进程通过fork操作产生,占用内存大小等同于父进 程,理论上需要两倍的内存来完成持久化操作,但Linux有写时复制机制 (copy-on-write)。父子进程会共享相同的物理内存页,当父进程处理写请 求时会把要修改的页创建副本,而子进程在fork操作过程中共享整个父进程内存快照。
3.硬盘
a)不要和其他高硬盘负载的服务部署在一起。如:存储服务、消息队列服务等。 b)AOF重写时会消耗大量硬盘IO,可以开启配置no-appendfsync-onrewrite,默认关闭。表示在AOF重写期间不做fsync操作。配置no-appendfsync-on-rewrite=yes时,在极端情况下可能丢失整个AOF重写期间的数据。 c)当开启AOF功能的Redis用于高流量写入场景时,如果使用普通机械 磁盘,写入吞吐一般在100MB/s左右,这时Redis实例的瓶颈主要在AOF同步硬盘上。 d)对于单机配置多个Redis实例的情况,可以配置不同实例分盘存储 AOF文件,分摊硬盘写入压力。AOF追加阻塞
当开启AOF持久化时,常用的同步硬盘的策略是everysec,用于平衡性 能和数据安全性。对于这种方式,Redis使用另一条线程每秒执行fsync同步 硬盘。当系统硬盘资源繁忙时,会造成Redis主线程阻塞,阻塞流程分析:
1)主线程负责写入AOF缓冲区。 2)AOF线程负责每秒执行一次同步磁盘操作,并记录最近一次同步时间。3)主线程负责对比上次AOF同步时间:
·如果距上次同步成功时间在2秒内,主线程直接返回。 ·如果距上次同步成功时间超过2秒,主线程将会阻塞,直到同步操作完成。通过对AOF阻塞流程可以发现两个问题:
1)everysec配置最多可能丢失2秒数据,不是1秒。 2)如果系统fsync缓慢,将会导致Redis主线程阻塞影响效率AOF阻塞问题定位:发生AOF阻塞时,Redis输出如下日志,
Asynchronous AOF fsync is taking too long (disk is busy). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis
优化AOF追加阻塞问题主要是优化系统硬盘负载。
Redis单线程架构导致无法充分利用CPU多核特性,通常的做法是在一 台机器上部署多个Redis实例。当多个实例开启AOF重写后,彼此之间会产 生对CPU和IO的竞争。因此需要采用一种 措施,把子进程工作进行隔离。Redis在info Persistence中为我们提供了监控 子进程运行状况的度量指标
我们基于以上指标,可以通过外部程序轮询控制AOF重写操作的执行, 整个过程如图5-6所示。
流程说明:
1)外部程序定时轮询监控机器(machine)上所有Redis实例。 2)对于开启AOF的实例,查看(aof_current_sizeaof_base_size)/aof_base_size确认增长率。 3)当增长率超过特定阈值(如100%),执行bgrewriteaof命令手动触发 当前实例的AOF重写。 4)运行期间循环检查aof_rewrite_in_progress和 aof_current_rewrite_time_sec指标,直到AOF重写结束。 5)确认实例AOF重写完成后,再检查其他实例并重复2)~4)步操作。 从而保证机器内每个Redis实例AOF重写串行化执行。3)混合使用 利用RDB做快照,AOF做日志追加。
一般都整合使用。
强一直性会先去同步到备,成功后才返回给用户成功,默认先返回成功再同步。
分散压力模式下, 用户先把多个redis注册到槽位的不同段位里,用户连到redis,然后通过算法找到映射,如果不是本机,返回用户是哪个redis让用户去连。
Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:
它先以 MULTI 开始一个事务, 然后将多个命令入队到事务中, 最后由 EXEC 命令触发事务, 一并执行事务中的所有命令,如果要停止事务的执行,可以使用discard命令代替exec命令即可。
一个事务从开始到执行会经历以下三个阶段:
执行时发生错误:
1.语法错误 :整个事务无法 执行
2.运行时错误: 比如set类型的值,但是用zadd命令代替sadd命令,那么正确的命令执行,错误的报错。可以看到Redis并不支持回滚功能。
有些应用场景需要在事务之前,确保事务中的key没有被其他客户端修 改过,才执行事务,否则不执行(类似乐观锁)。Redis提供了watch命令来 解决这类问题。
可以看到“客户端-1”在执行multi之前执行了watch命令,“客户 端-2”在“客户端-1”执行exec之前修改了key值,造成事务没有执行(exec结果 为nil)。
以下是一个事务的例子
redis 127.0.0.1:6379> MULTIOK redis 127.0.0.1:6379> SET book-name "Mastering C++ in 21 days"QUEUED redis 127.0.0.1:6379> GET book-nameQUEUEDredis 127.0.0.1:6379> SADD tag "C++" "Programming" "Mastering Series"QUEUEDredis 127.0.0.1:6379> SMEMBERS tagQUEUEDredis 127.0.0.1:6379> EXEC1) OK2) "Mastering C++ in 21 days"3) (integer) 34) 1) "Mastering Series" 2) "C++" 3) "Programming"
单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令发生运行时错误的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。
序号 命令及描述1 DISCARD取消事务,放弃执行事务块内的所有命令。2 EXEC执行所有事务块内的命令。3 MULTI标记一个事务块的开始。4 UNWATCH取消 WATCH 命令对所有 key 的监视。5 WATCH key [key ...]监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务。这意味着通常情况下一个请求会遵循以下步骤:
1)发送命令
2)命令排队 3)命令执行 4)返回结果其中1)+4)称为Round Trip Time(RTT,往返时间)。Redis提供了批量操作命令(例如mget、mset等),有效地节约RTT。但 大部分命令是不支持批量操作的,例如要执行n次hgetall命令,并没有 mhgetall命令存在,需要消耗n次RTT,Pipeline(流水线)机制能改善上面这类问题,它能将一组Redis命令进 行组装,通过一次RTT传输给Redis,再将这组Redis命令的执行结果按顺序返回给客户端,
redis-cli的–pipe选项实际上就是使用Pipeline机制,
原生批量命令与Pipeline对比:可以使用Pipeline模拟出批量操作的效果,但是在使用时要注意它与原生批量命令的区别,具体包含以下几点:
·原生批量命令是原子的,Pipeline是非原子的。 ·原生批量命令是一个命令对应多个key,Pipeline支持多个命令。 ·原生批量命令是Redis服务端支持实现的,而Pipeline需要服务端和客户端的共同实现。$(echo -en "PING\r\n SET runoobkey redis\r\nGET runoobkey\r\nINCR visitor\r\nINCR visitor\r\nINCR visitor\r\n"; sleep 10) | nc localhost 6379+PONG+OKredis:1:2:3以上实例中我们通过使用 PING 命令查看redis服务是否可用,之后我们设置了 runoobkey 的值为 redis,然后我们获取 runoobkey 的值并使得 visitor 自增 3 次。在返回的结果中我们可以看到这些命令一次性向 redis 服务提交,并最终一次性读取所有服务端的响应
管道技术最显著的优势是提高了 redis 服务的性能,开启管道后,我们的速度效率提升。但是管道中间可能会存在部分失败的情况,也就是说不能保证每条命令都能执行成功,如果中间有命令出现错误,redis不会中断执行,而是直接执行下一条命令,然后将所有命令的执行结果(执行成功或者执行失败)放到列表中统一返回,如果需要每条命令都执行成功,我们在批量执行过程中需要监控执行数量和返回的成功数量是否一致。
在Redis中执行Lua脚本有两种方法:eval和evalsha,版本:>= 2.6.0
Eval :redis 127.0.0.1:6379> EVAL script numkeys key [key ...] arg [arg ...]
redis 127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second1) "key1"2) "key2"3) "first"4) "second"eval 'return "hello world"' 0 "hello world"eval 'return "hello world" .. KEYS[1] .. KEYS[2]' 2 YES SIR"hello worldYESSIR"eval 'return "hello world" .. ARGV[1] .. ARGV[2]' 0 3 4"hello world34"evalsha:除了使用eval,Redis还提供了evalsha命令来执行Lua脚本。如图3-8所 示,首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验和, evalsha命令使用SHA1作为参数可以直接执行对应Lua脚本,避免每次发送 Lua脚本的开销,脚本也会常驻在服务端,脚本功能得到了复用。
加载脚本:script load命令可以将脚本内容加载到Redis内存中,例如下 面将lua_get.lua加载到Redis中,得到SHA1为:“7413dc2440db1fea7c0a0bde841fa68eefaf149c”
# redis-cli script load “$(cat lua_get.lua)” "7413dc2440db1fea7c0a0bde841fa68eefaf149c执行脚本:evalsha的使用方法如下,参数使用SHA1值,执行逻辑和 eval一致。
evalsha 脚本SHA1值 key个数 key列表 参数列表127.0.0.1:6379> evalsha 7413dc2440db1fea7c0a0bde841fa68eefaf149c 1 redis world
“hello redisworld”Lua可以使用redis.call函数实现对Redis的访问,例如下面代码是Lua使用 redis.call调用了Redis的set和get操作:
redis.call(“set”, “hello”, “world”) redis.call(“get”, “hello”
除此之外Lua还可以使用redis.pcall函数实现对Redis的调用,redis.call和 redis.pcall的不同在于,如果redis.call执行失败,那么脚本执行结束会直接返 回错误,而redis.pcall会忽略错误继续执行脚本,Lua可以使用redis.log函数将Lua脚本的日志输出到Redis的日志文件中,但是一定要控制日志级别。
Lua脚本功能为Redis开发和运维人员带来如下三个好处:
·Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。 ·Lua脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这 些命令常驻在Redis内存中,实现复用的效果。 ·Lua脚本可以将多条命令一次性打包,有效地减少网络开销。如:
local mylist = redis.call(“lrange”, KEYS[1], 0, -1)
local count = 0 for index,key in ipairs(mylist) do redis.call(“incr”,key) count = count + 1 end return count将上述脚本写入lrange_and_mincr.lua文件中,并执行如下操作,redis-cli --eval lrange_and_mincr.lua key
(integer) 5
Redis如何管理Lua脚本:
(1)script load : 此命令用于将Lua脚本加载到Redis内存中 script load script
(2)script exists 此命令用于判断sha1是否已经加载到Redis内存中:scripts exists sha1 [sha1 …]
127.0.0.1:6379> script exists a5260dd66ce02462c5b5231c727b3f7772c0bcc5 1) (integer) 1 返回结果代表sha1[sha1…]被加载到Redis内存的个数。 (3)script flush 此命令用于清除Redis内存已经加载的所有Lua脚本,在执行script flush 后,a5260dd66ce02462c5b5231c727b3f7772c0bcc5不再存在: (4)script kill 此命令用于杀掉正在执行的Lua脚本。如果Lua脚本比较耗时,甚至Lua 脚本存在问题,那么此时Lua脚本的执行会阻塞Redis,直到脚本执行完毕或 者外部进行干预将其结束。Redis提供了一个lua-time-limit参数,默认是5秒,它是Lua脚本的“超时 时间”,但这个超时时间仅仅是当Lua脚本时间超过lua-time-limit后,向其他命令调用发送BUSY的信号,但是并不会停止掉服务端和客户端的脚本执 行,所以当达到lua-time-limit值之后,其他客户端在执行正常的命令时,将 会收到“Busy Redis is busy running a script”错误,并且提示使用script kill或者 shutdown nosave命令来杀掉这个busy的脚本:此时Redis已经阻塞,无法处理正常的调用,这时可以选择继续等待, 但更多时候需要快速将脚本杀掉。使用shutdown save显然不太合适,所以选 择script kill,当script kill执行之后,客户端调用会恢复:127.0.0.1:6379> script kill OK但是有一点需要注意,如果当前Lua脚本正在执行写操作,那么script kill将不会生效。例如,我们模拟一个不停的写操作:
while 1==1 do
redis.call(“set”,“k”,“v”) end上面提示Lua脚本正在向Redis执行写命令,要么等待脚本执行结束要么 使用shutdown save停掉Redis服务。可见Lua脚本虽然好用,但是使用不当破坏性也是难以想象的.
Redis提供了基于“发布/订阅”模式的消息机制,此种模式下,消息发布 者和订阅者不进行直接通信,发布者客户端向指定的频道(channel)发布消 息,订阅该频道的每个客户端都可以收到该消息,如图3-16所示。Redis提供了若干命令支持该功能,在实际应用开发时,能够为此类问题提供实现方法。
1.发布消息 publish channel message, 下面操作会向channel:sports频道发布一条消息“Tim won the championship”,返回结果为订阅者个数 : publish channel:sports “Tim won the championship”
2.订阅消息 订阅者可以订阅一个或多个频道,下面操作为当前客户端订阅了 channel:sports频道: subscribe channel:sports 当此时另一个客户端发布一条消息: publish channel:sports “James lost the championship” 订阅者客户端会收到如下消息
127.0.0.1:6379> subscribe channel:sports Reading messages… (press Ctrl-C to quit) …
“message”
“channel:sports”
“James lost the championship”
如果有多个客户端同时订阅了channel:sports,整个过程如图3-17所示。
有关订阅命令有两点需要注意:
1.客户端在执行订阅命令之后进入了订阅状态,只能接收subscribe、 psubscribe、unsubscribe、punsubscribe的四个命令。2.新开启的订阅客户端,无法收到该频道之前的消息,因为Redis不会对发布的消息进行持久化。
和很多专业的消息队列系统(例如Kafka、RocketMQ)相比,Redis的发布订阅略显粗糙,例如无法实现消息堆积和回溯。但胜在足够简单。
3.取消订阅 unsubscribe [channel [channel …]]
4.4.按照模式订阅和取消订阅:除了subcribe和unsubscribe命令,Redis命令还支持glob风格的订阅命令 psubscribe和取消订阅命令punsubscribe,例如下面操作订阅以it开头的所有频道:
psubscribe pattern [pattern…]
punsubscribe [pattern [pattern …]]
psubscribe it*
5.查询订阅
(1)查看活跃的频道 :pubsub channels [pattern] 所谓活跃的频道是指当前频道至少有一个订阅者,其中[pattern]是可以
指定具体的模式: pubsub channels 或 pubsub channels channel:r(2)查看频道订阅数 :pubsub numsub [channel …]
(3)查看模式订阅数 :pubsub numpatCEO:Redis3.2版本提供了GEO(地理信息定位)功能,支持存储地理位置信息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能,对于需 要实现这些功能的开发者来说是一大福音.
geoadd key longitude latitude member [longitude latitude member …] longitude、latitude、member分别是该地理位置的经度、纬度、成员,表 3-7展示5个城市的经纬度cities:locations是上面5个城市地理位置信息的集合,现向其添加北京的地理位置信息
127.0.0.1:6379> geoadd cities:locations 116.28 39.55 beijing (integer) 1
返回结果代表添加成功的个数,如果cities:locations没有包含beijing,那么返回结果为1,如果已经存在则返回0: 如果需要更新地理位置信息,仍然可以使用geoadd命令,虽然返回结果 为0。geoadd命令可以同时添加多个地理位置信息.2.获取地理位置信息 geopos key member [member …]
3.获取两个地理位置的距离 geodist key member1 member2 [unit]
其中unit代表返回结果的单位,包含以下四种:
·m(meters)代表米。 ·km(kilometers)代表公里。 ·mi(miles)代表英里 ·ft(feet)代表尺。下面操作用于计算天津到北京的距离,并以公里为单位:
127.0.0.1:6379> geodist cities:locations tianjin beijing km “89.2061” 4.获取指定位置范围内的地理信息位置集合 georadius key longitude latitude radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]georadiusbymember key member radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]
georadius和georadiusbymember两个命令的作用是一样的,都是以一个地 理位置为中心算出指定半径内的其他地理信息位置,不同的是georadius命令 的中心位置给出了具体的经纬度,georadiusbymember只需给出成员即可。其 中radiusm|km|ft|mi是必需参数,指定了半径(带单位),这两个命令有很多可选参数,如下所示·withcoord:返回结果中包含经纬度。
·withdist:返回结果中包含离中心节点位置的距离。 ·withhash:返回结果中包含geohash,有关geohash后面介绍。 ·COUNT count:指定返回结果的数量。 ·asc|desc:返回结果按照离中心节点的距离做升序或者降序。 ·store key:将返回结果的地理位置信息保存到指定键。 ·storedist key:将返回结果离中心节点的距离保存到指定键。 下面操作计算五座城市中,距离北京150公里以内的城市: 127.0.0.1:6379> georadiusbymember cities:locations beijing 150 km5.获取geohash geohash key member [member …] Redis使用geohash[3]将二维经纬度转换为一维字符串,下面操作会返回 beijing的geohash值。127.0.0.1:6379> geohash cities:locations beijing -> 1) "wx4ww02w070
geohash有如下特点:
GEO的数据类型为zset,Redis将所有地理位置信息的geohash存放在zset中。字符串越长,表示的位置更精确,表3-8给出了字符串长度对应的精 度,例如geohash长度为9时,精度在2米左右。两个字符串越相似,它们之间的距离越近,Redis利用字符串前缀匹配算法实现相关的命令.
geohash编码和经纬度是可以相互转换的.
Redis正是使用有序集合并结合geohash的特性实现了GEO的若干命令。
6.删除地理位置信息 zrem key member .GEO没有提供删除成员的命令,但是因为GEO的底层实现是zset,所以 可以借用zrem命令实现对地理位置信息的删除分区是分割数据到多个Redis实例的处理过程,因此每个实例只保存key的一个子集。
分区的优势
分区的不足
redis的一些特性在分区方面表现的不是很好:
分区类型
Redis 有两种类型分区。 假设有4个Redis实例 R0,R1,R2,R3,和类似user:1,user:2这样的表示用户的多个key,对既定的key有多种不同方式来选择这个key存放在哪个实例中。也就是说,有不同的系统来映射某个key到某个Redis服务。
范围分区
最简单的分区方式是按范围分区,就是映射一定范围的对象到特定的Redis实例。
比如,ID从0到10000的用户会保存到实例R0,ID从10001到 20000的用户会保存到R1,以此类推。
这种方式是可行的,并且在实际中使用,不足就是要有一个区间范围到实例的映射表。这个表要被管理,同时还需要各 种对象的映射表,通常对Redis来说并非是好的方法。
哈希分区
另外一种分区方法是hash分区。这对任何key都适用,也无需是object_name:这种形式,像下面描述的一样简单:
缓存穿透是指查询一个一定不存在的数据,由于缓存不命中,接着查询数据库也无法查询出结果,因此也不会写入到缓存中,这将会导致每个查询都会去请求数据库,造成缓存穿透;
解决:
用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
如果一个查询返回的数据为空,把这个空结果进行缓存,然后对这个数据设置过期时间,但它的过期时间会很短,比如60秒,最长不超过五分钟。
待验证解决方案:设置两层redis层,不同的过期时间,检测ip攻击次数,封ip
缓存雪崩是指,由于缓存层承载着大量请求,有效的保护了存储层,但是如果缓存层由于某些原因整体不能提供服务,于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。
解决:集群里的主从配置。访问进行限流。
一款java操作redis的工具,https://mvnrepository.com/artifact/redis.clients/jedis jar包下载
在实际项目中比较推荐使用try catch finally的形式来进行代码的书写: 一方面可以在Jedis出现异常的时候(本身是网络操作),将异常进行捕获 或者抛出;另一个方面无论执行成功或者失败,将Jedis连接关闭掉,在开发中关闭不用的连接资源是一种好的习惯。
Jedis本身没有提供序列化的工具,也就是说开发者需要自己 引入序列化的工具,序列化的工具有很多,例如XML、Json、谷歌的 Protobuf、Facebook的Thrift等等。
实例:
import redis.clients.jedis.Jedis;import java.util.List;import java.util.Map;import java.util.Set;public class JedisTest { public static void main(String[] args) { test(); } public static void test() { //获取连接 Jedis jedis = new Jedis("localHost", 6379);//如果不写参数,默认就是localHost 和 6379 //操作 jedis.set("a", "1"); //获取String value = jedis.get("a"); System.out.println(value);//可以使用setex()方法存储可以指定过期时间key valuejedis.setex("b", 20, "2");//将key b value 2 存入redis,并且20秒后自动删除键值对//hash类型 jedis.hset("c", "1", "1");jedis.hset("c", "2", "2");jedis.hset("c", "3", "3");String hvalue = jedis.hget("c", "1");System.out.println(hvalue);Mapm = jedis.hgetAll("c");//获取所有键值对 for(Map.Entry e : m.entrySet()) { System.out.println(e.getKey() + "---" + e.getValue()); } //list类型 jedis.lpush("d", "1", "2", "3");//可以存多个从左边存 jedis.rpush("d", "4", "5", "6");//可以存多个从右边存 List l = jedis.lrange("d", 0, -1); l.stream().forEach(System.out::println); String elemt = jedis.lpop("d"); System.out.println(elemt); elemt = jedis.rpop("d"); System.out.println(elemt); //set类型 jedis.sadd("e", "1","2","3"); jedis.srem("e", "1"); Set s = jedis.smembers("e"); System.out.println(s); //sortSet jedis.zadd("f", 10, "1"); jedis.zadd("f", 5, "2"); jedis.zadd("f", 15, "3"); jedis.zadd("f", 20, "4"); jedis.zrem("f", "4"); Set zs = jedis.zrange("f", 0, -1); System.out.println(zs); //关闭连接 jedis.close(); }}
JedisPool
public static void test2() { //创建一个配置对象 JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(50);//最大允许连接数 jedisPoolConfig.setMaxIdle(10);//最大空闲连接 //如池子中最大连接数、最大空闲连接数、 //最小空闲连接数、连接活性检测 //创建连接池对象 JedisPool jedisPool = new JedisPool(jedisPoolConfig); Jedis jedis = null; try { // 1. 从连接池获取jedis对象 jedis = jedisPool.getResource(); // 2. 执行操作 jedis.get("hello"); } catch (Exception e) { logger.error(e.getMessage(),e); } finally { if (jedis != null) { // 如果使用JedisPool,close操作不是关闭连接,代表归还连接池 jedis.close(); } } }
JedisPool工具类JedisPoolUtils(自己实现)
import redis.clients.jedis.Jedis;import redis.clients.jedis.JedisPool;import redis.clients.jedis.JedisPoolConfig;import java.io.IOException;import java.io.InputStream;import java.util.Properties;/** * JedisPool工具类 * jedis.propertities在src目录下 * maxTotle=50 * maxIdle=10 */public class JedisPoolUtils { private static JedisPool jedisPool; static{ //类加载时读取配置文件 通过类加载器 InputStream in = JedisPoolUtils.class.getClassLoader().getResourceAsStream("jedis.propertities"); //创建propertities对象 Properties properties = new Properties(); //关联文件 try { properties.load(in); } catch (IOException e) { e.printStackTrace(); } //获取数据,设置到JedisPool中 JedisPoolConfig config = new JedisPoolConfig(); config.setMaxTotal(Integer.parseInt(properties.getProperty("maxTotle"))); config.setMaxIdle(Integer.parseInt(properties.getProperty("maxIdle)"))); //初始化 jedisPool = new JedisPool(config); } public static Jedis getJedisPool() { return jedisPool.getResource(); }}
客户端连接Redis使用TCP协议,并且Redis制定了RESP(REdis Serialization Protocol,Redis序列化协议)实现客户端与服务端的正常交互,这种协议简单高效,既能够被机器解析,又容易 被人类识别。直接连的方式每次需要建立TCP连接,而连接池的方式可以预先初始化好Jedis连接,每次只需要从Jedis连接池借用即可,用完了在归还给池子,而借用和归还都是本地进行,只有少量的并发开销,远远小于新建TCP连接的开销,另外直连的方式无法限制Jedis对象个数,在极端情况下可能会造成连接泄漏,而连接池形式可以有效的保护和控制资源的使用,但是直连的方式也有自己的优势。
jedis的内部结构
jedis以输入命令参数是否为二进制,将请求的具体实现部署在两个类中,例如Jedis和BinaryJedis,client和Binary和BinaryClient,与Redis服务器的连接信息封装在Client的积累,Connection中,BinaryClient中提供了client、PipeLine、Transcation变量,对应3种请求模式。PipeLine和Transcation很相似,区别在于Transcation实例化的时候就会自动发送MULTI命令,开启事物模式,而PipeLine则按情况手动开启,他们均依靠client发送命令。
client请求模式
client模式下,发送请求会先通过connect()方法判断是否已经连接,若未连接:
1.实例化Socket并配置
2.连接Socket获取OutputStream和InputSteam
3.如果SSL连接,则会通过SSLSocketFactory创建socket连接。
protocol是一个通讯工具类,将Redis的各类执行关键字存储为静态变量,可以直观调用命令,同时,将命令包装成符合Redis统一请求的协议,回复消息的处理也在这个类进行,先通过通讯协议提取出当次请求的回复消息,将Object类型的消息,格式化为String,list等具体类型,如果消息有Error则以异常形式抛出。
pipeline和Transation模式
利用jedis对象生成一个pipeline对象,直接可以调用 jedis.pipelined()。将del命令封装到pipeline中,可以调用pipeline.del(String key),这个 方法和jedis.del(String key)的写法是完全一致的,只不过此时不会真正的执行命令。使用pipeline.sync()完成此次pipeline对象的调用。除了pipeline.sync(),还可以使用pipeline.syncAndReturnAll()将 pipeline的命令进行返回。
Pipeline和Transation都继承MultiKeyPipelineBase,MultiKeyPipelineBase 和PipelineBase的区别在于处理的命令不同,内部均调用Client发送命令,Pipeline有一个内部类对象MultiResponseBuilder,当事物结束时,会以list的形式一次性返回所有命令的执行结果,这个对象就是用来存储当PipeLine开始其模式后,在事物结束时,存储所有的返回结果。
Queable用一个linkedlist装入每个命令的返回结果,Response是一个泛型,set(Object data)方法传入格式化前的结果,get()方法返回格式化之后的结果。
Client中sendConmand时,会同时执行PipelinedCommands++,记录发送条目,之后返回一个Response,并将这个实例塞入了PipeLinedResponses队列中。
Response主要有三个属性
1.格式化前的回复消息data
2.格式化后的回复消息response
3.格式化方式builder
刚发消息时,Response的data和response都是空值,只有格式化的builder,syn()用于一次性读取所有回复,首先调用 clien的getAll方法,getAll()方法根据自己记录的PipelinedCommands和Redis通讯协议,读取相同条目的回复消息到一个list,并返回给pipeline,随后遍历这个list逐个将消息赋值给pipelinedResponses中的每个Response的data,执行Response.get方法时,date里已经有值了。但是是Object类型,所以还要调用build()方法,做一次数据转换,返回格式化的数据。
Transation的exec()方法和sync方法类似。
Jedis的Lua脚本
Jedis中执行Lua脚本和redis-cli十分类似,Jedis提供了三个重要的函数实 现Lua脚本的执行: Object eval(String script, int keyCount, String… params)eval函数有三个参数,分别是:
·script:Lua脚本内容。 ·keyCount:键的个数。 ·params:相关参数KEYS和ARGVString key = "hello"; String script = "return redis.call('get',KEYS[1])"; Object result = jedis.eval(script, 1, key); // 打印结果为world System.out.println(result)
Object evalsha(String sha1, int keyCount, String… params)
String scriptLoad(String script)
scriptLoad和evalsha函数要一起使用,首先使用scriptLoad将脚本加载到 Redis中,代码如下:
evalsha函数用来执行脚本的SHA1校验和,它需要三个参数:
·scriptSha:脚本的SHA1。 ·keyCount:键的个数。 ·params:相关参数KEYS和ARGVString scriptSha = jedis.scriptLoad(script);Stirng key = "hello"; Object result = jedis.evalsha(scriptSha, 1, key); // 打印结果为world System.out.println(result);
关系型数据库和非关系型数据库在使用场景上差别比较大,所以并不存在孰强孰弱,只有结合自身的业务特点才能发挥出这两类数据库的优势,下面说说这两类数据库的一些特点:
首先一般非关系型数据库是基于CAP模型,而传统的关系型数据库是基于ACID模型的。 \1. 数据存储结构: 首先关系型数据库一般都有固定的表结构,并且需要通过DDL语句来修改表结构,不是很容易进行扩展,而非关系型数据库的存储机制就有很多了,比如基于文档的,K-V键值对的,还有基于图的等,对于数据的格式十分灵活没有固定的表结构,方便扩展,因此如果业务的数据结构并不是固定的或者经常变动比较大的,那么非关系型数据库是个好的选择\2. 可扩展性
传统的关系型数据库给人一种横向扩展难,不好对数据进行分片等,而一些非关系型数据库则原生就支持数据的水平扩展(比如mongodb的sharding机制),并且这可能也是很多NoSQL的一大卖点,其实象Mysql这种关系型数据库的水平扩展也并不是难,即使NoSQL水平扩展容易但对于向跨分片进行joins这种场景都没有什么太好的解决办法,不管是关系型还是非关系型数据库,解决水平扩展或者跨分片Joins这种场景,在应用层和数据库层中间加一层中间件来做数据处理也许是个好的办法\3. 数据一致性
非关系型数据库一般强调的是数据最终一致性,而不没有像ACID一样强调数据的强一致性,从非关系型数据库中读到的有可能还是处于一个中间态的数据,因此如果你的业务对于数据的一致性要求很高,那么非关系型数据库并不一个很好的选择,非关系型数据库可能更多的偏向于OLAP场景,而关系型数据库更多偏向于OLTP场景出自:https://blog.csdn.net/weixin_39407066/article/details/107447707?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522159822486119195188312616%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=159822486119195188312616&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-7-107447707.pc_ecpm_v3_pc_rank_v3&utm_term=redis&spm=1018.2118.3001.4187