资讯动态

Redis Bitmap

发布时间:2026/10/9 2:52:52 来源:尧图企业网站定制
Redis Bitmap目录• Bitmap是什么• Redis命令• 应用• 去重• 签到统计• 模拟布隆过滤器实现并模拟误判Bitmap是什么先来看个场景假设现在有15个用户userId 0, 16, 11, 14。他们对网站进行了访问那么当前的Bitmap初始化结果为下面这个图可以把Bitmap想象成一个以位为基本单位的数组数组的每个单元只能放0或1Bitmap本身不是Redis的一种独立数据类型它的底层是字符串但是它可以对字符串的位进行操作。一个字符串底层是字节数组每个字节8bit一个String可以当成一个bit数组来用比如设置key它的值是“abc”底层就是3个字节24bit每个bit可以单独设置0或1Redis命令SETBIT设置指定offset的bit值0或1SETBIT key offset value192.168.79.129:1setbit redisBitmap 0 1 0 192.168.79.129:1setbit redisBitmap 1 1 0 192.168.79.129:1setbit redisBitmap 0 0 1返回值是offset位置原来的bit值GETBIT获取指定offset的bit值GETBIT key offset返回值是offset位置的bit值192.168.79.129:1getbit redisBitmap 0 0 192.168.79.129:1getbit redisBitmap 1 1BITFIELD一次性设置/获取多个bit位设置offset为0,1,3,4的bit位为1192.168.79.129:1bitfield redisBitmap set u1 0 1 set u1 1 1 set u1 3 1 set u1 4 1 1) 0 2) 0 3) 0 4) 0 192.168.79.129:1bitfield redisBitmap set u1 0 1 set u1 1 1 set u1 3 1 set u1 4 1 1) 1 2) 1 3) 1 4) 1u11个连续bit作为一个无符号整数u22个连续bit作为一个无符号整数获取offset为0,1,2,3的bit位值 192.168.79.129:1bitfield redisBitmap get u1 0 get u1 1 get u1 2 get u1 3 1) 1 2) 1 3) 1 4) 1 用u2,1的二进制表示为01u2,2位设置offset从0开始用u2设置个1进去 192.168.79.129:1bitfield redisBitmap set u2 0 1 1) 3 原来offset 0 1位置bit为1 1u2,11二进制转十进制3 192.168.79.129:1bitfield redisBitmap get u1 0 get u1 1 get u1 2 get u1 3 1) 0 2) 1 3) 1 4) 1 最前面那俩bit位为0 1正好是十进制的1也就是set u2 0 1中的1BITCOUNT统计值为1的bit个数可以指定start和end单位是字节BITCOUNT key start end192.168.79.129:1bitfield redisBitmap get u1 0 get u1 1 get u1 2 get u1 3 get u1 4 get u1 5 get u1 6 get u1 7 1) 0 2) 1 3) 1 4) 1 5) 1 6) 0 7) 0 8) 0 192.168.79.129:1bitcount redisBitmap 0 0 单位是字节统计第0个字节bit0到bit7的bit位的1的个数 4 192.168.79.129:1bitcount redisBitmap 0 1 单位是字节统计第0到1个字节bit0到bit15的bit位的1的个数 4 192.168.79.129:1bitcount redisBitmap 1 1 单位是字节统计第1个字节bit8到bit15的bit位的1的个数 0 192.168.79.129:1BITOP对多个Bitmap进行位操作BITOP operator destkey key1 key2 ...opAND、OR、NOT、XORdestkey操作后的结果放到它里面key1第一个Bitmapkey2第二个Bitmap查看第一个bitmap的bit位值 192.168.79.129:1bitfield redisBitmap get u1 0 get u1 1 get u1 2 get u1 3 get u1 4 get u1 5 get u1 6 get u1 7 get u1 8 1) 0 2) 1 3) 1 4) 1 5) 1 6) 0 7) 0 8) 0 9) 0 设置第二个bitmap的bit位值 192.168.79.129:1bitfield redisBitmap2 set u1 0 1 set u1 1 1 1) 0 2) 0 查看第二个bitmap的bit位值 192.168.79.129:1bitfield redisBitmap2 get u1 0 get u1 1 get u1 2 1) 1 2) 1 3) 0 做与运算 192.168.79.129:1bitop and andResult redisBitmap redisBitmap2 1 第0位010第1位111第2位100前三个就是010 192.168.79.129:1getbit andResult 0 0 192.168.79.129:1getbit andResult 1 1 192.168.79.129:1getbit andResult 2 0应用去重多个用户多次登录去重记录一次就行Service RequiredArgsConstructor public class UserDedupService { //用户ID去重 private final StringRedisTemplate redisTemplate; //标记用户已存在 //返回booleantrue代表之前存在false代表之前不存在现在set进去 public boolean markUser(String key, long userId){ Boolean existed redisTemplate.opsForValue().setBit(key, userId, true); return existed ! null existed; } //判断用户是否存在 public boolean isExisted(String key, long userId){ Boolean isExisted redisTemplate.opsForValue().getBit(key, userId); return isExisted ! null isExisted; } //统计去重的用户数 public long countExistedUsers(String key){ Long count redisTemplate.execute( ((RedisCallbackLong) connection - connection.stringCommands().bitCount( redisTemplate.getStringSerializer().serialize(key) ) ) ); return count !null ? count : 0; } }SpringBootTest public class UserDedupTest { Autowired private UserDedupService service; Test void test(){ String key user:dedup; // 去重userId ListLong list Arrays.asList(1001L, 1002L, 1003L, 1004L, 1001L, 1004L, 1000L, 1005L, 1006L); //记录 for (Long userId : list) { boolean marked service.markUser(key, userId); if (marked){ System.out.println(用户 userId重复出现); }else { System.out.println(用户 userId第一次出现); } } //统计去重后的用户数 long count service.countExistedUsers(key); System.out.println(去重后的用户数为 count); //看某个用户是否存在 System.out.println(1001是否存在 service.isExisted(key, 1001L)); System.out.println(10是否存在 service.isExisted(key, 10L)); } }运行结果用户1001第一次出现 用户1002第一次出现 用户1003第一次出现 用户1004第一次出现 用户1001重复出现 用户1004重复出现 用户1000第一次出现 用户1005第一次出现 用户1006第一次出现 去重后的用户数为7 1001是否存在true 10是否存在false签到统计统一用户在一年内共签到多少次从某天往前推连续签到了多少天Service RequiredArgsConstructor public class UserSignService { //用户签到 private final StringRedisTemplate redisTemplate; // RedisKey 每个用户每个年份的签到记录键 private String buildKey(long userId, int year){ return sign: userId : year; } // 用户签到 public boolean sign(long userId, LocalDate date){ String key buildKey(userId,date.getYear()); long dayOffset date.getDayOfYear() - 1; Boolean signed redisTemplate.opsForValue().setBit(key, dayOffset, true); return signed ! null signed; } // 用户某天是否签到过 public boolean checkSign(long userId, LocalDate date){ long dateOffset date.getDayOfYear() -1; String key buildKey(userId,date.getYear()); Boolean bit redisTemplate.opsForValue().getBit(key, dateOffset); return bit ! null bit; } // 这一年的签到总数 public long countSigns(long userId, int year){ Long count redisTemplate.execute((RedisCallbackLong) connection - connection.stringCommands().bitCount( redisTemplate.getStringSerializer().serialize(buildKey(userId, year)) )); return count ! null ? count : 0; } // 从某天开始往前找连续签到的天数 public long countContinuousSigns(long userId, LocalDate date){ String key buildKey(userId, date.getYear()); // 某天对应的下标 long dayOffset date.getDayOfYear() - 1; long count 0; for (long i dayOffset; i 0; i--){ Boolean bit redisTemplate.opsForValue().getBit(key, i); if (bit ! null bit){ count; }else { break; } } return count; } }SpringBootTest public class UserSignTest { Autowired private UserSignService service; Test void test(){ long userId 20261007L; int year 2026; LocalDate today LocalDate.of(year, 10, 7); // 签到今天(7号)前天昨天都签到了 service.sign(userId, today); service.sign(userId, today.minusDays(1)); service.sign(userId, today.minusDays(2)); // 4号没签到1号签到 service.sign(userId, LocalDate.of(year, 10, 1)); //看某天签到没 System.out.println(10月7号是否签到 service.checkSign(userId, today)); System.out.println(10月4号是否签到 service.checkSign(userId, today.minusDays(3))); // 某年签到总数 long count service.countSigns(userId, year); System.out.println(签到总天数 count); // 某天往前连续签到天数 count service.countContinuousSigns(userId, today); System.out.println(从10月7日起连续签到天数 count); count service.countContinuousSigns(userId, today.minusDays(1)); System.out.println(从10月6日起连续签到天数 count); // 是否重复签到 System.out.println(10月7日是否重复签到 service.sign(userId, today)); } }运行结果10月7号是否签到true 10月4号是否签到false 签到总天数4 从10月7日起连续签到天数3 从10月6日起连续签到天数2 10月7日是否重复签到true模拟布隆过滤器实现并模拟误判布隆过滤器用来判断一个元素是否可能存在于集合中。它说这个元素存在可能存在它说这个元素不存在一定不存在它的底层就是Bitmap核心思路一个元素经过多个哈希函数映射到Bitmap的多个bit位这些位全部设为1。误判对一个不存在的元素求了多个bit位发现在Bitmap里这些位都是1但是这个元素却不存在于集合中。发生了误判Service RequiredArgsConstructor public class BloomFilterService { // 模拟布隆过滤器实现 private final StringRedisTemplate redisTemplate; // 三个hash函数 private static final int HASH_NUM 3; // 桶少点模拟误判 private static final int BITMAP_SIZE 32; // hash函数求槽位 // 模拟HashMap的hash函数 private ListLong hashOffset(String value){ ListLong offsets new ArrayList(HASH_NUM); int h1 value.hashCode(); h1 h1 ^ (h1 16); // 换个种子再扰动一次 int h2 (value lyl).hashCode(); h2 h2 ^ (h2 16); // 弄三个hash值映射bitmap槽位 for (int i 0; i HASH_NUM; i) { long offset Math.abs(h1 (long)i * h2) (BITMAP_SIZE - 1); offsets.add(offset); } return offsets; } // 添加元素到布隆过滤器 public void add(String key, String value){ for (Long offset : hashOffset(value)) { redisTemplate.opsForValue().setBit(key, offset, true); } } // 判断元素是否存在所有槽位都是1可能存在有一个是0就不存在 public boolean mightExist(String key, String value){ for (Long offset : hashOffset(value)) { if (Boolean.FALSE.equals(redisTemplate.opsForValue().getBit(key, offset))){ return false; } } return true; } }SpringBootTest public class BloomFilterTest { Autowired private BloomFilterService service; Test void testBloomFilter() { String key bloom:user; // 添加一批用户 ListString users Arrays.asList(张三, 李四, 王五, 赵六, 孙七, 周八, 吴九, 郑十); for (String user : users) { service.add(key, user); System.out.println(添加用户 user); } // 判断已添加的用户 for (String user : users) { boolean exists service.mightExist(key, user); System.out.println(用户 user 是否存在 exists); } // 判断未添加的用户桶只有32个模拟误判 ListString notAdded Arrays.asList(钱十一, 冯十二, 陈十三, 褚十四, 卫十五, 蒋十六, 沈十七, 韩十八); int falsePositiveCount 0; for (String user : notAdded) { boolean exists service.mightExist(key, user); if (exists) { falsePositiveCount; System.out.println(用户 user 是否存在 exists ← 误判实际不存在但判断为存在); } else { System.out.println(用户 user 是否存在 exists 确实不存在); } } System.out.println(未添加元素数 notAdded.size() 误判数 falsePositiveCount 误判率 String.format(%.2f, (double) falsePositiveCount / notAdded.size() * 100) %); } }运行结果添加用户张三 添加用户李四 添加用户王五 添加用户赵六 添加用户孙七 添加用户周八 添加用户吴九 添加用户郑十 用户张三是否存在true 用户李四是否存在true 用户王五是否存在true 用户赵六是否存在true 用户孙七是否存在true 用户周八是否存在true 用户吴九是否存在true 用户郑十是否存在true 用户钱十一是否存在true ← 误判实际不存在但判断为存在 用户冯十二是否存在true ← 误判实际不存在但判断为存在 用户陈十三是否存在true ← 误判实际不存在但判断为存在 用户褚十四是否存在false确实不存在 用户卫十五是否存在false确实不存在 用户蒋十六是否存在false确实不存在 用户沈十七是否存在false确实不存在 用户韩十八是否存在false确实不存在 未添加元素数8误判数3误判率37.50%

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑