更新时间:2025-07-02 GMT+08:00

大Bitmap分页查询

针对大位图(Bitmap)或指定区间[start, end]子位图的获取场景,传统实现方案存在两大瓶颈:

  • 低效数据拼接问题:若客户端通过循环调用GETBIT逐位获取数据并拼接,会产生高频次网络请求与计算开销,存在显著的性能瓶颈。
  • Key访问风险:若直接使用GET获取完整位图,数据库层需传输超大二进制数据块,易引发网络阻塞及服务端延迟波动,影响系统稳定性。

为此,数据库侧提供增强型位图操作命令RANGEBITARRAY,其核心优势为:

  • 分块高效查询:支持通过范围参数直接获取位图指定区间的二进制数据块,客户端可通过多次调用完成大数据集的分段获取与组装。
  • 规避大Key风险:通过按需分块传输机制,避免单次操作触及大Key的性能临界点,有效降低服务端延迟抖动。
  • 端到端性能优化:减少网络往返次数与服务端资源消耗,实现查询吞吐量与响应稳定性的双重提升。

建议在海量位图数据读取场景优先使用此命令。

表1 RANGEBITARRAY

类别

说明

语法

RANGEBITARRAY key start end

时间复杂度

O(C),C表示[start, end]区间长度。

命令描述

获取Bitmap指定区间中所有bit值(0、1)组成的字符串。

选项

  • Key:Key名称(Bitmap数据结构)。
  • start:起始的偏移量(包含该值)。
  • end:结束的偏移量(包含该值)。

返回值

  • 执行成功:返回指定区间中所有bit值(0、1)组成的字符串。
  • 若key不存在:返回空字符串。
  • 其他情况返回相应的异常信息(如“不支持老编码”和“Key必须为Bitmap类型”)。

示例

提前执行如下命令预置数据:

SETBIT foo 2 1
SETBIT foo 3 1
SETBIT foo 5 1
  • 命令示例:
RANGEBITARRAY foo 0 5
  • 返回示例:
"001101"

注意事项

  • 您可以在管理控制台右上角,选择“工单 > 新建工单”联系客服咨询实例版本是否支持该功能。如需使用该功能,您可以参考升级内核小版本升级内核版本。
  • Key必须是Bitmap类型,STRING类型需先用GETBIT命令进行转换。
  • start和end的语义与GETRANGE命令相同。
  • 客户端如果需要多次执行RANGEBITARRAY并进行拼接,可以使用PIPELINE模式加速。
  • 范围查询的粒度可以根据实际情况选择,粒度越细,产生的时延毛刺就越小;粒度越粗,产生的时延毛刺就越大,通常建议32KB的范围查询粒度,再进行拼接。

代码参考

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.util.SafeEncoder;
import redis.clients.jedis.Protocol;
import java.nio.charset.StandardCharsets;

public class BitmapRangePager {

    private final JedisPool jedisPool;
    private static final int PAGE_SIZE_BYTES = 32 * 1024; // 32KB分页(基于字节单位)

    public BitmapRangePager(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }

    public String getFullBitmap(String key) {
        try (Jedis jedis = jedisPool.getResource()) {
            // 获取Bitmap总字节长度
            long totalBytes = jedis.strlen(key);
            if (totalBytes == 0) return "";

            StringBuilder result = new StringBuilder();

            // 基于字节的分页循环(更直观)
            for (long byteOffset = 0; byteOffset < totalBytes; byteOffset += PAGE_SIZE_BYTES) {
                // 计算当前页的字节数(最后一页可能不足32KB)
                int currentPageBytes = (int) Math.min(PAGE_SIZE_BYTES, totalBytes - byteOffset);
                
                // 转换为bit区间(闭区间)
                long bitStart = byteOffset * 8;
                long bitEnd = (byteOffset + currentPageBytes) * 8 - 1; // 包含end位
                
                // 执行命令并拼接结果
                String pageBits = executeRangeBitArray(jedis, key, bitStart, bitEnd);
                result.append(pageBits);
            }
            return result.toString();
        }
    }

    private String executeRangeBitArray(Jedis jedis, String key, long start, long end) {
        // 发送自定义命令RANGEBITARRAY key start end
        Object response = jedis.sendCommand(
                new Protocol.Command("RANGEBITARRAY"),
                SafeEncoder.encode(key),
                Protocol.toByteArray(start),
                Protocol.toByteArray(end)
        );

        // 处理响应(确保二进制转字符串)
        if (response instanceof byte[]) {
            return new String((byte[]) response, StandardCharsets.US_ASCII);
        } else {
            return response.toString();
        }
    }
}
import redis
import math

class BitmapRangePager:
    def __init__(self, redis_client):
        self.redis = redis_client
        self.PAGE_SIZE_BYTES = 32 * 1024  # 32KB 分页(基于字节单位)

    def get_full_bitmap(self, key):
        """
        获取完整的 Bitmap 字符串
        :param key: Redis 键名
        :return: 由 '0'/'1' 组成的完整字符串
        """
        # 获取 Bitmap 总字节数
        total_bytes = self.redis.strlen(key)
        if total_bytes == 0:
            return ""

        result = []
        # 基于字节的分页循环
        for byte_offset in range(0, total_bytes, self.PAGE_SIZE_BYTES):
            # 计算当前页的字节数(最后一页可能不足 32KB)
            current_page_bytes = min(self.PAGE_SIZE_BYTES, total_bytes - byte_offset)
            
            # 转换为 bit 区间闭区间 [start, end]
            bit_start = byte_offset * 8
            bit_end = (byte_offset + current_page_bytes) * 8 - 1
            
            # 执行命令并收集结果
            page_bits = self.execute_range_bitarray(key, bit_start, bit_end)
            result.append(page_bits)

        return ''.join(result)

    def execute_range_bitarray(self, key, start, end):
        """
        执行自定义命令 RANGEBITARRAY key start end
        :return: 由 '0'/'1' 组成的字符串
        """
        # 发送自定义命令(需服务端支持)
        response = self.redis.execute_command(
            "RANGEBITARRAY",  # 注意实际命令名可能需要调整
            key,
            start,
            end
        )
        # 处理响应(redis-py 返回 bytes 类型)
        return response.decode('ascii')  # 假设响应是 ASCII 字符串

# 使用示例
if __name__ == "__main__":
    # 创建 Redis 客户端
    r = redis.Redis(host='localhost', port=6379, decode_responses=False)  # 保持原始 bytes 响应
    
    pager = BitmapRangePager(r)
    full_bitmap = pager.get_full_bitmap("my_large_bitmap")
    
    # 输出示例(仅打印前100字符)
    print(f"Bitmap (first 100 chars): {full_bitmap[:100]}")
    print(f"Total length: {len(full_bitmap)} bits")