写在前面
简介
上篇文章介绍了Redis的安装,官网上是这样介绍的,Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。
这段话高度概括了Redis的功能,它支持多种数据结构,提供多种可持久化方案,并且提供分布式和集群的部署方案,保证高可用性。
单看上面的介绍,咱们可能还感觉不到redis的强大之处。下面将同是作为内存存储的开源的memcache与Redis做一个对比。
与Memcached的对比
数据来源于 https://db-engines.com/。
下列图中第二列为Memcached第三列为Redis。
- 在缓存数据库排名中Redis排名第一,Memcached排名第四
- 上图中Typing类型对比,Memcached那一栏为no(其实是只支持string类型),而Redis支持多种类型。
- Redis支持二级索引,Memcached不支持。他们同为NoSql。Redis比Memcached支持的语言多得多,当面临多语言开发团队的时候Redis会有更好的支持性。
- Redis 服务器支持Lua脚本执行。Redis 使用单个 Lua 解释器去运行所有脚本,并且, Redis 也保证脚本会以原子性(atomic)的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。这和使用 MULTI / EXEC 包围的事务很类似。在其他别的客户端看来,脚本的效果(effect)要么是不可见的(not visible),要么就是已完成的(already completed)。而Memcached不支持任何语言脚本。
- Redis提供分布式和高可用的集群部署方案,而Memcached不支持。
- Redis提供一些原子性操作,而Memcached不支持。
- Redis支持持久化,而Memcached不支持。
redis的命令组
命令组介绍
redis的命令十分丰富,其分为14个命令组,如下:
- cluster :跟集群操作有关的命令组,比如:cluster info等
- connection:跟链接有关的命令组,比如:auth,quit,select 等
- geo:跟地理位置有关的命令组,比如:geoadd,geohash等
- hashes:操作数据类型为map的命令组,比如:hset,hmset,hget等
- HyperLogLog:获取基数的命令组(一般用于大数据统计等),比如:pfadd等
- keys:操作key的命令组,比如:del,object,expire等
- lists:操作数据类型为列表的命令组,比如lset,lget,lpush,lpop等
- pub/sub:发布订阅操作指令组,比如pubsub,publish等
- scripting:执行lua脚本的操作指令组,比如eval,evalsha等
- server:操作服务端数据的指令组,比如:bgrewriteaof,bgsave等
- sets:操作数据类型为集合的指令组,比如 sadd,scard,sdiff等
- sorted sets:操作数为有序集合的指令组,比如zadd,zcard等
- streams:流操作指令组,比如:xack,xadd等
- strings:操作数据类型为字符的指令组,比如:set,get,mset,setnx等
- transcations:事务的指令组,比如:exec,watch等
strings命令组
对字符串的操作指令
在客户端通过 help @string 可以查看所有string的操作指令
- set指令可以给一个key设置一个值,后面可以跟 [ex seconds(有效期单位为秒) | px milliseconds(有效期单位为毫秒)] | [nx(没有key是设置成功) | xx(有key是设置成功)]
下面是在客户端的运行事例即结果。
set key value nx , set key value ex seconds, set key value px millisenconds value的设置方式也可以通过 setnx key value,setex key seconds value, psetex key milliseconds value实现。
setnx指令保证只能有一个线程可以将key设置成功,所以该指令经常用于分布式锁,为了避免死锁,可以设置超时时间。
不过没有setxx
对于过期的key,Redis并不会马上主动清除key,其实Redis的清楚策略有三种:
- 被动删除:当客户端操作一个过期的key时,才会主动删除key,比如:get,set,del, incr等
- 主动删除:服务端会定期检查整理自身的资源,这个过程中会删除一批过期的key
- 超过maxmemory时,会主动删除过期的key
- setrange 指令可以根据给定的下标替换key的值。如果key为空并且不是从第0个位置开始替换的则用0补充。
- get指令可以获取通过set指令设置的key的值。
- getset指令除了设置key的值还返回老的值。
- getrange指令可以截取指定位置的字符串。redis中除了正向索引(1,2,3…) 还有反向索引(…-3,-2,-1),两者可以结合使用。
- append指令可以在给定个key后面追加内容
除了设置单个key的,redis还支持设置多个值的指令。
- mget指令可以一次获取多个key的值
- mset指令可以一次设置多个key的值
- msetnx指令可以一次性设置多个值,仅在设置的key都不存在时成功。如果有多余一个key存在时那么所有的key都设置不成功。
- strlen 指令返回指定key的字节个数
通过上面的运行结果,对于k1的长度是10没有问题,但是k2返回的长度是6 。因为我的客户端是UTF8编码方式,UTF8会将一个汉字用三个字节表示,这里两个汉字所以返回的长度是6 。说明strlen获取的长度是字节的个数。通过get指令获取的k2的值返回的是字节编码。
Redis是一个二进制安全的存储系统,即客户端以什么样的编码格式发给服务端,服务端会不做任何处理的存入系统,当获取时会原封不动的返回给客户端。所以对于同一个key存储获取获取的多个客户端需要保持编码一直,否则可能会出现解码后乱码的情况。
比如对于汉字“中国”UTF8编码的客户端与GBK编码的客户端有不同的编码,所以服务端存储的也会不一样,通过strlen获取的长度也不一样前者长度为 6 后者长度 为 4.下面看一下运行结果:
客户端为UTF8编码的运行结果:
客户端为GBK编码的运行结果:
通过redis-cli --raw链接服务端,get指令会按客户端的编码进行格式化:
可以发现k2的值是乱码,因为设置k2的客户端的编码为UTF8,而get k2的客户端的编码为GBK。
这就是Redis的二进制安全。
对数值类型操作指令
Rredis对数值类型的操作也是放到strings指令组里的。
- decr指令对key进行减一操作
- decrby指令对key减去指定的值
- incr指令对key进行加一操作
- incrby指令对key加上指定的值
在并发的情况下redis保证对key的加减操作具有原子性,不会出现多个线程同时对同一个值进行加减操作。
如果指定的key不存在,那么在执行这些操作之前,会先将它的值设定为0。
如果指定的key中存储的值不是字符串类型(fix:)或者存储的字符串类型不能表示为一个整数,那么执行这个命令时服务器会返回一个错误(eq:(error) ERR value is not an integer or out of range)。
这些操作仅限于64位的有符号整型数据。
通过incr可以实现网站限流的操作:
UNCTION LIMIT_API_CALL(ip):
current = GET(ip)
IF current != NULL AND current > 10 THEN
ERROR "too many requests per second"
ELSE
value = INCR(ip)
IF value == 1 THEN
EXPIRE(value,1)
END
PERFORM_API_CALL()
END
上述方法的思路是,从第一个请求开始设置过期时间为1秒。如果1秒内请求数超过了10个,那么会抛异常。否则,计数器会清零。
上述代码中,可能会进入竞态条件,比如客户端在执行INCR之后,没有成功设置EXPIRE时间。这个ip的key 会造成内存泄漏,直到下次有同一个ip发送相同的请求过来。
把上述INCR和EXPIRE命令写在lua脚本并执行EVAL命令可以避免上述问题(只有redis版本>=2.6才可以使用)。
Redis并没有一个明确的类型来表示整型数据,所以这些操作是一个字符串操作。
在java中数值类型的长度都有固定的字节个数。比如整型为固定的4个字节。但是在redis中并不是这样,9999 与 10000 所占的字节个数不一样(对于浮点数也是一样):
执行这些操作的时候,key对应存储的字符串被解析为10进制的64位有符号整型数据。
事实上,Redis 内部采用整数形式(Integer representation)来存储对应的整数值,所以对该类字符串值实际上是用整数保存,也就不存在存储整数的字符串表示(String representation)所带来的额外消耗。
- incrbyfloat指令对key加上指定的浮点值
没有 decrbyfloat。
bitmap(位图)操作指令
在strings的指令组里面还有一类以bit开头的指令,这些指令是按位来设置一个key的。我们都知道,一个字节是8个bit位,每个bit位都可以用0或者1来表示。
比如 ‘A’的ASCII码值为 0 1000001 在redis中可以这样设置:
- setbit指令可以设置key的value在offset出的bit值。其中偏移量offset的取值范围为 0 < offset < 232 - 1
- getbit指令获取key对应的value在offest处的bit值。当offest超出value的长度的时候总是返回0.
通过上面strlen的返回结果可知,即使通过setbit设置的key,redis还是按字节数统计的,并且是按字节返回的。当通过getbit获取超出value的长度的时候,并不会开辟空间。
- bitcount指令统计value被设置为1的bit数。
bitcount 可以指定start end,但是这个start,end是按字节并不是按位。
- bitfield key get 指令可以获取一个value从指定偏移位置开始截取指定长度的bit位并转为10进制的有符号(或者无符号)的值。
这个指令是区别于 get 和getbit指令的。
比如对于一个key的value是0 1000001 也就是 ‘A’ 。- get key 只能按字节获取,也就是只能返回 ‘A’。
- getbit key offest 只能获取某一位的bit值。
- bitfield key get指令 可以获取任意bit位置的任意长度的bit位,这里定义为域。
对于0 1000001 也就是 ‘A’,想要获取前四位的bit值 0 100
可以这样操作:
bitfield k1 get u4 0 的意思是从key为k1的value中从第0个位置开始截取4(4)位按无符号(u)整数返回
bitfield k1 get i4 0 的意思是从key为k1的value中从第0个位置开始截取4(4)位按有符号(i)整数返回
bitfield 可以将get连续使用。
- bitfield key set 指令可以设置一个value的指定域的值,并返回他的原值。
简单说一下redis是如何将二进制转换为有符号十进制数字的。
对于k1 1 0001000
- 当获取无符号整数时比较简单,就直接按权展开求和即1 * 27 + 0 * 2 6 + 0 * 2 5 + 0 * 2 4 + 1 * 2 3 + 0 * 2 2 + 0 * 2 1 + 0 * 2 0 = 136
- 当获取有符号整数时,若第0位为1,
- 从第1位开始取数 0001000
- 将取到的数字求返 1110111
- 将求返的数字 + 1后 与第0位结合 11111000 ,第0位为符号位 后七位按权展开求和 则为 -120
- 当取有符号整数时,若第0位为0,则直接按权展开求和。
当set的值超过指定域的最大值时,则默认从低位开始截取指定的长度存入库。
bitfield key incrby 可以对value的指定域进行自增或自减(若给定的值为负数)。
当自增(自减)的结果超出域的最大范围的时候redis给了三种处理方式 并且用 overflow 表示
- overflow wrap : 回环算法,适用于有符号和无符号整型两种类型。对于无符号整型,回环计数将对整型最大值进行取模操作(C语言的标准行为)。对于有符号整型,上溢从最负的负数开始取数,下溢则从最大的正数开始取数,例如,如果i8整型的值设为127,自加1后的值变为-128。
- overflow sat : 饱和算法,下溢之后设为最小的整型值,上溢之后设为最大的整数值。例如,i8整型的值从120开始加10后,结果是127,继续增加,结果还是保持为127。下溢也是同理,但量结果值将会保持在最负的负数值。
- overflow fail :失败算法,这种模式下,在检测到上溢或下溢时,不做任何操作。相应的返回值会设为NULL,并返回给调用者。
注意每种溢出(OVERFLOW)控制方法,仅影响紧跟在INCRBY命令后的子命令,直到重新指定溢出(OVERFLOW)控制方法。
如果没有指定溢出控制方法,默认情况下,将使用WRAP算法。
- bitop指令可以对一个或多个key进行按位 与(and)、或(or)、非(not)、异或(xor)操作。除了非(not)只能接受一个key,剩下的三个都可以接收多个key。
- bitpos命令返回value值内第一个为0或者1的bit的位置。
如果我们在空字符串或者0字节的字符串里面查找bit为1的内容,那么结果将返回-1。
如果我们在字符串里面查找bit为0而且字符串只包含1的值时,将返回字符串最右边的第一个空位。如果有一个字符串是三个字节的值为0xff的字符串,那么命令BITPOS key 0将会返回24,因为0-23位都是1。
基本上,我们可以把字符串看成右边有无数个0。
然而,如果你用指定start和end范围进行查找指定值时,如果该范围内没有对应值,结果将返回-1。