Redis bit 的反直觉特性
Redis 里 String
的数据结构都不陌生了, 这里介绍一点使用 String
的 BitMap
特性时, 遇到的反直觉的特性.
前情回顾
学C语言讲ASCII编码的时候, 我们就知道了字符的编码:
'h'.ord
# 104
'h'.ord.to_s(2)
# "1101000"
'i'.ord
# 105
'i'.ord.to_s(2)
# "1101001"
# (<=Byte High) 0110 1000 0110 1001 (Byte Low=>)
# => Memory growth direction =>
关于大端模式和小端模式.
上面例子, h
对应的二进制的存储为 0110 1000
, 正想我们看到的这样, 右边为字节的低位, 左边为高位, index 从左向右数 依次增大.
也是为了顾及人类的可读性, 网络传输中使用了 大端模式
.
但是如此一来, 就不方便机器处理了, 因为 大端模式
跟 内存的增长顺序相反.
小端模式
就是字节的高位对应内存地址的高位, 机器处理的时候就省去了翻转的操作, 提升效率.
Redis 的 String 使用小端模式
UTF8下, 每个英文字符占一个byte, 一个byte八个bit. unshift
方法把每个取出的值从左边插入, 类似 lpush
.
require 'redis'
conn = Redis.new
conn.set('key', 'hi')
p value = conn.get('key')
# "hi"
bits = []
(value.bytesize * 8).times do |index|
bits.unshift conn.getbit('key', index)
end
p bits.join
# "1001011000010110"
Redis中, 以 BitMap
存贮的数据跟普通的字符串并没有什么不同, 只是我们赋予了这些二进制不同的含义.
我们可以按照 01101000 01101001
的顺序依次设置bit位, 然后用字符串的形式来读取 (只是试验, 没人会这样干):
conn.del('key')
bits = '0110100001101001'.split('').map(&:to_i)
p bits # [0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1]
bits.each_with_index do |value, index|
conn.setbit('key', index, value)
end
p conn.get('key')
# "hi"
Redis 的 bitcount 按字节计算
start
和 end
是闭区间, 单位是 byte .
BITCOUNT key [start end]
官方文档虽然有写是根据bytes, 但是直觉反应是按照bit计算啊, 如果没注意就很惨 ~
conn.del('key')
conn.setbit('key', 6, 1)
conn.setbit('key', 7, 1)
conn.setbit('key', 8, 1)
conn.setbit('key', 16, 1)
p conn.bitcount('key')
# 4
p conn.bitcount('key', 0, 1)
# 3
第一个byte是 0~7
,
第一个byte是 8~15
,
第一个byte是 16~23
.
假设 BitMap
中保存的是用户按天打卡的数据, 如果想要某个月份的打卡总数, 只能自己算了.
计算某月的打卡总数
Redis 在 3.2 之后提供了 bitfield
命令, 用来操作连续的bit位.
该命令限制每次最多操作64个位 (有符号数模式64个, 无符号模式63个).
估计这是一个很小众而且参数复杂的命令, Ruby 的 Redis Client 都没有支持它.
首先, 我们利用 Ruby 的打开类特性, 把自定义的 bitfield
的 get 方法塞入 Redis 类.
(bitfield
还有很多其他用法, 这个方法都舍去没用)
class Redis
def bitfield_get(key, offset = 0, len = 1, signed: false)
type = "#{(signed ? 'i' : 'u')}#{len}"
synchronize do |client|
client.call([:bitfield, key, 'get', type, offset]).first
end
end
end
我们在 key 为 record
的 String 上存储用户的打卡记录, 假设index从1开始, 跳过0.
conn.del 'record'
# 假设 record index 从 1 开始
conn.setbit 'record', 1, 1 # 1月1号
conn.setbit 'record', 32, 1 # 2月1号
conn.setbit 'record', 33, 1 # 2月2号
conn.setbit 'record', 40, 1 # 2月9号
conn.setbit 'record', 59, 1 # 2月28号
conn.setbit 'record', 60, 1 # 3月1号
读取二月份打卡天数:
result = conn.bitfield_get('record', 32, 28)
# 201850881
result.to_s(2)
# "1100000010000000000000000001"
# 由于Redis是小端模式, 也就是index从左边往右数, 跟内存增长方向一致
result.to_s(2).count('1')
# 4
Ruby Redis Client 中包含了一个 method_missing:
def method_missing(command, *args)
synchronize do |client|
client.call([command] + args)
end
end
所以我们也可以不使用自定义的 bitfield_get
方法, 直接利用 method_missing 传参:
conn.bitfield(record_key, 'get', "u#{len}", offset)[0]
这样的缺点也很明显, bitfield
本身的参数已经很复杂了, 如此一来参数的含义就更难以理解.
小结
require 'redis'
require 'date'
conn = Redis.new
conn.del 'record'
# 假设 record index 从 1 开始
conn.setbit 'record', 1, 1 # 1月1号
conn.setbit 'record', 32, 1 # 2月1号
conn.setbit 'record', 33, 1 # 2月2号
conn.setbit 'record', 40, 1 # 2月9号
conn.setbit 'record', 59, 1 # 2月28号
conn.setbit 'record', 60, 1 # 3月1号
class Redis
# https://redis.io/commands/bitfield
def bitfield_get(key, offset = 0, len = 1, signed: false)
type = "#{(signed ? 'i' : 'u')}#{len}"
synchronize do |client|
client.call([:bitfield, key, 'get', type, offset]).first
end
end
end
def record_count(conn, record_key, year_month)
# 当月的一号
target = Date.strptime(year_month, '%Y-%m')
# 从元旦当月一号的偏移量
start_of_year = Date.new(target.year)
offset = (target - start_of_year).to_i + 1
# 当月有多少天
end_of_month = Date.new(target.year, target.month, -1)
len = (end_of_month - target).to_i + 1
p "offset:#{offset}, len:#{len}"
result = conn.bitfield_get(record_key, offset, len)
# result = conn.bitfield(record_key, 'get', "u#{len}", offset)[0]
result.to_s(2).count('1')
end
p record_count(conn, 'record', '2019-02')
# "offset:32, len:28"
# 4
Reference
https://redis.io/commands/bitcount