Практика Redis — растровый бой

Redis

учиться никто конец территория , и июнь общий нежелание .

Связанные серии

вводить

Согласно официальному сайту, растровые изображения Redis Bitmaps не являются фактическим типом данных, а набором определенных в строковом типе.бит-ориентированные операции. Ограничение строк в Redis max составляет512MB, поэтому максимум можно установить в растровом изображении2^32разные биты(4,29 миллиарда). Наименьшая единица бита — бит, и значение каждого бита может быть только 0 или 1.

Расчет размера хранилища растровых изображений: (maxOffset / 8 / 1024 / 1024) МБ. где maxOffset — максимальное количество бит растрового изображения.

Основное использование

SETBIT key offset value

Установить битовое значение указанного значения ключа по смещению, смещение начинается с 0. Возвращаемое значение является исходным значением бита со смещением

# 通过位操作将 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

Получить битовое значение указанного значения ключа по смещению, смещение начинается с 0. Возвращает 0, если смещение выходит за пределы диапазона текущего растрового изображения.

127.0.0.1:6379> set i i       # 二进制为 01101001
OK
127.0.0.1:6379> getbit i 0    # 第1位为0
(integer) 0
127.0.0.1:6379> getbit i 1    # 第2位为0
(integer) 1
127.0.0.1:6379> getbit i 7    # 第8位为0
(integer) 1

BITCOUNT key [start end]

Подсчитайте количество битов, установленных в 1 в указанном значении ключа. Статистический диапазон можно ограничить, указав параметры star и end.

Обратите внимание, что звездочка и конец здесь относятся не к индексу бита, а к индексу байта. Например, если начало равно 1, фактический соответствующий битовый индекс равен 8 (1 байт = 8 бит).

127.0.0.1:6379> set hi hi           # 二进制为 0110100001101001
OK
127.0.0.1:6379> bitcount hi         # 所有是1的位数:7个
(integer) 7
127.0.0.1:6379> bitcount hi 1 2     # 即统计 01101001 中1的位数
(integer) 4

BITPOS key bit [start] [end]

Считая первое вхождение бита 0 или 1, диапазон может быть указан по началу и концу, что также относится к нижнему индексу байта.

  • Найти 1 в несуществующем ключе или пустой строке, вернуть -1
  • В случае, когда все биты равны 1, а бит равен 0, вернуть первый пустой бит в крайнем правом углу строки.
    127.0.0.1:6379> get nilkey           # 不存在的key
    (nil)
    127.0.0.1:6379> bitpos nilkey 1      # 在不存在的key中查首次出现1的位
    (integer) -1
    127.0.0.1:6379> setbit nilkey 0 0    # 空字符串
    (integer) 0
    127.0.0.1:6379> get nilkey
    "\x00"
    127.0.0.1:6379> bitpos nilkey 1
    (integer) -1

Ключ назначения операции BITOP [клавиша …]

Работает с одной или несколькими строками битов и сохраняет результат в ключе назначения. Когда длины определенной строки недостаточно, соответствующий бит заполняется 0

  • И (логическое И): вернуть 1, если оба равны 1, иначе вернуть 0
    127.0.0.1:6379> set a a                  # 二进制  01100001
    OK
    127.0.0.1:6379> set c c                  # 二进制  01100011
    OK
    127.0.0.1:6379> bitop and destkey a c    # 与操作  01100001 -> a
    (integer) 1
    127.0.0.1:6379> get destkey
    "a"
  • ИЛИ (логическое ИЛИ): возвращает 1, если есть 1, в противном случае возвращает 0
    127.0.0.1:6379> set a a                 # 二进制  01100001
    OK
    127.0.0.1:6379> set b b                 # 二进制  01100010
    OK
    127.0.0.1:6379> bitop or destkey a b    # 或操作  01100011 -> c
    (integer) 1
    127.0.0.1:6379> get destkey
    "c"
    127.0.0.1:6379>     
  • XOR (исключающее логическое ИЛИ): возвращает 0, когда оба равны 0 или 1, в противном случае возвращает 1.
    127.0.0.1:6379> set a a                 # 二进制  01100001
    OK
    127.0.0.1:6379> set z Z                 # 二进制  01011010 (大写的Z)
    OK
    127.0.0.1:6379> bitop xor destkey a z   # 异或    00111011 -> ; 分号
    (integer) 1
    127.0.0.1:6379> get destkey
    ";"
  • НЕ (логическое НЕ): Отрицательное значение, 1 становится 0, 0 становится 1.Можно передать только один ключ для работы.
    01010101 -> 10101010

Сцена боя

Вот пример входа пользователя, чтобы объяснить, как применить его в реальном бою.

  • Реализовать вход пользователя
  • Подсчитайте все проверки сегодня
  • Получить количество чекинов указанного пользователя за весь год
  • Подсчитайте количество пользователей, которые непрерывно регистрировались в течение последних 7 дней.
  • Подсчитайте количество пользователей, которые зарегистрировались в этом месяце
  • Подсчитайте количество пользователей, которые вошли в систему за последние 7 дней.

Преимущества использования растровых изображений:

  • Самая интуитивная точка для занятияменьше памяти, данные одного человека за год — 365 бит, 46 байт;
  • Манипулировать несколькими строками с помощью побитовых операций,эффективный;
  • Когда другие все еще используют базу данных для записи информации о регистрации, вы используете операции с битовыми картами,сильныйподняться сразу;

Вот демонстрация на основе SpringBoot:

  • Ситуация ежедневной регистрации используется в качестве записи, а формат ключаsign:{yyyyMMdd}
  • ID пользователя как смещение

Вход пользователя

Используя идентификатор пользователя в качестве смещения, передайтеsetBitУстановите значение этой позиции на 1

Проверьте, вошел ли пользователь сегодня

Используя идентификатор пользователя в качестве смещения, передайтеgetBitЗапросить, равно ли значение в этой позиции 1

Подсчитайте все проверки сегодня

пройти черезbitCountвнедрить статистику

Подсчитайте количество чекинов указанных пользователей в течение года

Redis не обеспечивает операции суммирования нескольких двоичных битовых строк, нам нужно их посчитать самостоятельно. Идеи:

  • Получите список ключей всех записей регистрации в этом году, т.е.sign:2020Ключ в начале можно передать командой Rediskeys sign:2020*Получать
  • Просмотрите полученный список ключей и подсчитайте количество ключей, которые были подписаны.

Подсчитайте количество пользователей, которые непрерывно регистрировались в течение последних 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;
    }
}

Пополнить

  • В приведенном выше примере наш идентификатор пользователя по умолчанию является числовым типом.Если ваш идентификатор пользователя представляет собой строку, вы можете использовать идентификатор пользователя в качестве ключа и взять день года в качестве смещения, чтобы запись одного дня это целый год одного человека.
  • в ходе выполненияBITOPВо время операции ключ результата будет сгенерирован заново, и ключ результата может быть сгенерирован путем подсчета предыдущих записей с помощью запланированной задачи рано утром каждого дня, чтобы ключ результата можно было использовать для непосредственного подсчета данных в бизнес.

Доступ к исходному коду

Весь код загружен на Github для быстрого доступа

>>>>>> Боевой Redis — растровое изображение

ежедневные комплименты

Это не легко создать, если вы найдете это полезным,попросить лайкслужба поддержки


Поиск внимания

Публичный аккаунт WeChat: Ю Дасянь