@@ -209,8 +209,186 @@ BITFIELD 命令支持 SET, GET, INCRBY, OVERFLOW 这 4 个子命令, 接下来
209209 2) (integer) 0
210210 ```
211211
212- 除了上面介绍的基本功能, 还有一下方法可以实现, 这里不再介绍
212+ - 根据索引对区域进行设置
213+ 除了根据偏移量设置位图, 还可以根据给定类型的位长度, 对位图在指定索引上存储的整数进行设置.
214+ ``` Redis
215+ BITFIELD bitmap SET type #index value
216+ ```
217+ 假如现在有一个位图, 其存储着多个 8 位长的无符号整数, 想要将其第 133 个 8 位无符号整数的值设置为 22.
218+ 如果使用偏移量, 就要手动计算 (133 - 1) * 8 的起始偏移量
219+ ``` Redis
220+ BITFIELD bitmap SET u8 1056 22
221+ ```
222+ 这样很不方便, 因此可以使用类型直接设置 (SET 字命令索引从 0 开始)
223+ ``` Redis
224+ BITFIELD bitmap SET u8 #132 22
225+ ```
226+
227+ - 获取区域存储的值
228+ 通过使用 BITFIELD 命令的 GET 子命令, 可以从给定的偏移量或索引取出指定类型整数值
229+ ``` Redis
230+ BITFIELD bitmap GET type offset
231+ BITFIELD bitmap GET type #index
232+ ```
233+
234+ - 执行加法或减法操作
235+ 除了设置于获取整数值, BITFIELD 命令还可以对位图的整数值执行加法或减法操作, 通过 INCRBY 子命令实现
236+ ``` Redis
237+ BITFIELD bitmap INCRBY type offset increment
238+ BITFIELD bitmap INCRBY type #index increment
239+ ```
240+ 如果要实现加法, 仍然使用 INCRBY 命令, 但 increment 使用负数, 从而得到减法的效果
241+ ``` Redis
242+ BITFIELD numbers SET u8 #0 10 -- 将区域的值设置未整数 10
243+ 1) (integer) 0
244+
245+ BITFIELD numbers GET u8 #0
246+ 1) (integer) 10
247+
248+ BITFIELD numbers INCRBY u8 #0 15 -- 将整数的值加上 15
249+ 1) (integer) 25
250+
251+ BITFIELD numbers INCRBY u8 #0 30 -- 将整数值加上 30
252+ 1) (integer) 55
253+
254+ BITFIELD numbers INCRBY u8 #0 -25 -- 将整数值减去 25
255+ 1) (integer) 30
256+ ```
257+
258+ - 处理溢出
259+ BITFIELD 命令还可以使用 OVERFLOW 子命令去控制 INCRBY 子命令在发生计算溢出时的行为
260+ ``` Redis
261+ BITFIELD bitmap [...] OVERFLOW WRAP|SAT|FAIL [...]
262+ ```
263+ OVERFLOW 参数有 WRAP, SAT 和 FAIL
264+ - WRAP 表示回绕 (wrap around) 方式处理溢出, 也就是 C 语言默认的溢出处理方式.
265+ - SAT 表示使用饱合运算 (saturation arithmetic) 方式处理溢出. 向上溢出的整数设置位最大值, 向下溢出的整数值则被设置位最小值
266+ - FAIL 表示让 INCRBY 子命令在检测到计算会引发溢出时, 拒绝执行计算, 并返回空值表示计算失败
267+
268+ 但 OVERFLOW 命令执行后不会产生任何返回, 此外, 如果没有设置溢出处理方式, 默认是 WRAP
269+
270+ 需要注意的是, 由于 OVERFLOW 子命令只会对同一个 BITFIELD 调用中在其后面的 INCRBY 命令产生效果
271+ ``` Redis
272+ BITFIELD bitmap INCRBY ... INCRBY OVERFLOW SAT INCRBY ...
273+ ```
274+ 上面命令中, 前两个就使用 WRAP 方法处理溢出, 第三个使用 SAT 方法处理溢出
213275
214- - 执行加法或减法操作
215- - 处理溢出
216- - 字符串命令对位图进行操作
276+ - 使用位图存储整数的原因
277+ 一般情况下, 用户使用字符串或者散列存储整数, Redis 都会为被存储的整数分配一个 long 类型的值, 并使用对象去包裹这个值, 然后再吧对象关联到数据库中
278+
279+ 而 BITFIELD 命令运行用户自行指定存储整数的类型, 并且不会使用对象去包裹这些整数, 因此当想要存储长度比 long 类型短的整数, 并且希望尽可能减少对象包括带来的内存消耗, 就可以考虑使用 BITFIELD 存储整数
280+
281+ - 复杂度: O(N), N 为用户给定的子命令数量
282+
283+
284+ ### 示例: 紧凑计数器
285+ - 与之间实现的计数器不同, 这个计数器允许用户自行指定计数器值的位长以及类型, 而不是使用 Redis 默认的 long 类型来存储计数器值
286+ - 与字符串或者散列实现的计数器不同, 这个计数器只能使用整数作为索引, 因此只适合存储一些与数字 ID 想关联的计数器数据
287+
288+ ``` Python
289+ from redis import Redis
290+
291+ def get_bitmap_index (index ):
292+ return " #" + str (index)
293+
294+ class CompactCounter :
295+ def __init__ (self , client , key , bit_length , signed ):
296+ """ 初始化紧凑计数器
297+
298+ Args:
299+ - client 指定客户端
300+ - key 参数指定计数器键名
301+ - bit_length 参数指定计数器存储的整数位
302+ - signed 参数用于指定计数器存储的符号
303+ """
304+ self .client = client
305+ self .key = key
306+ if signed:
307+ self .type = " i" + str (bit_length)
308+ else :
309+ self .type = " u" + str (bit_length)
310+
311+ def increase (self , index , n = 1 ):
312+ """ 对索引 index 的计数器执行加法操作, 然后返回计数器的当前值"""
313+ bitmap_index = get_bitmap_index(index)
314+ result = self .client.execute_command(
315+ " BITFIELD" ,
316+ self .key,
317+ " OVERFLOW" ,
318+ " SAT" ,
319+ " INCRBY" ,
320+ self .type,
321+ bitmap_index,
322+ n,
323+ )
324+ return result[0 ]
325+
326+ def decrease (self , index , n = 1 ):
327+ """ 对索引 index 上的计数器执行减法操作, 然后返回计数器的当前值"""
328+ bitmap_index = get_bitmap_index(index)
329+ decrement = - n
330+ result = self .client.execute_command(
331+ " BITFIELD" ,
332+ self .key,
333+ " OVERFLOW" ,
334+ " SAT" ,
335+ " INCRBY" ,
336+ self .type,
337+ bitmap_index,
338+ decrement,
339+ )
340+ return result[0 ]
341+
342+ def get (self , index ):
343+ """ 获取索引 index 上的计数器的当前值"""
344+ bitmap_index = get_bitmap_index(index)
345+ result = self .client.execute_command(
346+ " BITfIELD" ,
347+ self .key,
348+ " GET" ,
349+ self .type,
350+ bitmap_index,
351+ )
352+ ```
353+ 假如要为一家游戏公司创建一个记录每个玩家每月登陆数的计数器, 按照一个月 30 天, 一天登陆 2~ 3 次的频率来计算, 一个普通玩家一个月的登陆次数通常不会超过 100 次. 对于这么小的数值, 使用 long 类型进行存储将浪费大量空间, 因此可以使用紧凑计数器来存储用户登陆次数:
354+ - 每个玩家都又一个整数类型的用户 ID, 可以使用 ID 作为计数器索引
355+ - 对于每个玩家, 使用一个 16 位长的无符号整数来存储其一个月内的登陆次数
356+ - 16 位无符号整数计数器能存储的最大值位 65536, 对于该功能来说, 已经足够大
357+ ``` Python
358+ client = Redis()
359+ counter = CompactCounter(client, " login_count" , 16 , False ) # 建立计数器
360+ print (counter.increase(10086 )) # 记录第 1 次登陆
361+ print (counter.increase(10086 )) # 再记录第 1 次登陆
362+ print (counter.get(10086 )) # 获取登陆次数
363+ ```
364+
365+ - 使用字符串命令对位图进行操作
366+ 由于 Redis 位图在字符串的基础上实现, 所以它会将位图键看作一个字符串键
367+ ``` Redis
368+ SETBIT bitmap 0 1
369+ > (integer) 0
370+
371+ TYPE bitmap
372+ > string
373+ ```
374+ 因此, 还可以使用字符串命令对位图进行操作
375+ ``` Redis
376+ GET 8bit-int
377+ > "\x04"
378+
379+ GET 32bit-ints
380+ > "\x00\x00\x00{\x00\x00\x01\x00\x00\x00\x00 ...}"
381+ ```
382+ 也可以使用 STRLEN 命令获取位图的字节长度
383+ ``` Redis
384+ STRLEN 8bit-int -- 1 字节
385+ > (integer) 1
386+
387+ STRLEN 32bit-ints -- 这个位图 16 字节
388+ > (integerf 16)
389+ ```
390+ 还可以使用 GETRANGE 命令获取位图的其中一部分字节:
391+ ``` Redis
392+ GETRANGE 32bit-ints 0 3
393+ ```
394+ 诸如此类
0 commit comments