setbit命令

  通过一个bit位来表示某个元素对应的值或者状态,其中的key就是对应元素本身。8个bit可以组成一个Byte,所以bitmap本身会极大的节省储存空间。

 语法:setbit key offset value

描述:

    对key所储存的字符串值,设置或清除指定偏移量上的位(bit)。

    位的设置或清除取决于 `value` 参数,可以是 `0` 也可以是 `1` 。

    当 `key` 不存在时,自动生成一个新的字符串值。

    字符串会进行伸展(grown)以确保它可以将 `value` 保存在指定的偏移量上。当字符串值进行伸展时,空白位置以 `0` 填充。

 注意:

    `offset` 参数必须大于或等于 `0` ,小于 2^32 (bit 映射被限制在 512 MB 之内)。

    因为 Redis 字符串的大小被限制在 512 兆(megabytes)以内, 所以用户能够使用的最大偏移量为 2^29-1(536870911) , 如果你需要使用比这更大的空间, 请使用多个 `key。`

    当生成一个很长的字符串时, Redis 需要分配内存空间, 该操作有时候可能会造成服务器阻塞(block)。 在2010年出产的Macbook Pro上, 设置偏移量为 536870911(512MB 内存分配)将耗费约 300 毫秒, 设置偏移量为 134217728(128MB 内存分配)将耗费约 80 毫秒, 设置偏移量 33554432(32MB 内存分配)将耗费约 30 毫秒, 设置偏移量为 8388608(8MB 内存分配)将耗费约 8 毫秒。

    语法:bitcount key [start] [end]

  返回值:被设置为 1 的位的数量

  描述:

    计算给定字符串中,被设置为 1 的比特位的数量

    一般情况下,给定的整个字符串都会被进行计数,通过指定额外的 start 或 end 参数,可以让计数只在特定的字节上进行。注意不是bit位,是字节。
      例如:假如key1的value是00001100 11001000 11110000
      <1> bitcount key1 0 0 
          这个是获取key1中第0个字节组中bit为1的count,也就是00001100 中查询,返回2
      <2> bitcount key1 0 1 
          这个是获取key1中第0-1个字节组中bit为1的count,也就是00001100 11001000中查询,返回5  
      <3> bitcount key1 1 2 
          这个是获取key1中第1-2个字节组中bit为1的count,也就是11001000 11110000中查询,返回7   

    start 和 end 参数的设置和 GETRANGE key start end 命令类似,都可以使用负数值: 比如 -1表示最后一个bit, -2 表示倒数第二个bit,以此类推。

    不存在的 key 被当成是空字符串来处理,因此对一个不存在的 key 进行 BITCOUNT 操作,结果为 0

使用场景:用户签到

  考虑到每月初需要重置连续签到次数,按用户每月存一条签到数据(也可以每年存一条数据)。Key的格式为u:sign:uid:yyyyMM,Value则采用长度为4个字节(32位)的位图(最大月份只有31天)。位图的每一位代表一天的签到,1表示已签,0表示未签。

  例如u:sign:1000:201902表示ID=1000的用户在2019年2月的签到记录。

# 用户2月17号签到
SETBIT u:sign:1000:201902 16 1 # 偏移量是从0开始,所以要把17减1

# 检查2月17号是否签到
GETBIT u:sign:1000:201902 16 # 偏移量是从0开始,所以要把17减1

# 统计2月份的签到次数
BITCOUNT u:sign:1000:201902

# 获取2月份前28天的签到数据
BITFIELD u:sign:1000:201902 get u28 0

# 获取2月份首次签到的日期
BITPOS u:sign:1000:201902 1 # 返回的首次签到的偏移量,加上1即为当月的某一天

 代码

@Slf4j
@Service
public class SignService {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 用户签到
     *
     * @param uid
     * @param localDate
     * @return
     */
    public Boolean doSign(int uid, LocalDate localDate) {
        int offset = localDate.getDayOfMonth() - 1;
        String signKey = buildSignKey(uid, localDate);
        return redisTemplate.opsForValue().setBit(signKey, offset, true);
    }

    /**
     * 检查用户是否签到
     *
     * @param uid
     * @param date
     * @return
     */
    public Boolean checkSign(int uid, LocalDate date) {
        int offset = date.getDayOfMonth() - 1;
        String signKey = buildSignKey(uid, date);
        return redisTemplate.opsForValue().getBit(signKey, offset);
    }

    /**
     * 获取签到次数
     *
     * @param uid
     * @param date
     * @return
     */
    public long getSignCount(int uid, LocalDate date) {
        String signKey = buildSignKey(uid, date);
        return (long) redisTemplate.execute((RedisCallback<Long>) conn -> conn.bitCount(signKey.getBytes()));
    }

    /**
     * 获得当月首次签到日期
     *
     * @param uid
     * @param date
     * @return
     */
    public LocalDate getFirstSignDate(int uid, LocalDate date) {
        String signKey = buildSignKey(uid, date);
        long pos = (long) redisTemplate.execute((RedisCallback<Long>) conn -> conn.bitPos(signKey.getBytes(), true));
        return pos < 0 ? null : date.withDayOfMonth((int) (pos + 1));
    }

    /**
     * 获取连续签到次数
     * 0是高位
     *
     * @return
     */
    public long getContinuousSignCount(int uid, LocalDate date) {
        int signCount = 0;
        String signKey = buildSignKey(uid, date);
        int dayOfMonth = date.getDayOfMonth();
        List<Long> list = (List<Long>) redisTemplate.execute((RedisCallback<List<Long>>) conn ->
                conn.bitField(signKey.getBytes(),
                        BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0)));
        if (list != null && list.size() > 0) {
            long v = list.get(0) == null ? 0 : list.get(0);
            for (int i = 0; i < date.getDayOfMonth(); i++) {
                /**
                 * 取低位连续不为0的个数即为连续签到次数,需考虑当天尚未签到的情况
                 */
                if (v >> 1 << 1 == v) {
                    //低位为0且非当天说明连续签到中断了
                    if (i > 0) {
                        break;
                    }
                } else {
                    signCount += 1;
                }
                v >>= 1;
            }
        }
        return signCount;
    }

    public Map<String, Boolean> getSignInfo(int uid, LocalDate date) {
        Map<String, Boolean> signMap = new HashMap<>(date.getDayOfMonth());
        int lengthOfMonth = date.lengthOfMonth();
        String signKey = buildSignKey(uid, date);
        List<Long> list = (List<Long>) redisTemplate.execute((RedisCallback<List<Long>>) conn ->
                conn.bitField(signKey.getBytes(),
                        BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(lengthOfMonth)).valueAt(0)));
        if (list != null && list.size() > 0) {
            long v = list.get(0) == null ? 0 : list.get(0);
            for (int i = date.lengthOfMonth(); i > 0; i--) {
                LocalDate d = date.withDayOfMonth(i);
                signMap.put(formatDate(d, "yyyy-MM-dd"), v >> 1 << 1 != v);
                v >>= 1;
            }
        }
        return signMap;
    }

    /**
     * 构建签到key
     *
     * @param uid
     * @param date
     * @return
     */
    private String buildSignKey(int uid, LocalDate date) {
        String monthSuffix = formatDate(date, "yyyyMM");
        return String.format("u:sign:%d:%s", uid, monthSuffix);
    }

    /**
     * 日期格式化
     *
     * @param date
     * @param pattern
     * @return
     */
    public String formatDate(LocalDate date, String pattern) {
        return date.format(DateTimeFormatter.ofPattern(pattern));
    }
}

场景二:统计活跃用户

  使用时间作为cacheKey,然后用户ID为offset,如果当日活跃过就设置为1,那么我该如果计算某几天/月/年的活跃用户呢(暂且约定,统计时间内只有有一天在线就称为活跃),有请下一个redis的命令
  命令 BITOP operation destkey key [key ...]
  说明:对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。
  说明:BITOP 命令支持 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种参数

  假设当前站点有5000W用户,那么一天的数据大约为50000000/8/1024/1024=6MB

使用场景三:用户在线状态

  对方给我提供了一个查询当前用户是否在线的接口。不了解对方是怎么做的,自己考虑了一下,使用bitmap是一个节约空间效率又高的一种方法,只需要一个key,然后用户ID为offset,如果在线就设置为1,不在线就设置为0,和上面的场景一样,5000W用户只需要6MB的空间。

  

  

posted on 2021-08-08 00:42  溪水静幽  阅读(1433)  评论(0编辑  收藏  举报