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的空間。

  

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM