1. 简介:

  1. string是Redis最基本的类型,可以理解成与memcached一模一样的类型,一个key对应一个value
  2. string类型是二进制安全的。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象
  3. 一个redis中字符串value最多可以是512M

2. 命令速览:

  1. get key:获取key的value
  2. set key value:设置k-v值
  3. append key value:在key的值后面追加value,返回追加后的value的长度
  4. strlen key:获取key的值长度
  5. setnx key value:如果在key不存在时设置值,如果存在不设置值
    如果key存在返回0,如果key不存在返回1
  6. incr key :将key中储存的数字值+1,只能对数字值操作,否则报错,如果为空,新增值为1
    在这里插入图片描述
  7. decr key:将key中储存的数字值-1,只能对数字值操作,如果为空,新增值为-1
  8. incrby/decrby key 步长:将key中储存的值增减自定义步长
  9. mset key1 value1 key2 value2:一次性设置一个或多个键值对
  10. mget key1 key2 key3:一次获取一个或多个value
  11. msetnx key1 value1 key2 value2:同时设置一个或多个键值对,当且仅当给定的key都不存在
  12. getrange key 起始位置 结束位置:获得范围内的值,包前包后,不修改原值
  13. setrange key 起始位置 value:用value从起始位置开始覆写key的字符串值
  14. setex key 过期时间 value:设置键值的同时设置过期时间,单位秒
  15. getset key value:以新换旧,设置新值同时获得旧值

3. 命令详解:

1. 消失的一号位

2. get:获取key

  • 格式get key
  • 返回值:key对应的value,或者nil(key不存在时),如果key的value不是string,就返回错误,因为GET只处理string类型的values
  • 复杂度:O(1)
redis> GET nonexisting
(nil)
redis> SET mykey "Hello"
OK
redis> GET mykey
"Hello"

3. set:设置key

  • 格式set key value [ex seconds | px milliseconds | exat timestamp | pxat milliseconds-timestamp | keepttl] [nx|xx] [get]
  • 注意点
    • 如果key已经保存了一个值,那么这个操作会覆盖原来的值,并且忽略原始类型
    • 当set命令执行成功后,之前设置的过期时间都将失效
    • 由于SET命令加上选项已经可以完全取代SETNX, SETEX, PSETEX的功能,所以在将来的版本中,redis可能会不推荐使用并且最终抛弃这几个命令
  • 可选项
    • ex seconds:设置key的过期时间,单位为秒
    • px milliseconds:设置key的过期时间,单位为毫秒
    • nx:只有key不存在的时候才会设置key的值
    • xx:只有key存在的时候才会设置key的值
  • 返回值:如果SET命令正常执行那么会返回OK,否则如果加了NX 或者 XX选项,但是没有设置条件。那么会返回nil
3.1 应用:使用set来实现锁

命令 SET resource-name anystring NX EX max-lock-time 是一种用 Redis 来实现锁机制的简单方法。如果上述命令返回OK,那么客户端就可以获得锁(如果上述命令返回Nil,那么客户端可以在一段时间之后重新尝试),并且可以通过DEL命令来释放锁。客户端加锁之后,如果没有主动释放,会在过期时间之后自动释放。可以通过如下优化使得上面的锁系统变得更加稳定:

  • 不要设置固定的字符串,而是设置为随机的大字符串,可以称为token
  • 通过脚步删除指定锁的key,而不是DEL命令

上述优化方法会避免下述场景:a客户端获得的锁(键key)已经由于过期时间到了被redis服务器删除,但是这个时候a客户端还去执行DEL命令。而b客户端已经在a设置的过期时间之后重新获取了这个同样key的锁,那么a执行DEL就会释放了b客户端加好的锁

4. getset:获取key的旧值并设置key的新值

  • 格式getset key value
  • 返回值
    • 返回之前的旧值,如果之前Key不存在将返回nil
    • 如果key存在但是对应的value不是字符串,就返回错误
  • 复杂度:O(1)
redis> INCR mycounter
(integer) 1
redis> GETSET mycounter "0"
"1"
redis> GET mycounter
"0"
4.1 应用:

GETSET可以和INCR一起使用实现支持重置的计数功能。举个例子:每当有事件发生的时候,一段程序都会调用INCR给key mycounter加1,但是有时我们需要获取计数器的值,并且自动将其重置为0。这可以通过GETSET mycounter “0”来实现:

INCR mycounter
GETSET mycounter "0"
GET mycounter

5. setex:设置给定过期时间的key

  • 格式setex key seconds value
  • 这个命令等效于执行下面的命令:
SET mykey value
EXPIRE mykey seconds
  • 返回值:无
redis> SETEX mykey 10 "Hello"
OK
redis> TTL mykey
(integer) 10
redis> GET mykey
"Hello"

6. setnx:如果key不存在则set,如果存在则不执行

  • 格式setnx key value
  • SETNX是”SET if Not eXists”的简写
  • 返回值
    • 1 如果key被设置了
    • 0 如果key没有被设置
  • 复杂度:O(1)
redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"
6.1 应用:使用SETNX加锁

请注意

  1. 不鼓励以下模式来实现the Redlock algorithm ,该算法实现起来有一些复杂,但是提供了更好的保证并且具有容错性。
  2. 无论如何,即使假设一个单例的加锁原语,但是从 2.6.12 开始,可以创建一个更加简单的加锁原语,相当于使用SET命令来获取锁,并且用一个简单的 Lua 脚本来释放锁。该模式被记录在SET命令的页面中。

也就是说,SETNX能够被使用并且以前也在被使用去作为一个加锁原语。例如,获取键为foo的锁,客户端可以尝试一下操作:

SETNX lock.foo <current Unix time + lock timeout + 1>

如果客户端获得锁,SETNX返回1,那么将lock.foo键的Unix时间设置为以后的某一时间。客户端随后会使用DEL lock.foo去释放该锁。
如果SETNX返回0,那么该键已经被其他的客户端锁定。如果这是一个非阻塞的锁,才能立刻返回给调用者,或者尝试重新获取该锁,直到成功或者过期超时。

处理死锁

以上加锁算法存在一个问题:如果客户端出现故障,崩溃或者其他情况无法释放该锁会发生什么情况?这是能够检测到这种情况,因为该锁包含一个Unix时间戳,如果这样一个时间戳等于当前的Unix时间,该锁将不再有效。

当以下这种情况发生时,我们不能调用DEL来删除该锁,并且尝试执行一个SETNX,因为这里存在一个竞态条件,当多个客户端察觉到一个过期的锁并且都尝试去释放它。

  • C1 和 C2 读lock.foo检查时间戳,因为他们执行完SETNX后都被返回了0,因为锁仍然被 C3 所持有,并且 C3 已经崩溃。
  • C1 发送DEL lock.foo
  • C1 发送SETNX lock.foo命令并且成功返回
  • C2 发送DEL lock.foo
  • C2 发送SETNX lock.foo命令并且成功返回
  • 错误:由于竞态条件导致 C1 和 C2 都获取到了锁

幸运的是,可以使用以下的算法来避免这种情况,请看 C4 客户端所使用的好的算法:

  • C4 发送SETNX lock.foo为了获得该锁
  • 已经崩溃的客户端 C3 仍然持有该锁,所以Redis将会返回0给 C4
  • C4 发送GET lock.foo检查该锁是否已经过期。如果没有过期,C4 客户端将会睡眠一会,并且从一开始进行重试操作
  • 另一种情况,如果因为 lock.foo键的Unix时间小于当前的Unix时间而导致该锁已经过期,C4 会尝试执行以下的操作:
GETSET lock.foo <current Unix timestamp + lock timeout + 1>
  • 由于GETSET 的语意,C4会检查已经过期的旧值是否仍然存储在lock.foo中。如果是的话,C4 会获得锁
  • 如果另一个客户端,假如为 C5 ,比 C4 更快的通过GETSET操作获取到锁,那么 C4 执行GETSET操作会被返回一个不过期的时间戳。C4 将会从第一个步骤重新开始。请注意:即使 C4 在将来几秒设置该键,这也不是问题。

为了使这种加锁算法更加的健壮,持有锁的客户端应该总是要检查是否超时,保证使用DEL释放锁之前不会过期,因为客户端故障的情况可能是复杂的,不止是崩溃,还会阻塞一段时间,阻止一些操作的执行,并且在阻塞恢复后尝试执行DEL(此时,该LOCK已经被其他客户端所持有)

7. incr:对存储的指定key的值执行原子的加1操作

  • 格式incr key
  • 如果指定的key不存在,那么在执行incr操作之前,会先将它的值设定为0
  • 如果指定的key中存储的值不是字符串类型(fix:)或者存储的字符串类型不能表示为一个整数,那么执行这个命令时服务器会返回一个错误(eq:(error) ERR value is not an integer or out of range)。这个操作仅限于64位的有符号整型数据
  • 注意:由于redis并没有一个明确的类型来表示整型数据,所以这个操作是一个字符串操作。执行这个操作的时候,key对应存储字符串被解析为10进制的64位有符号整型数据。事实上,Redis 内部采用整数形式(Integer representation)来存储对应的整数值,所以对该类字符串值实际上是用整数保存,也就不存在存储整数的字符串表示(String representation)所带来的额外消耗
  • 返回值:执行递增操作后key对应的值
  • 复杂度:O(1)
redis> SET mykey "10"
OK
redis> INCR mykey
(integer) 11
redis> GET mykey
"11"
redis> 
7.1 应用1:计数器

Redis的原子递增操作最常用的使用场景是计数器。

使用思路是:每次有相关操作的时候,就向Redis服务器发送一个incr命令。

例如这样一个场景:我们有一个web应用,我们想记录每个用户每天访问这个网站的次数。

web应用只需要通过拼接用户id和代表当前时间的字符串作为key,每次用户访问这个页面的时候对这个key执行一下incr命令。

这个场景可以有很多种扩展方法:

  • 通过结合使用INCR和EXPIRE命令,可以实现一个只记录用户在指定间隔时间内的访问次数的计数器
  • 客户端可以通过GETSET命令获取当前计数器的值并且重置为0
  • 通过类似于DECR或者INCRBY等原子递增/递减的命令,可以根据用户的操作来增加或者减少某些值 比如在线游戏,需要对用户的游戏分数进行实时控制,分数可能增加也可能减少。
7.2 应用2:限速器1

限速器是一种可以限制某些操作执行速率的特殊场景。

传统的例子就是限制某个公共api的请求数目。

假设我们要解决如下问题:限制某个api每秒每个ip的请求次数不超过10次。

我们可以通过incr命令来实现两种方法解决这个问题。

7.3 应用3: 限速器2

更加简单和直接的实现如下:

FUNCTION LIMIT_API_CALL(ip)
ts = CURRENT_UNIX_TIME()
keyname = ip+":"+ts
current = GET(keyname)
IF current != NULL AND current > 10 THEN
    ERROR "too many requests per second"
ELSE
    MULTI
        INCR(keyname,1)
        EXPIRE(keyname,10)
    EXEC
    PERFORM_API_CALL()
END

这种方法的基本点是每个ip每秒生成一个可以记录请求数的计数器。

但是这些计数器每次递增的时候都设置了10秒的过期时间,这样在进入下一秒之后,redis会自动删除前一秒的计数器。

注意上面伪代码中我们用到了MULTI和EXEC命令,将递增操作和设置过期时间的操作放在了一个事务中, 从而保证了两个操作的原子性。

7.4 应用4: 限速器3

另外一个实现是对每个ip只用一个单独的计数器(不是每秒生成一个),但是需要注意避免竟态条件。 我们会对多种不同的变量进行测试。

FUNCTION 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才可以使用)

local current
current = redis.call("incr",KEYS[1])
if tonumber(current) == 1 then
    redis.call("expire",KEYS[1],1)
end

还可以通过使用redis的list来解决上述问题避免进入竞态条件。

实现代码更加复杂并且利用了一些redis的新的feature,可以记录当前请求的客户端ip地址。这个有没有好处 取决于应用程序本身。

FUNCTION LIMIT_API_CALL(ip)
current = LLEN(ip)
IF current > 10 THEN
    ERROR "too many requests per second"
ELSE
    IF EXISTS(ip) == FALSE
        MULTI
            RPUSH(ip,ip)
            EXPIRE(ip,1)
        EXEC
    ELSE
        RPUSHX(ip,ip)
    END
    PERFORM_API_CALL()
END

RPUSHX命令会往list中插入一个元素,如果key存在的话

上述实现也可能会出现竞态,比如我们在执行EXISTS指令之后返回了false,但是另外一个客户端创建了这个key。

后果就是我们会少记录一个请求。但是这种情况很少出现,所以我们的请求限速器还是能够运行良好的

8. decr:对key对应的数字做减1操作

  • 格式decr key
  • 如果key不存在,那么在操作之前,这个key对应的值会被置为0
  • 如果key有一个错误类型的value或者是一个不能表示成数字的字符串,就返回错误。这个操作最大支持在64位有符号的整型数字
  • 返回值:减小之后的value
  • 复杂度:O(1)
redis> SET mykey "10"
OK
redis> DECR mykey
(integer) 9
redis> SET mykey "234293482390480948029348230948"
OK
redis> DECR mykey
ERR value is not an integer or out of range
redis> 

9. incrby:将key对应的数字加指定值

  • 格式incrby key increment
  • 如果key不存在,操作之前,key就会被置为0
  • 如果key的value类型错误或者是个不能表示成数字的字符串,就返回错误。这个操作最多支持64位有符号的正型数字
  • 返回值:操作后的数值
  • 复杂度:O(1)
redis> SET mykey "10"
OK
redis> INCRBY mykey 5
(integer) 15

10. decrby:将key对应的数字减指定值

  • 格式decrby key increment
  • 如果key不存在,操作之前,key就会被置为0。
  • 如果key的value类型错误或者是个不能表示成数字的字符串,就返回错误。这个操作最多支持64位有符号的正型数字
  • 返回值:操作后的数值
  • 复杂度:O(1)
redis> SET mykey "10"
OK
redis> DECRBY mykey 5
(integer) 5

11. incrbyfloat:通过指定浮点数key来增长浮点数(存放于string中)的值

  • 格式incrbyfloat key increment
  • 如果key不存在,操作之前,key就会被置为0。
  • 下面任一情况都会返回错误
    • key 包含非法值(不是一个string)
    • 当前的key或者相加后的值不能解析为一个双精度的浮点值.(超出精度范围了)
  • 如果操作命令成功, 相加后的值将替换原值存储在对应的键值上, 并以string的类型返回. string中已存的值或者相加参数可以任意选用指数符号,但相加计算的结果会以科学计数法的格式存储. 无论各计算的内部精度如何, 输出精度都固定为小数点后17位
  • 返回值:操作后的数值
  • 复杂度:O(1)
redis> SET mykey 10.50
OK
redis> INCRBYFLOAT mykey 0.1
"10.6"
redis> SET mykey 5.0e3
OK
redis> INCRBYFLOAT mykey 2.0e2
"5200"

12. append:追加指定字符串

  • 格式append key value
  • 如果 key 已经存在,并且值为字符串,那么这个命令会把 value 追加到原来值(value)的结尾。 如果 key 不存在,那么它将首先创建一个空字符串的key,再执行追加操作,这种情况 APPEND 将类似于 SET 操作
  • 返回值:返回append后字符串值(value)的长度
  • 复杂度:O(1)。均摊时间复杂度是O(1), 因为redis用的动态字符串的库在每次分配空间的时候会增加一倍的可用空闲空间,所以在添加的value较小而且已经存在的 value是任意大小的情况下,均摊时间复杂度是O(1)
redis> EXISTS mykey
(integer) 0
redis> APPEND mykey "Hello"
(integer) 5
redis> APPEND mykey " World"
(integer) 11
redis> GET mykey
"Hello World"
12.1 模式:节拍序列(Time series)

APPEND 命令可以用来连接一系列固定长度的样例,与使用列表相比这样更加紧凑. 通常会用来记录节拍序列. 每收到一个新的节拍样例就可以这样记录:

APPEND timeseries "fixed-size sample"

在节拍序列里, 可以很容易地访问序列中的每个元素:

  • STRLEN 可以用来计算样例个数.
  • GETRANGE 允许随机访问序列中的各个元素. 如果序列中有明确的节拍信息, 在Redis 2.6中就可以使用GETRANGE配合Lua脚本来实现一个二分查找算法.
  • SETRANGE 可以用来覆写已有的节拍序列.

该模式的局限在于只能做追加操作. Redis目前缺少剪裁字符串的命令, 所以无法方便地把序列剪裁成指定的尺寸. 但是, 节拍序列在空间占用上效率极好.

小贴士: 在键值中组合Unix时间戳, 可以在构建一系列相关键值时缩短键值长度,更优雅地分配Redis实例.

使用定长字符串进行温度采样的例子(在实际使用时,采用二进制格式会更好).

redis> APPEND ts "0043"
(integer) 4
redis> APPEND ts "0035"
(integer) 8
redis> GETRANGE ts 0 3
"0043"
redis> GETRANGE ts 4 7
"0035"
redis>

13. getrange:返回key对应字符串指定起止位置字符串

  • 格式getrange key start end
  • 警告:这个命令是被改成GETRANGE的,在小于2.0的Redis版本中叫SUBSTR
  • 可以用负的位移来表示从string尾部开始数的下标。所以-1就是最后一个字符,-2就是倒数第二个,以此类推。
  • 这个函数处理超出范围的请求时,都把结果限制在string内
  • 复杂度:O(N) N是字符串长度,复杂度由最终返回长度决定,但由于通过一个字符串创建子字符串是很容易的,它可以被认为是O(1)
redis> SET mykey "This is a string"
OK
redis> GETRANGE mykey 0 3
"This"
redis> GETRANGE mykey -3 -1
"ing"
redis> GETRANGE mykey 0 -1
"This is a string"
redis> GETRANGE mykey 10 100
"string"

14. setrange:覆盖指定位置开始的字符串

  • 格式setrange key offset value
  • 如果offset比当前key对应string还要长,那这个string后面就补0以达到offset。不存在的keys被认为是空字符串,所以这个命令可以确保key有一个足够大的字符串,能在offset处设置value。
  • 注意:offset最大可以是229-1(536870911),因为redis字符串限制在512M大小。如果你需要超过这个大小,你可以用多个keys
  • 警告:当set最后一个字节并且key还没有一个字符串value或者其value是个比较小的字符串时,Redis需要立即分配所有内存,这有可能会导致服务阻塞一会。在一台2010MacBook Pro上,set536870911字节(分配512MB)需要~300ms,set134217728字节(分配128MB)需要~80ms,set33554432比特位(分配32MB)需要~30ms,set8388608比特(分配8MB)需要8ms。注意,一旦第一次内存分配完,后面对同一个key调用SETRANGE就不会预先得到内存分配
  • 返回值:该命令修改后的字符串长度
  • 返回值:O(1)
redis> SET key1 "Hello World"
OK
redis> SETRANGE key1 6 "Redis"
(integer) 11
redis> GET key1
"Hello Redis"
redis> SETRANGE key2 6 "Redis"
(integer) 11
redis> GET key2
"\x00\x00\x00\x00\x00\x00Redis"

15. strlen:返回key的值的长度

  • 格式strlen key
  • 返回值:key对应的字符串value的长度,或者0(key不存在)
  • 复杂度:O(1)
redis> SET mykey "Hello world"
OK
redis> STRLEN mykey
(integer) 11
redis> STRLEN nonexisting
(integer) 0

16. mget:返回所有给定key的value

  • 格式mget key1 [key2 key3....]
  • 对于每个不对应string或者不存在的key,都返回特殊值nil。正因为此,这个操作从来不会失败
  • 返回值:指定的key对应的values的list
  • 复杂度:O(N) where N is the number of keys to retrieve
redis> SET key1 "Hello"
OK
redis> SET key2 "World"
OK
redis> MGET key1 key2 nonexisting
1) "Hello"
2) "World"
3) (nil)

17. mset:批量设置key value

  • 格式mset k1 v1 [k2 v2 k3 v3 ...]
  • MSET会用新的value替换已经存在的value,就像普通的SET命令一样
  • 原子性:MSET是原子的,所以所有给定的keys是一次性set的。客户端不可能看到这种一部分keys被更新而另外的没有改变的情况
  • 返回值:总是OK,因为MSET不会失败
redis> MSET key1 "Hello" key2 "World"
OK
redis> GET key1
"Hello"
redis> GET key2
"World"

18. msetnx:批量设置key value,只要有一个key存在,全部不执行

  • 格式msetnxkey value [key value ...]
  • 只要有一个key已经存在,MSETNX一个操作都不会执行。 由于这种特性,MSETNX可以实现要么所有的操作都成功,要么一个都不执行,这样可以用来设置不同的key,来表示一个唯一的对象的不同字段。
  • MSETNX是原子的,所以所有给定的keys是一次性set的。客户端不可能看到这种一部分keys被更新而另外的没有改变的情况
  • 返回值
    • 1 如果所有的key被set
    • 0 如果没有key被set(至少其中有一个key是存在的)
  • 时间复杂度:O(N) where N is the number of keys to set
redis> MSETNX key1 "Hello" key2 "there"
(integer) 1
redis> MSETNX key2 "there" key3 "world"
(integer) 0
redis> MGET key1 key2 key3
1) "Hello"
2) "there"
3) (nil)

19. psetnx:设置给定过期时间(毫秒)的key

  • 格式psetex key milliseconds value
  • PSETEX和SETEX一样,唯一的区别是到期时间以毫秒为单位,而不是秒
  • 复杂度:O(1)
redis> PSETEX mykey 1000 "Hello"
OK
redis> PTTL mykey
(integer) 999
redis> GET mykey
"Hello"

20. bitcount:统计value的二进制中为1的bit数

  • 格式bitcount key [start end]
  • 一般情况下,给定的整个字符串都会被进行计数,通过指定额外的 start 或 end 参数,可以让计数只在特定的位上进行
  • start 和 end 参数的设置和 GETRANGE 命令类似,都可以使用负数值:比如 -1 表示最后一个位,而 -2 表示倒数第二个位,以此类推。
  • 不存在的 key 被当成是空字符串来处理,因此对一个不存在的 key 进行 BITCOUNT 操作,结果为 0
  • 返回值:被设置为 1 的位的数量
  • 复杂度:O(n)
redis> SET mykey "foobar"
OK
redis> BITCOUNT mykey
(integer) 26
redis> BITCOUNT mykey 0 0
(integer) 4
redis> BITCOUNT mykey 1 1
(integer) 6
20.1 应用:使用 bitmap 实现用户上线次数统计

假设现在我们希望记录自己网站上的用户的上线频率,比如说,计算用户 A 上线了多少天,用户 B 上线了多少天,诸如此类,以此作为数据,从而决定让哪些用户参加 beta 测试等活动 —— 这个模式可以使用 SETBIT 和 BITCOUNT 来实现。

比如说,每当用户在某一天上线的时候,我们就使用 SETBIT ,以用户名作为 key ,将那天所代表的网站的上线日作为 offset 参数,并将这个 offset 上的为设置为 1 。

举个例子,如果今天是网站上线的第 100 天,而用户 peter 在今天阅览过网站,那么执行命令 SETBIT peter 100 1 ;如果明天 peter 也继续阅览网站,那么执行命令 SETBIT peter 101 1 ,以此类推。

当要计算 peter 总共以来的上线次数时,就使用 BITCOUNT 命令:执行 BITCOUNT peter ,得出的结果就是 peter 上线的总天数

20.2 性能:

前面的上线次数统计例子,即使运行 10 年,占用的空间也只是每个用户 10*365 比特位(bit),也即是每个用户 456 字节。对于这种大小的数据来说, BITCOUNT 的处理速度就像 GET 和 INCR 这种 O(1) 复杂度的操作一样快。

  • 如果你的 bitmap 数据非常大,那么可以考虑使用以下两种方法:
    • 将一个大的 bitmap 分散到不同的 key 中,作为小的 bitmap 来处理。使用 Lua 脚本可以很方便地完成这一工作。
    • 使用 BITCOUNT 的 start 和 end 参数,每次只对所需的部分位进行计算,将位的累积工作(accumulating)放到客户端进行,并且对结果进行缓存 (caching)。

21. bitop:对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上

  • 格式bitop and|or|not|xor destkey key [key ...]
  • BITOP AND destkey srckey1 srckey2 srckey3 … srckeyN ,对一个或多个 key 求逻辑并,并将结果保存到 destkey 。
  • BITOP OR destkey srckey1 srckey2 srckey3 … srckeyN,对一个或多个 key 求逻辑或,并将结果保存到 destkey 。
  • BITOP XOR destkey srckey1 srckey2 srckey3 … srckeyN,对一个或多个 key 求逻辑异或,并将结果保存到 destkey 。
  • BITOP NOT destkey srckey,对给定 key 求逻辑非,并将结果保存到 destkey
  • 当 BITOP 处理不同长度的字符串时,较短的那个字符串所缺少的部分会被看作 0
  • 空的 key 也被看作是包含 0 的字符串序列
  • 返回值:保存到 destkey 的字符串的长度,和输入 key 中最长的字符串长度相等
  • 复杂度:O(n)
redis> SET key1 "foobar"
OK
redis> SET key2 "abcdef"
OK
redis> BITOP AND dest key1 key2
(integer) 6
redis> GET dest
"`bc`ab"
21.1 应用:使用 bitop 实现用户上线次数统计

BITOP是对BITCOUNT命令一个很好的补充。

不同的bitmaps进行组合操作可以获得目标bitmap以进行人口统计操作

21.2 性能:

BITOP可能是一个缓慢的命令,它的时间复杂度是O(N)。 在处理长字符串时应注意一下效率问题。

对于实时的指标和统计,涉及大输入一个很好的方法是 使用bit-wise操作以避免阻塞主实例。

22. bitfield:把Redis字符串当作位数组,并能对变长位宽和任意未字节对齐的指定整型位域进行寻址

  • 格式BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]
  • 在实践中,可以使用该命令对一个有符号的5位整型数的1234位设置指定值,也可以对一个31位无符号整型数的4567位进行取值。类似地,在对指定的整数进行自增和自减操作,本命令可以提供有保证的、可配置的上溢和下溢处理操作。
  • BITFIELD命令能操作多字节位域,它会执行一系列操作,并返回一个响应数组,在参数列表中每个响应数组匹配相应的操作。
  • 例如,下面的命令是对一个8位有符号整数偏移100位自增1,并获取4位无符号整数的值:
> BITFIELD mykey INCRBY i5 100 1 GET u4 0
1) (integer) 1
2) (integer) 0
  • 提示:
    • 用GET指令对超出当前字符串长度的位(含key不存在的情况)进行寻址,执行操作的结果会对缺失部分的位(bits)赋值为0。
    • 用SET或INCRBY指令对超出当前字符串长度的位(含key不存在的情况)进行寻址,将会扩展字符串并对扩展部分进行补0,扩展方式包括:按需扩展、按最小长度扩展和按最大寻址能力扩展。
22.1 支持子命令和整型:
  • GET – 返回指定的位域
  • SET – 设置指定位域的值并返回它的原值
  • INCRBY – 自增或自减(如果increment为负数)指定位域的值并返回它的新值

还有一个命令通过设置溢出行为来改变调用INCRBY指令的后序操作:

  • OVERFLOW [WRAP|SAT|FAIL]

当需要一个整型时,有符号整型需在位数前加i,无符号在位数前加u。例如,u8是一个8位的无符号整型,i16是一个16位的有符号整型。

有符号整型最大支持64位,而无符号整型最大支持63位。对无符号整型的限制,是由于当前Redis协议不能在响应消息中返回64位无符号整数。

22.2 位和位偏移

bitfield命令有两种方式来指定位偏移。如果未定带数字的前缀,将会以字符串的第0位作为起始位。

不过,如果偏移量带有#前缀,那么指定的偏移量需要乘以整型宽度,例如:

BITFIELD mystring SET i8 #0 100 i8 #1 200

将会在第1个i8整数的偏移0位和第2个整数的偏移8位进行设值。如果想得到一个给定长度的普通整型数组,则不一定要在客户端进行计算

22.3 溢出控制

使用OVERFLOW命令,用户可以通过指定下列其中一种行为来调整自增或自减操作溢出(或下溢)后的行为:

  • WRAP: 回环算法,适用于有符号和无符号整型两种类型。对于无符号整型,回环计数将对整型最大值进行取模操作(C语言的标准行为)。对于有符号整型,上溢从最负的负数开始取数,下溢则从最大的正数开始取数,例如,如果i8整型的值设为127,自加1后的值变为-128。
  • SAT: 饱和算法,下溢之后设为最小的整型值,上溢之后设为最大的整数值。例如,i8整型的值从120开始加10后,结果是127,继续增加,结果还是保持为127。下溢也是同理,但量结果值将会保持在最负的负数值。
  • FAIL: 失败算法,这种模式下,在检测到上溢或下溢时,不做任何操作。相应的返回值会设为NULL,并返回给调用者。

注意每种溢出(OVERFLOW)控制方法,仅影响紧跟在INCRBY命令后的子命令,直到重新指定溢出(OVERFLOW)控制方法。

如果没有指定溢出控制方法,默认情况下,将使用WRAP算法。

> BITFIELD mykey incrby u2 100 1 OVERFLOW SAT incrby u2 102 1
1) (integer) 1
2) (integer) 1
> BITFIELD mykey incrby u2 100 1 OVERFLOW SAT incrby u2 102 1
1) (integer) 2
2) (integer) 2
> BITFIELD mykey incrby u2 100 1 OVERFLOW SAT incrby u2 102 1
1) (integer) 3
2) (integer) 3
> BITFIELD mykey incrby u2 100 1 OVERFLOW SAT incrby u2 102 1
1) (integer) 0
2) (integer) 3
22.4 返回值

本命令返回一个针对子命令给定位置的处理结果组成的数组。OVERFLOW子命令在响应消息中,不会统计结果的条数。

下面是OVERFLOW FAIL返回NULL的样例:

> BITFIELD mykey OVERFLOW FAIL incrby u2 102 1
1) (nil)
22.5 动机(Motivations)

本命令的动机是为了能够在单个大位图(large bitmap)中高效地存储多个小整数(或对键分成多个key,避免出现超大键),同时开放Redis提供的新使用案例,尤其是在实时分析领域。这种使用案例可以通过指定的溢出控制方法来支持。

22.6 性能考虑(Performance considerations)

通常,BITFIELD是一个非常快的命令,但是注意,对短字符串的远地址(fat bits)寻址,将会比在存在的位执行命令更加耗时。

22.7 字节序(Orders of bits)

BITFIELD命令使用的位图表现形式,可看作是从0位开始的,例如:把一个5位的无符号整数23,对一个所有位事先置0的位图,从第7位开始赋值,其结果如下所示:

+--------+--------+
|00000001|01110000|
+--------+--------+

当偏移量和整型大小是字节边界对齐时,此时与大端模式(big endian)相同,但是,当字节边界未对齐时,那么理解字节序将变得非常重要

23. bitpos:返回字符串里面第一个被设置为1或者0的bit位

  • 格式bitpos key bit [start] [end]
  • 复杂度:O(N)
  • 返回一个位置,把字符串当做一个从左到右的字节数组,第一个符合条件的在位置0,其次在位置8,等等。
  • GETBIT 和 SETBIT 相似的也是操作字节位的命令。
  • 默认情况下整个字符串都会被检索一次,只有在指定start和end参数(指定start和end位是可行的),该范围被解释为一个字节的范围,而不是一系列的位。所以start=0 并且 end=2是指前三个字节范围内查找。
  • 注意,返回的位的位置始终是从0开始的,即使使用了start来指定了一个开始字节也是这样。
  • 和GETRANGE命令一样,start和end也可以包含负值,负值将从字符串的末尾开始计算,-1是字符串的最后一个字节,-2是倒数第二个,等等。
  • 不存在的key将会被当做空字符串来处理
  • 返回值
    • 命令返回字符串里面第一个被设置为1或者0的bit位。
    • 如果我们在空字符串或者0字节的字符串里面查找bit为1的内容,那么结果将返回-1。
    • 如果我们在字符串里面查找bit为0而且字符串只包含1的值时,将返回字符串最右边的第一个空位。如果有一个字符串是三个字节的值为0xff的字符串,那么命令BITPOS key 0将会返回24,因为0-23位都是1。
    • 基本上,我们可以把字符串看成右边有无数个0。
    • 然而,如果你用指定start和end范围进行查找指定值时,如果该范围内没有对应值,结果将返回-1
redis> SET mykey "\xff\xf0\x00"
OK
redis> BITPOS mykey 0 # 查找字符串里面bit值为0的位置
(integer) 12
redis> SET mykey "\x00\xff\xf0"
OK
redis> BITPOS mykey 1 0 # 查找字符串里面bit值为1从第0个字节开始的位置
(integer) 8
redis> BITPOS mykey 1 2 # 查找字符串里面bit值为1从第2个字节(12)开始的位置
(integer) 16
redis> set mykey "\x00\x00\x00"
OK
redis> BITPOS mykey 1 # 查找字符串里面bit值为1的位置
(integer) -1

24. getbit:返回key对应的string在offset处的bit值

  • 格式getbit key offset
  • 当offset超出了字符串长度的时候,这个字符串就被假定为由0比特填充的连续空间
  • 当key不存在的时候,它就认为是一个空字符串,所以offset总是超出范围,然后value也被认为是由0比特填充的连续空间。到内存分配
  • 返回值:在offset处的bit值
  • 复杂度:O(1)
redis> SETBIT mykey 7 1
(integer) 0
redis> GETBIT mykey 0
(integer) 0
redis> GETBIT mykey 7
(integer) 1
redis> GETBIT mykey 100
(integer) 0

25. setbit:设置或者清空key的value(字符串)在offset处的bit值

  • 格式setbit key offset value
  • 那个位置的bit要么被设置,要么被清空,这个由value(只能是0或者1)来决定
  • 当key不存在的时候,就创建一个新的字符串value
  • 要确保这个字符串大到在offset处有bit值。参数offset需要大于等于0,并且小于232(限制bitmap大小为512)。当key对应的字符串增大的时候,新增的部分bit值都是设置为0
  • 警告:当set最后一个bit(offset等于232-1)并且key还没有一个字符串value或者其value是个比较小的字符串时,Redis需要立即分配所有内存,这有可能会导致服务阻塞一会。在一台2010MacBook Pro上,offset为232-1(分配512MB)需要~300ms,offset为230-1(分配128MB)需要~80ms,offset为228-1(分配32MB)需要~30ms,offset为226-1(分配8MB)需要8ms。注意,一旦第一次内存分配完,后面对同一个key调用SETBIT就不会预先得到内存分配
  • 返回值:在offset处原来的bit值
redis> SETBIT mykey 7 1
(integer) 0
redis> SETBIT mykey 7 0
(integer) 1
redis> GET mykey
"\x00"
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐