I made a sign-in function, and the architect thought it could be optimized

background

I believe everyone is familiar with the sign-in function. The function is that users can sign-in once a day, and they can get rewards for a fixed number of consecutive days. Here I simplify the function:

  • Each user can only sign in once a day;

  • Get coupons for 7 consecutive days after signing in;

The interface is like this:

Sign in

Check-in is a good gadget for drainage, let’s see how I did it (here for explanation, the logic becomes simpler. It may be more complicated in practice).

my design

First, a check-in form is needed signto record the check-in time.

CREATE TABLE `sign` (
  `id` varchar(255) CHARACTER SET utf8mb4 NOT NULL COMMENT '主键id',
  `user_id` varchar(255) CHARACTER SET utf8mb4 DEFAULT '' COMMENT '用户id',
  `sign_date` datetime DEFAULT NULL COMMENT '签到时间',
  KEY `idx_id` (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='签到';

You also need a table that counts consecutive check-ins continue_sign. Used to count the number of consecutive check-ins.

CREATE TABLE `continue_sign` (
  `id` varchar(255) CHARACTER SET utf8mb4 NOT NULL COMMENT '主键id',
  `user_id` varchar(255) CHARACTER SET utf8mb4 DEFAULT '' COMMENT '用户id',
  `continue_counts` int(11) DEFAULT NULL COMMENT '连续签到次数',
  KEY `idx_id` (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='连续签到统计';

Let's take a look at the Java code:

First, the user signs in and inserts data into the sign-in table. A user can only sign in once on the same day. If the user has already checked in, and the user checks in on the same day, the following prompt will appear.

After data is inserted into the check-in table, the check-in table is continuously counted.

addSignCountsById()Method code:

Accumulate the number of check-ins.

When the number of consecutive check-ins is greater than or equal to 7, a coupon will be issued.

signThe table data is as follows:

continue_signThe table data is as follows:

In this way, we have completed the sign-in function with MySQL.

Architect optimization

The architect looked at it and said that with the development of time, the sign-in table data will become larger and larger, if there are tens of thousands of users, or even tens of millions. The query is slower, which will affect the user experience. At this time, we must consider caching, but also consider sub-database and table.

But a small sign-in function can do so much. Is there an easier way?

There is a data structure Bitmap in Redis that can solve this problem.

Bitmap is a binary array with unlimited length (when the length is 2 billion, it occupies more than 200 MB of memory). The value in the array is 0 or 1.

For example: sign:1:202009 indicates the check-in record of the user with id 1 in September 2020

SETBIT sign:1:202009 0 1     #偏移量0开始,表示9月1号签到一天
SETBIT sign:1:202009 1 1     #表示9月2号签到一天
SETBIT sign:1:202009 2 1     #表示9月3号签到一天

BITCOUNT sign:1:202009       # 统计2月份的签到次数

BITFIELD sign:1:202009 get u30 0    #获取前30位的情况

Java code implementation

Sign-in method:

/**
 * 签到
 * @param userId
 * @param date
 * @return
 */
public boolean doSign(int userId, LocalDate date) {
    int offset = date.getDayOfMonth() - 1;
    return jedis.setbit(buildSignKey(userId, date),offset,true);
}

Consecutive check-in times:

public long getContinueSignCount(int userId, LocalDate date) {
        int signCount = 0;
        String type = String.format("u%d", date.getDayOfMonth());
        List<Long> list = jedis.bitfield(buildSignKey(userId, date), "GET", type, "0");
        if (CollectionUtils.isNotEmpty(list)) {
            // 取低位连续不为0的个数即为连续签到次数,需考虑当天尚未签到的情况
            long value = list.get(0) == null ? 0 : list.get(0);
            int bound = date.getDayOfMonth();
            //连续签到判定算法
            for (int i = 0; i < bound; i++) {
                if (value >> 1 << 1 == value) {
                    // 低位为0且非当天说明连续签到中断了
                    if (i > 0) {
                        break;
                    }
                } else {
                    signCount += 1;
                }
                value >>= 1;
            }
        }
        return signCount;
    }

Sign-in test

public void testDoSign() {
    SignRedisService service = new SignRedisService();
    LocalDate today = LocalDate.now();
    boolean signed = service.doSign(1, today);
    if (signed) {
        System.out.println("您已签到:" + formatDate(today, "yyyy-MM-dd"));
    } else {
        System.out.println("签到完成:" + formatDate(today, "yyyy-MM-dd"));
    }
}

After debugging, the check-in records we obtained are as follows:

签到完成:2020-09-17
签到完成:2020-09-16
签到完成:2020-09-15
签到完成:2020-09-14
签到完成:2020-09-13
签到完成:2020-09-12
签到完成:2020-09-11
签到完成:2020-09-10

Continuous check-in test

@Test
public void getContinueSign() {
    SignRedisService service = new SignRedisService();
    LocalDate today = LocalDate.now();
    long continueSignCount = service.getContinueSignCount(1, today);
    System.out.println("连续签到次数:"+continueSignCount);
}

Test Results

连续签到次数:8

The code List<Long> list = jedis.bitfield(buildSignKey(userId, date), "GET", type, "0");means to get the number of bits before the specified date, we look at debugging, listthe value is 114943.

Convert to binary:

11100000011111111

Indicates that the 17th is the previous bit value, and the 10th to 17th digits are all 1, which means that the sign-in is from September 10th to 17th.

The architect said that using Redis's Bitmap is fast and has better performance in high concurrency situations. Moreover, it occupies a small space, and the Bitmap can store approximately one bit (the length of the bit array is approximately five or six billion).

Although the use of Bitmap looks very high, I still think that the information displayed using MySQL is more comprehensive and easy to query. If the concurrency is high, you can use MySQL + cache.

Guess you like

Origin blog.csdn.net/wujialv/article/details/108723154