Redis: Using BitMap to implement user check-in

BitMap

We can complete the check-in function through mysql. For example, the following table

A user’s check-in is a record. If there are 10 million users and the average number of check-ins per person per year is 10, then the amount of data in this table per year will be 100 million.

Each check-in requires a total of 22 bytes of memory (8 + 8 + 1 + 1 + 3 + 1), which requires up to 600 bytes per month.

How can we simplify it a little bit? In fact, we can consider a very common solution when we were young. When we were young, we prepared a small card. As long as you signed in, you would put a check mark. I would finally judge whether you signed in. In fact, you only need to look at the small card to know.

We can use a solution like this to realize our check-in needs.

We count user check-in information on a monthly basis. The sign-in record is 1, and the non-sign-in record is 0.

Each bit corresponds to each day of the month, forming a mapping relationship. Using 0 and 1 to mark the business status is called a BitMap. In this way, we can use a very small space to represent a large amount of data.

Redis uses string type data structure to implement BitMap, so the maximum upper limit is 512M, which is 2^32 bits when converted into bits.

BitMap operation commands are:

  • SETBIT: Store a 0 or 1 at the specified location (offset)
  • GETBIT: Get the bit value at the specified position (offset)
  • BITCOUNT: Counts the number of bits with a value of 1 in the BitMap
  • BITFIELD: operate (query, modify, increment) the value of the specified position (offset) in the bit array in BitMap
  • BITFIELD_RO: Get the bit array in BitMap and return it in decimal form
  • BITOP: Perform bit operations (AND, OR, XOR) on the results of multiple BitMap
  • BITPOS: Find the position where the first 0 or 1 appears in the specified range in the bit array

Implement the check-in function

Requirements: Implement the check-in interface and save the current user’s check-in information for the day to Redis.

Idea: We can use the year and month as the key of the bitMap, and then save it in a bitMap. Every time you check in, go to the corresponding position and change the number from 0 to 1. As long as the corresponding number is 1, it means that you have checked in on that day. , otherwise there will be no sign-in.

We found through the interface document that this interface does not pass any parameters. How can we know exactly which day to sign in without parameters? This is very easy. You can get it directly through the background code, and then go to the corresponding address to modify the bitMap.

UserController

 @PostMapping("/sign")
 public Result sign(){
    return userService.sign();
 }

UserServiceImpl

@Override
public Result sign() {
    // 1. Get the current logged in user
    Long userId = UserHolder.getUser().getId();
    // 2. Get the date
    LocalDateTime now = LocalDateTime.now();
    // 3. Splice key
    String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
    String key = USER_SIGN_KEY + userId + keySuffix;
    // 4. Get the day of the month today is
    int dayOfMonth = now.getDayOfMonth();
    // 5. Write Redis SETBIT key offset 1
    stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
    return Result.ok();
}

Sign-in statistics

Question 1: What is the number of consecutive check-in days? Starting from the last check-in and counting forward until the first failure to check-in is encountered, the total number of check-ins is calculated, which is the number of consecutive check-in days.

Java logic code: Get the last check-in data of the current month, define a counter, and then keep counting forward until the first non-0 number is obtained. Every time a non-0 number is obtained, the counter + 1, until After traversing all the data, you can get the total number of check-in days in the current month.

Question 2: How to get all the check-in data from this month to today?

BITFIELD key GET u[dayOfMonth] 0

Suppose today is the 10th, then we can start from the first day of the current month and get the number of digits of the current day. If it is the 10th, then it is 10 digits. If we get the data for this period, we can get all The data is there, so how many times have you checked in in these 10 days? Just count how many 1’s there are.

Question 3: How to traverse each bit from back to front?

Note: The data returned by bitMap is in decimal. If it returns a number 8, how do I know which ones are 0 and which ones are 1? We only need to AND the decimal number obtained with 1, because 1 is 1 only when it meets 1, and other numbers are 0. We AND the sign-in result with 1, and each time it is ANDed, By moving the check-in result one position to the right and pushing it inward one by one, we can complete the effect of traversing one by one.

Requirement: Implement the following interface to count the number of consecutive check-in days of the current user as of the current time in this month.

When a user has time, we can organize the corresponding key. At this time, we can find all the check-in records of this user as of this day, and then based on this set of algorithms, we can count the number of consecutive check-ins he has had.

UserController

@GetMapping("/sign/count")
public Result signCount(){
    return userService.signCount();
}

UserServiceImpl

@Override
public Result signCount() {
    // 1. Get the current logged in user
    Long userId = UserHolder.getUser().getId();
    // 2. Get the date
    LocalDateTime now = LocalDateTime.now();
    // 3. Splice key
    String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
    String key = USER_SIGN_KEY + userId + keySuffix;
    // 4. Get the day of the month today is
    int dayOfMonth = now.getDayOfMonth();
    // 5. Get all the sign-in records of this month as of today, and return a decimal number BITFIELD sign:5:202203 GET u14 0
    List<Long> result = stringRedisTemplate.opsForValue().bitField(
            key,
            BitFieldSubCommands.create()
                    .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0)
    );
    if (result == null || result.isEmpty()) {
        // No sign-in results
        return Result.ok(0);
    }
    Long num = result.get(0);
    if (num == null || num == 0) {
        return Result.ok(0);
    }
    // 6. Loop through
    int count = 0;
    while (true) {
        // 6.1. Let this number be ANDed with 1 to get the last bit of the number // Determine whether this bit is 0
        if ((num & 1) == 0) {
            // If it is 0, it means no sign-in, end
            break;
        }else {
            // If it is not 0, it means you have signed in, and the counter + 1
            count + + ;
        }
        // Shift the number one bit to the right, discard the last bit, and continue with the next bit.
        num >>>= 1;
    }
    return Result.ok(count);
}