Verwenden Sie Redis BitMap, um die Anmeldung zu implementieren, historische Anmelde- und Anmeldestatistikfunktionen abzufragen (SpringBoot-Umgebung).

I. Einleitung

      Das Einchecken ist eine sehr häufige Funktion. Wenn es mithilfe einer Datenbank implementiert wird, ist das Einchecken eines Benutzers ein Datensatz. Wenn es 1 Million Benutzer gibt und die durchschnittliche Anzahl der Eincheckvorgänge pro Benutzer und Jahr 30 beträgt, dann Die Datenmenge in dieser Tabelle pro Jahr beträgt 3.000.000 Felder. Im Allgemeinen gibt es nicht zu viele Check-in-Datensatzfelder. Ein Datenelement wird als 30 Byte berechnet, was etwa 858,3 MB pro Jahr entspricht. Überprüfen Sie jedoch B. die Abfrage, ob der Benutzer an diesem Tag eingecheckt hat, die Abfrage der Check-in-Datensätze des Benutzers in den letzten 7 Tagen, die Abfrage der Check-in-Datensätze des Benutzers in den letzten 30 Tagen und das Zählen der Anzahl Benutzer-Check-Ins. Wenn diese Abfragen an die Check-In-Tabelle gesendet werden müssen, ist der Druck auf die Datenbank sehr groß. Angesichts der Tatsache, dass die Datenmenge weiter zunimmt, wird hier Redis BitMap verwendet, um eine effiziente Anmeldung zu erreichen Statistiken.

2. Redis BitMap-Bitmap-Prinzip

      BitMap ist kein neuer Datentyp in Redis. Die zugrunde liegende Schicht ist die Redis-Implementierung. Die Bitmap (BitMap) von Redis ist ein Array, das aus mehreren Binärbits besteht. Es gibt nur zwei Zustände, 0 und 1. Jede Binärdatei im Array hat entsprechende Bits Offsets (beginnend bei 0). Durch diese Offsets können ein oder mehrere in der Bitmap angegebene Binärbits bearbeitet werden. Da ein Bit zum Speichern eines Datenelements verwendet wird, kann erheblich Platz gespart werden.

Fügen Sie hier eine Bildbeschreibung ein

2.1. Was kann BitMap lösen?

  • BitMap kann viele Probleme lösen. Der Kern besteht darin, Bit-Arrays zu verwenden, um Speicherplatz zu sparen. Zu den allgemeinen Diensten gehören das Einchecken von Benutzern, das Einstempeln, das Zählen aktiver Benutzer, das Zählen des Online-Status des Benutzers, das Implementieren von Bloom-Filtern, die Datendeduplizierung, die Schnellsuche usw.

  • Wie BitMap Bit-Arrays verwendet, um Speicherplatz zu sparen
    Finden Sie heraus, ob eine bestimmte Zahl m unter 2 Milliarden zufälligen Ganzzahlen existiert, und gehen Sie von einem 32-Bit-Betriebssystem und 4G-Speicher aus.
    Die kleinste vom Computer dem Speicher zugewiesene Einheit ist Bit. In Java belegt int 4 Bytes, und 1 Byte = 8 Bits (1 Byte = 8 Bit).
    Wenn jede Zahl in int gespeichert wird, sind das 2 Milliarden ints, also beträgt der belegte Platz etwa (2000000000*4/1024/1024/1024)≈7,45G
    Wenn es in Bits gespeichert wird, ist es anders. 2 Milliarden Zahlen sind 2 Milliarden Bits und der belegte Speicherplatz beträgt etwa (2000000000/8/1024/1024/1024)≈0,23G

2.2. Berechnung des BitMap-Speicherplatzes

  • wird mit dem String-Typ in Redis gespeichert. Die maximale Länge der Zeichenfolge in Redis beträgt 512 MB, daher beträgt der maximale Offset-Wert von BitMap auch: 512 * 1024 * 1024 * 8 = 2^32 Das heißt, eine BitMap kann nur2^32 Bits speichern, was fast 429 Millionen entspricht.
  • Beachten Sie auch ein Problem. Wenn wir ein Datenelement nur mit einem Offset von 99 in einer BitMap speichern, belegt diese BitMap auch 100 Bit Speicher und die Bits 0-98 werden implizit auf 0 initialisiert.

2.3. Probleme mit BitMap

  • Datenkollision. Wenn beispielsweise beim Zuordnen einer Zeichenfolge zu einer BitMap ein Kollisionsproblem auftritt, können Sie die Verwendung von Bloom Filter zur Lösung in Betracht ziehen. Bloom Filter verwendet mehrere Hash-Funktionen, um die Wahrscheinlichkeit einer Kollision zu verringern.

  • Die Datenlage ist spärlich. Als weiteres Beispiel müssen wir zum Speichern der drei Daten (10, 100000, 10000000) eine BitMap mit einer Länge von 9999999 erstellen, tatsächlich werden jedoch nur 3 Daten gespeichert. Zu diesem Zeitpunkt wird viel Speicherplatz verschwendet Wenn Sie darauf stoßen, kann das Problem durch die Einführung von Roaring BitMap gelöst werden.

3. Grundlegende Syntax des Redis-BitMap-Betriebs und Anmeldung bei der nativen Implementierung

3.1. Grundlegende Syntax

# 设置指定偏移量上的位的值(0 或 1),语法:SETBIT key offset value
## 示例:给mykey 偏移量为9的位置设置值为1
SETBIT mykey 9 1

# 获取指定偏移量上的位的值,语法:GETBIT key offset
## 示例:获取mykey 偏移量为9上的值
GETBIT mykey 9

# 统计指定范围内所有位为1的数量 如果不指定范围则统计整个key,这个范围是以字节为单位的比如start设置成1其实代表8bit,对应偏移量是8开始,语法:BITCOUNT key [start end]
## 示例:获取mykey 所有所有位为1的数量
BITCOUNT mykey

# 在指定范围内查找第一个被设置为 1 或 0 的位,语法:BITPOS key bit [start] [end]
## 示例:查找mykey中第一个被设置为 1 的位置
BITPOS mykey 1

# 对位图的指定偏移量进行位级别的读写操作:语法:BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment]
## GET type offset 用于获取指定偏移量上的位,type 可以是 u<n>(无符号整数)或 i<n>(有符号整数),offset 是位图的偏移量。
## SET type offset value 用于设置指定偏移量上的位,type 是位的类型,offset 是位图的偏移量,value 是要设置的值。
## INCRBY type offset increment 用于递增或递减指定偏移量上的位,type 是位的类型,offset 是位图的偏移量,increment 是递增或递减的值。
## 示例:获取mykey 偏移量从 0 开始的4位无符号整数(u4 表示 4 位的无符号整数)
BITFIELD mykey GET u4 0

# 对一个或多个位图执行指定的位运算操作(AND、OR、XOR、NOT),语法:BITOP operation destkey key [key ...]
## 示例:将key1和key1进行AND运算(对应位都为 1 时结果位为 1,否则为 0),将运算后的结果保存到新的key:destkey 
BITOP AND destkey key1 key2

3.2. Redis BitMap implementiert Anweisungen für den Anmeldevorgang

      ​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​: Die drei Tage vom 13. bis zum 5. November 2023 werden als Datum für den Check-in ausgewählt. Da der Offset bei 0 beginnt, sind die entsprechenden Offsets 0, 2 und 4 und die anderen Daten nicht angemeldet.

  • 1. Benutzeranmeldestelle hinzufügenkey = USER_SIGN_IN:U0001:202311, wobei U0001 die Benutzernummer und 202311 die entsprechende darstellt Jahr und Monat< /span>

    127.0.0.1:6379> SETBIT USER_SIGN_IN:U0001:202311 0 1
    (integer) 0
    127.0.0.1:6379> SETBIT USER_SIGN_IN:U0001:202311 2 1
    (integer) 0
    127.0.0.1:6379> SETBIT USER_SIGN_IN:U0001:202311 4 1
    (integer) 0
    
  • 2. Prüfen Sie, ob an dem vom Benutzer angegebenen Datum ein Check-in stattfindet (dasselbe gilt für die Prüfung, ob an dem Tag ein Check-in stattfindet). Hier wird geprüft, ob am 5. ein Check-in stattfindet. Der Offset beträgt 4. Wenn 1 zurückgegeben wird, bedeutet dies, dass ein Check-in erfolgt.

    127.0.0.1:6379> GETBIT USER_SIGN_IN:U0001:202311 4
    (integer) 1
    
  • 3. Überprüfen Sie, wie viele Tage der Benutzer im November 2023 eingecheckt hat.

    127.0.0.1:6379> BITCOUNT USER_SIGN_IN:U0001:202311
    (integer) 3
    
  • 4. Überprüfen Sie die Daten, an denen sich der Benutzer im November 2023 angemeldet hat. Der November hat 30 Tage.

    • Erhalten Sie über BITFIELD eine 30-Bit-Dezimalzahl ohne Vorzeichen, beginnend bei Offset 0
    127.0.0.1:6379> BITFIELD USER_SIGN_IN:U0001:202311 GET u30 0
    1) (integer) 704643072
    
    • Konvertieren Sie die erhaltene vorzeichenlose Dezimalzahl in eine Binärzahl. Hier können Sie sehen, dass die 1 3 5 Positionswerte der Binärzahl von links nach rechts alle 1 sind, was dem Offset 0 2 4 entspricht. Dies ist nicht von rechts nach links , und dann übergeben Die Geschäftscodebeurteilung bestimmt, dass das binäre entsprechende Bit 1 ist, was bedeutet, dass eine Anmeldung vorliegt. Der spezifische Code wird unten implementiert.
    # 十进制
    704643072
    # 二进制
    101010000000000000000000000000
    

4. SpringBoot verwendet Redis BitMap, um Anmelde- und Statistikfunktionen zu implementieren

      Hier verwenden wir die SpringBoot-Umgebung RedisTemplate, um Redis zu betreiben. Wenn Sie den Artikel integrieren müssen, können Sie ihn anzeigen. SpringBoot integriert den Lettuce-Client, um Redis zu betreiben:https:/ /blog.csdn.net/weixin_44606481/ article/details/133907103

Ich werde hier auch das Hutool-Toolkit zur Betriebszeitanalyse verwenden, das bei Bedarf eingeführt werden kann.

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.17</version>
</dependency>

4.1. Code-Implementierung

Die Kommentare im Code sind relativ vollständig, daher werde ich hier keine zusätzliche Einführung geben.

import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.YearMonth;
import java.util.*;

/**
 * 签到业务
 */
@Service
public class SignInService {
    
    

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    private static String yyyy_MM_dd = "yyyy-MM-dd";
    private static String yyyy_MM = "yyyy-MM";

    /**
     * 用户签到
     * @param userNo 用户编号
     * @param date   日期 格式yyyy-MM-dd
     */
    public boolean signIn(String userNo, String date) {
    
    
        // 获取缓存key
        String cacheKey = getCacheKey(userNo, date);
        // 获取日期
        DateTime dateTime = DateUtil.parse(date, yyyy_MM_dd);
        int day = dateTime.dayOfMonth();
        // 设置给BitMap对应位标记 其中offset为0表示第一天所以要day-1
        Boolean result = redisTemplate.opsForValue().setBit(cacheKey, day - 1, true);
        // 如果响应true则代表之前已经签到,在Redis指令操作setbit 设置对应位为1的时候,如果之前是0或者不存在会响应0,如果为1则响应1
        if (result) {
    
    
            System.out.println("用户userNo=" + userNo + " date=" + date + "  已签到");
        }
        return result;
    }

    /**
     * 查看用户指定日期是否签到(查看当天是否有签到同理)
     * @param userNo
     * @param date   日期 格式yyyy-MM-dd
     */
    public boolean isSignIn(String userNo, String date) {
    
    
        // 获取缓存key
        String cacheKey = getCacheKey(userNo, date);
        // 获取日期
        DateTime dateTime = DateUtil.parse(date, yyyy_MM_dd);
        int day = dateTime.dayOfMonth();
        return redisTemplate.opsForValue().getBit(cacheKey, day - 1);
    }

    /**
     * 统计用户指定年月签到次数
     * @param userNo
     * @param date   格式yyyy-MM
     */
    public Long getSignInCount(String userNo, String date) {
    
    
        // 获取缓存key
        String cacheKey = getCacheKey(userNo, date);
        // 不知道是那个版本才有的下面这个方法,我的现在使用的spring-data-redis是2.3.9.RELEASE 是没有这个方法的,改用connection直接调用bitCount
//        Long count = redisTemplate.opsForValue().bitCount(key, start, end);
        Long count = redisTemplate.execute(connection -> connection.bitCount(cacheKey.getBytes()), true);
        return count;
    }

    /**
     * 获取用户指定年月签到列表,也可以通过这种方式获取用户月签到次数
     *
     * @param userNo
     * @param date   格式yyyy-MM
     */
    public List<Map> getSignInList(String userNo, String date) {
    
    
        // 获取缓存key
        String cacheKey = getCacheKey(userNo, date);
        // 获取传入月份有多少天
        DateTime dateTime = DateUtil.parse(date, yyyy_MM);
        YearMonth yearMonth = YearMonth.of(dateTime.year(), dateTime.monthBaseOne());
        int days = yearMonth.lengthOfMonth();
        BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands.create()
                .get(BitFieldSubCommands.BitFieldType.unsigned(days)).valueAt(0);
        // 获取位图的无符号十进制整数
        List<Long> list = redisTemplate.opsForValue().bitField(cacheKey, bitFieldSubCommands);
        if (list == null || list.isEmpty()) {
    
    
            return null;
        }
        // 获取位图的无符号十进制整数值
        long bitMapNum = list.get(0);
        // 进行位运算判断组装那些日期有签到
        List<Map> result = new ArrayList<>();
        for (int i = days; i > 0; i--) {
    
    
            Map<String, Object> map = new HashMap<>();
            map.put("day", i);
            //先 右移,然后在 左移,如果得到的结果仍然与本身相等,则 最低位是0 所以是未签到
            if (bitMapNum >> 1 << 1 == bitMapNum) {
    
    
                map.put("active", false);
            } else {
    
    
                //与本身不等,则最低位是1 表示已签到
                map.put("active", true);
            }
            result.add(map);
            // 将位图的无符号十进制整数右移一位,准备下一轮判断
            bitMapNum >>= 1;
        }
        Collections.reverse(result);
        return result;
    }


    /**
     * 获取缓存key
     */
    private static String getCacheKey(String userNo, String date) {
    
    
        DateTime dateTime = DateUtil.parse(date, yyyy_MM);
        return String.format("USER_SIGN_IN:%s:%s", userNo, dateTime.year() + "" + dateTime.monthBaseOne());
    }
}

4.2. Funktionstest

import com.redisscene.service.SignInService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.List;
import java.util.Map;

/**
 * 签到功能测试
 */
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class SignInTest {
    
    

    @Autowired
    private SignInService signInService;

    /**
     * 测试用户签到
     */
    @Test
    public void t1() {
    
    
        boolean b1 = signInService.signIn("U0001", "2023-11-01");
        boolean b2 = signInService.signIn("U0001", "2023-11-03");
        boolean b3 = signInService.signIn("U0001", "2023-11-05");
        boolean b4 = signInService.signIn("U0001", "2023-11-01");
        System.out.println("b1=" + b1 + " b2=" + b2 + " b3=" + b3 + " b4=" + b4);
    }

    /**
     * 测试查看用户指定日期是否签到(查看当天是否有签到同理)
     */
    @Test
    public void t2() {
    
    
        boolean b1 = signInService.isSignIn("U0001", "2023-11-01");
        System.out.println(b1 ? "b1已签到" : "b1未签到");
        boolean b2 = signInService.isSignIn("U0001", "2023-11-06");
        System.out.println(b2 ? "b2已签到" : "b2未签到");
    }

    /**
     * 测试统计用户指定年月签到次数
     */
    @Test
    public void t3() {
    
    
        Long count = signInService.getSignInCount("U0001", "2023-11");
        System.out.println("签到次数count=" + count);
    }

    /**
     * 测试获取用户指定年月签到列表,也可以通过这种方式获取用户月签到次数
     */
    @Test
    public void t4() {
    
    
        List<Map> list = signInService.getSignInList("U0001", "2023-11");
        if (list != null && !list.isEmpty()) {
    
    
            list.forEach(item -> System.out.println(item));
        }
    }
}

おすすめ

転載: blog.csdn.net/weixin_44606481/article/details/134446032