Redis修行 — 位圖實戰

php

介紹

按照官網的說法,Redis位圖Bitmaps不是實際的數據類型,而是在字符串類型上定義的一組面向位的操做。在Redis中字符串限制最大爲512MB,因此位圖中最大能夠設置2^32個不一樣的位(42.9億個)。圖位的最小單位是比特(bit),每一個bit的值只能是0或1。css

位圖的存儲大小計算: (maxOffset / 8 / 1024 / 1024)MB。其中maxOffset爲位圖的最大位數java

基本用法

SETBIT key offset value

設置指定key的值在offset處的bit值,offset從0開始。返回值爲在offset處原來的bit值git

# 經過位操做將 h 改爲 i
127.0.0.1:6379> SET h h         # 二進制爲 01101000
OK
127.0.0.1:6379> SETBIT h 7 1    # 將最後一位改爲1 => 01101001
(integer) 0
127.0.0.1:6379> GET h
"i"
複製代碼

GETBIT key offset

獲取指定key的值在offset處的bit值,offset從0開始。若是offset超出了當前位圖的範圍,則返回0。github

127.0.0.1:6379set i i       # 二進制爲 01101001
OK
127.0.0.1:6379getbit i 0    # 第1位爲0
(integer) 0
127.0.0.1:6379getbit i 1    # 第2位爲0
(integer) 1
127.0.0.1:6379getbit i 7    # 第8位爲0
(integer) 1
複製代碼

BITCOUNT key [start end]

統計指定key值中被設置爲1的bit數。能夠經過指定參數star和end來限制統計範圍。web

注意,這裏的star和end不是指bit的下標,而是字節(byte)的下標。好比start爲1,則實際對應的bit下標爲8(1byte = 8 bit)redis

127.0.0.1:6379set hi hi           # 二進制爲 0110100001101001
OK
127.0.0.1:6379bitcount hi         # 全部是1的位數:7個
(integer) 7
127.0.0.1:6379bitcount hi 1 2     # 即統計 01101001 中1的位數
(integer) 4
複製代碼

BITPOS key bit [start] [end]

統計首次出現的0或1的bit位,能夠經過start和end來指定範圍,一樣是指字節的下標。數據庫

  • 在不存在的key或者空字符串中查找1,則返回-1
  • 在全部bit都爲1中查找bit爲0的狀況下,返回字符串最右邊的第一個空位
    127.0.0.1:6379get nilkey           # 不存在的key
    (nil)
    127.0.0.1:6379bitpos nilkey 1      # 在不存在的key中查首次出現1的位
    (integer-1
    127.0.0.1:6379setbit nilkey 0 0    # 空字符串
    (integer) 0
    127.0.0.1:6379get nilkey
    "\x00"
    127.0.0.1:6379bitpos nilkey 1
    (integer-1
複製代碼

BITOP operation destkey key [key …]

對一個或多個二進制位字符串進行操做,並將結果保存到 destkey 上。當某個字符串長度不夠時,對應的位用0補上ruby

  • AND(邏輯與):都爲1返回1,不然返回0
    127.0.0.1:6379set a a                  # 二進制  01100001
    OK
    127.0.0.1:6379set c c                  # 二進制  01100011
    OK
    127.0.0.1:6379bitop and destkey a c    # 與操做  01100001 -a
    (integer) 1
    127.0.0.1:6379get destkey
    "a"
複製代碼
  • OR(邏輯或):只要有一個1就返回1,不然返回0
    127.0.0.1:6379set a a                 # 二進制  01100001
    OK
    127.0.0.1:6379set b b                 # 二進制  01100010
    OK
    127.0.0.1:6379bitop or destkey a b    # 或操做  01100011 -c
    (integer) 1
    127.0.0.1:6379get destkey
    "c"
    127.0.0.1:6379>     
複製代碼
  • XOR(邏輯異或):當都是0或者都是1時返回0,不然返回1
    127.0.0.1:6379set a a                 # 二進制  01100001
    OK
    127.0.0.1:6379set z Z                 # 二進制  01011010 (大寫的Z)
    OK
    127.0.0.1:6379bitop xor destkey a z   # 異或    00111011 -> ; 分號
    (integer) 1
    127.0.0.1:6379get destkey
    ";"
複製代碼
  • NOT(邏輯非):取反,1變成0,0變成1。只能傳入一個要操做的key
    01010101 -> 10101010
複製代碼

場景實戰

這裏用一個用戶簽到的例子來說解如何在實戰中應用,需求:微信

  • 實現用戶簽到
  • 統計今天全部的簽到數量
  • 獲取指定用戶整年的簽到數
  • 統計近7天連續簽到的用戶數量
  • 統計本月所有簽到過的用戶數量
  • 統計近7天有過簽到的用戶數量

使用位圖的好處:

  • 最直觀的一點佔用存儲少,1我的1年的數據也就365 bit,46個字節;
  • 經過位運算操做多個字符串,效率高
  • 當別人還在用數據庫記錄簽到信息的時候,你用位圖操做,逼格一下就上去了;

這裏基於SpringBoot進行演示:

  • 天天的簽到狀況做爲一條記錄,key格式爲sign:{yyyyMMdd}
  • 用戶ID做爲偏移量

用戶簽到

將用戶ID做爲偏移量,經過setBit設置該位置的值爲1

查詢用戶今天是否已經簽到了

將用戶ID做爲偏移量,經過getBit查詢該位置上的值是否爲1

統計今天全部的簽到數量

經過bitCount去實現統計

統計指定用戶整年的簽到數

Redis中並無提供對多個二進制位字符串進行求和操做,咱們須要本身去統計。思路:

  • 獲取本年全部簽到記錄的key列表,即sign:2020開頭的key,能夠經過Redis指令keys sign:2020*獲取
  • 遍歷獲取到的key列表,統計已經簽到過的key的數量

統計近7天連續簽到的用戶數量

  • 對近7天的簽到記錄的進行邏輯與操做,生成一個連續七天簽到的記錄
  • 對生成的記錄進行bitCount

統計近7天有過簽到的用戶數量

和統計7天連續簽到思路同樣,只是這裏使用邏輯或操做

完整代碼

Service

@Service
public class RedisService {

    private final StringRedisTemplate stringRedisTemplate;

    public RedisService(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 獲取指定格式的key
     *
     * @param pattern 格式
     * @return set
     */

    public Set<String> getKeys(String pattern) {
        return stringRedisTemplate.keys(pattern);
    }

    /**
     * 設置指定位的值
     *
     * @param key    鍵
     * @param offset 偏移量 0開始 對應bit的位置
     * @param value  true爲1,false爲0
     * @return boolean
     */

    public Boolean setBit(String key, long offset, boolean value) {
        return stringRedisTemplate.opsForValue().setBit(key, offset, value);
    }

    /**
     * 獲取指定位的值
     *
     * @param key    鍵
     * @param offset 偏移量 0開始
     * @return boolean
     */

    public Boolean getBit(String key, long offset) {
        return stringRedisTemplate.opsForValue().getBit(key, offset);
    }

    /**
     * 統計字符串被設置爲1的bit數
     *
     * @param key 鍵
     * @return long
     */

    public Long bitCount(String key) {
        return stringRedisTemplate.execute(
                (RedisCallback<Long>) connection -> connection.bitCount(key.getBytes())
        );
    }

    /**
     * 統計字符串指定位上被設置爲1的bit數
     *
     * @param key   鍵
     * @param start 開始位置  注意對應byte的位置,是bit位置*8
     * @param end   結束位置
     * @return long
     */

    public Long bitCount(String key, long start, long end) {
        return stringRedisTemplate.execute(
                (RedisCallback<Long>) connection -> connection.bitCount(key.getBytes(), start, end)
        );
    }

    /**
     * 不一樣字符串之間進行位操做
     *
     * @param op      操做類型:與、或、異或、否
     * @param destKey 最終存放結構的鍵
     * @param keys    要操做的鍵
     * @return Long
     */

    public Long bitOp(RedisStringCommands.BitOperation op, String destKey, Collection<String> keys) {
        int size = keys.size();
        byte[][] bytes = new byte[size][];

        int index = 0;
        for (String key : keys) {
            bytes[index++] = key.getBytes();
        }
        return stringRedisTemplate.execute((RedisCallback<Long>) con -> con.bitOp(op, destKey.getBytes(), bytes));
    }

    /**
     * 對符合指定格式的key值進行未操做
     *
     * @param op      操做類型:與、或、異或、否
     * @param destKey 存放結果的鍵
     * @param pattern key格式
     * @return Long
     */

    public Long bitOp(RedisStringCommands.BitOperation op, String destKey, String pattern) {
        Set<String> keys = getKeys(pattern);
        int size = keys.size();
        if (size == 0) {
            return 0L;
        }
        byte[][] bytes = new byte[size][];

        int index = 0;
        for (String key : keys) {
            bytes[index++] = key.getBytes();
        }
        return stringRedisTemplate.execute((RedisCallback<Long>) con -> con.bitOp(op, destKey.getBytes(), bytes));
    }
}
複製代碼

controller

@RestController
@RequestMapping("/redis/bit")
public class BitMapController {

    private final DateTimeFormatter formatters = DateTimeFormatter.ofPattern("yyyyMMdd");

    /**
     * 定義簽到前綴
     * key格式爲 sing:{yyyyMMdd}
     */

    private static final String SIGN_PREFIX = "sign:";

    /**
     * 連續一週簽到
     */

    private static final String SIGN_ALL_WEEK_KEY = "signAllWeek";

    /**
     * 連續一個月簽到
     */

    private static final String SIGN_ALL_MONTH_KEY = "signAllMonth";

    /**
     * 一週內有簽到過的
     */

    private static final String SIGN_IN_WEEK_KEY = "signInWeek";

    private final RedisService redisService;

    public BitMapController(RedisService redisService) {
        this.redisService = redisService;
    }

    /**
     * 初始化本年今天以前的測試數據
     */

    @GetMapping("/init")
    public void initData() {
        // 獲取本年的日期列表
        List<String> dateKeyList = new ArrayList<>();
        LocalDate curDate = LocalDate.now();
        LocalDate beginDate = LocalDate.parse("2020-01-01");
        while (beginDate.isBefore(curDate)) {
            dateKeyList.add(SIGN_PREFIX + beginDate.format(formatters));
            beginDate = beginDate.plusDays(1);
        }
        // 是否簽到
        boolean isSign;
        StringBuilder signInfo;
        for (int i = 1; i < 6; i++) {
            signInfo = new StringBuilder("用戶【").append(i).append("】:");
            for (String dateKey : dateKeyList) {
                if (i == 1) {
                    // 用戶1所有簽到
                    isSign = true;
                } else {
                    // 其餘用戶隨機
                    isSign = Math.random() > 0.5;
                }
                redisService.setBit(dateKey, i, isSign);
                signInfo.append(isSign ? 1 : 0).append(", ");
            }
            System.out.println(signInfo.toString());
        }
    }

    /**
     * 用戶當天簽到
     * 用戶ID做爲位圖的偏移量
     */

    @GetMapping("/sign/{userId}")
    public String sign(@PathVariable Long userId) {
        redisService.setBit(SIGN_PREFIX + getCurDate(), userId, true);
        return "簽到成功";
    }

    /**
     * 查詢用戶今天是否已經簽到了
     */

    @GetMapping("/isSign/{userId}")
    public String isSign(@PathVariable Long userId) {
        Boolean isSign = redisService.getBit(SIGN_PREFIX + getCurDate(), userId);
        if (isSign) {
            return String.format("用戶【%d】今日已簽到", userId);
        }
        return String.format("用戶【%d】今日還沒有簽到,請簽到", userId);
    }

    /**
     * 統計今天全部的簽到數量
     */

    @GetMapping("/todayCount")
    public String todayCount() {
        return String.format("今日已簽到人數: %d", redisService.bitCount(SIGN_PREFIX + getCurDate()));
    }

    /**
     * 統計指定用戶整年的簽到數
     */

    @GetMapping("/userYearSign/{userId}")
    public String userYearSign(@PathVariable Long userId) {
        int year = LocalDate.now().getYear();
        // 獲取全部的key
        Set<String> keys = redisService.getKeys(SIGN_PREFIX + year + "*");
        /*
         * 可使用BitSet 去存儲用戶天天的簽到信息,用於其餘的操做
         * BitSet users = new BitSet();
         * 統計全部已經簽到的數量 對應 redis的bitCount
         * users.cardinality()
         */

        int signCount = 0;
        for (String key : keys) {
            if (redisService.getBit(key, userId)) {
                signCount++;
            }
        }
        return String.format("本年已累計簽到: %d 次", signCount);
    }

    /**
     * 統計近7天連續簽到的用戶數量
     * 邏輯與
     */

    @GetMapping("/signAllWeek")
    public String signAllWeek() {
        List<String> weekDays = getWeekKeys();
        redisService.bitOp(RedisStringCommands.BitOperation.AND, SIGN_ALL_WEEK_KEY, weekDays);
        return String.format("近7天連續簽到用戶數:%d", redisService.bitCount(SIGN_ALL_WEEK_KEY));
    }

    /**
     * 統計本月所有簽到過的用戶數量
     */

    @GetMapping("/signAllMonth")
    public String signAllMonth() {
        redisService.bitOp(
                RedisStringCommands.BitOperation.AND,
                SIGN_ALL_MONTH_KEY,
                SIGN_PREFIX + LocalDate.now().getYear()
        );
        return String.format("月所有簽到過的用戶數:%d", redisService.bitCount(SIGN_ALL_MONTH_KEY));
    }

    /**
     * 統計近7天有過簽到的用戶數量,只簽到1次也算
     * 邏輯或
     */

    @GetMapping("/signInWeek")
    public String signInWeek() {
        List<String> weekDays = getWeekKeys();
        redisService.bitOp(RedisStringCommands.BitOperation.OR, SIGN_IN_WEEK_KEY, weekDays);
        return String.format("近7天有過簽到的用戶數:%d", redisService.bitCount(SIGN_IN_WEEK_KEY));
    }

    /**
     * 獲取當天的日期
     *
     * @return yyyyMMdd
     */

    private String getCurDate() {
        return LocalDate.now().format(formatters);
    }

    /**
     * 獲取近一週的日期對應的key
     */

    private List<String> getWeekKeys() {
        List<String> dateList = new ArrayList<>();
        LocalDate curDate = LocalDate.now();
        dateList.add(SIGN_PREFIX + curDate.format(formatters));
        for (int i = 1; i < 7; i++) {
            dateList.add(SIGN_PREFIX + curDate.plusDays(-i).format(formatters));
        }
        return dateList;
    }
}
複製代碼

補充

  • 上述例子中咱們默認用戶ID是數字類型,若是大家的用戶ID是字符串的,那麼能夠將用戶ID做爲key,取當天是今年的第幾天做爲偏移量,這樣一天記錄就是一我的整年的簽到記錄;
  • 在進行BITOP操做時會從新生成一個結果的key,能夠在天天凌晨經過定時任務去統計以前的記錄來生成這個結果key,這樣在業務中就能夠直接經過這個結果key來統計數據

訪問源碼

全部代碼均上傳至Github上,方便你們訪問

>>>>>> Redis實戰 — 位圖 <<<<<<

平常求贊

創做不易,若是各位以爲有幫助,求點贊 支持


求關注

微信公衆號: 俞大仙

相關文章
相關標籤/搜索