Redis only uses caching? 20 ways to make colleagues call you awesome



Hello everyone, I am Yihang

Back-end programmers, whether they are going out for interviews or working as interviewers, Redis is almost 100% the technical point they will ask about; the reason is mainly because it is too powerful and the usage rate is too high; resulting in almost no projects. Everywhere.

As for the Redis part, as expected, the first question is: What did you use Redis for the project you worked on ? Most people's answer will be: caching ; when asked whether it is used in other scenarios, some friends who use it less will shake their heads slightly.

In fact, it is true that most of the usage scenarios of Redis are for caching; however, because Redis supports relatively rich data structures, the functions it can implement are not limited to caching, but can be applied to various business scenarios. Develop a system that is both simple and efficient;

The following is a list of 20 wonderful usage scenarios of Redis. Each solution is explained with an actual business requirement and an API combined with the data structure. I hope everyone can understand its underlying implementation, learn to draw inferences from one example, and apply it to all aspects of the project:

This article is a bit long. If you don’t have enough time, I suggest you read the table of contents, save it, and look it up when you need it.

Test source code: https://github.com/vehang/ehang-spring-boot/tree/main/spring-boot-011-redis

cache

This article assumes that you have already understood Redis and know some of the most basic uses of Redis. If you do not know the basic API of Redis, you can first read the novice tutorial: https://www.runoob.com/redis, then the caching part And the demonstration of basic API will not be explained in detail;

However, the basic data structure is listed here again to facilitate the understanding of subsequent solutions:

structure type The value stored in the structure structural literacy
String string Can be a string, integer or float Operate on the entire string or a part of the string; perform increment or decrement operations on integers or floating point numbers;
ListList A linked list, each node on the linked list contains a string Perform push and pop operations on both ends of the linked list to read single or multiple elements; find or delete elements based on value;
Set collection An unordered collection containing strings A collection of strings, including basic methods: see if it exists, add, get, delete; also include calculation of intersection, union, difference, etc.
HashHash An unordered hash table containing key-value pairs Containing methods include: adding, getting, and deleting a single element
Zset ordered collection Like hashing, used to store key-value pairs An ordered mapping between string members and floating-point scores; the order of elements is determined by the size of the score; the inclusion methods include: adding, getting, deleting individual elements, and getting elements based on score ranges or members
  • rely

    All the following use cases tested by SpringBoot need to introduce Redis dependencies

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    

lottery

Once upon a time, lottery was a popular way for Internet apps to promote and attract new users. If you don’t have a good plan during the holidays, just draw a prize! A bunch of users participate, and then a few lucky users are randomly selected and given physical/virtual prizes; at this time, developers need to write a lottery algorithm to achieve the extraction of lucky users; in fact, we can completely use the Redis collection ( Set), you can easily realize the lottery function;

API required for function implementation

  • SADD key member1 [member2]: Add one or more participating users;
  • SRANDMEMBER KEY [count]: Randomly returns one or more users;
  • SPOP key: randomly returns one or more users and deletes the returned users;

SRANDMEMBER and SPOP are mainly used for two different lottery modes. SRANDMEMBER is suitable for scenarios where a user can win multiple times (that is, after winning, he will not be removed from the user pool and continue to participate in the drawing of other prizes); while SPOP is suitable for Scenarios where you can only win once (once you win, the user will be removed from the user pool, and it will be impossible to draw that user again in subsequent draws); SPOP is usually used more often.

Redis-cli operations

127.0.0.1:6379> SADD raffle user1
(integer) 1
127.0.0.1:6379> SADD raffle user2 user3 user4 user5 user6 user7 user8 user9 user10
(integer) 9
127.0.0.1:6379> SRANDMEMBER raffle 2
1) "user5"
2) "user2"
127.0.0.1:6379> SPOP raffle 2
1) "user3"
2) "user4"
127.0.0.1:6379> SPOP raffle 2
1) "user10"
2) "user9"

SpringBoot implementation

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.List;

/**
 * @author 一行Java
 * @title: RaffleMain
 * @projectName ehang-spring-boot
 * @description: TODO
 * @date 2022/7/18 15:17
 */
@Slf4j
@SpringBootTest
public class RaffleMain {
    
    
    private final String KEY_RAFFLE_PROFIX = "raffle:";
    @Autowired
    RedisTemplate redisTemplate;

    @Test
    void test() {
    
    
        Integer raffleId = 1;
        join(raffleId, 1000, 1001, 2233, 7890, 44556, 74512);
        List lucky = lucky(raffleId, 2);
        log.info("活动:{} 的幸运中奖用户是:{}", raffleId, lucky);
    }

    public void join(Integer raffleId, Integer... userIds) {
    
    
        String key = KEY_RAFFLE_PROFIX + raffleId;
        redisTemplate.opsForSet().add(key, userIds);
    }

    public List lucky(Integer raffleId, long num) {
    
    
        String key = KEY_RAFFLE_PROFIX + raffleId;
        // 随机抽取 抽完之后将用户移除奖池
        List list = redisTemplate.opsForSet().pop(key, num);
        // 随机抽取 抽完之后用户保留在池子里
        //List list = redisTemplate.opsForSet().randomMembers(key, num);
        return list;
    }

}

Set implements the like/collection function

Apps with interactive attributes generally have functions such as likes/collections/likes to enhance interaction between users.

Traditional implementation : After the user likes, a piece of data is recorded in the database, and a summary number of likes/collections is generally recorded in the theme database for easy display;

Redis solution : Redis-based collection (Set) records the user data of collections and likes corresponding to each post/article. At the same time, the set also provides the ability to check whether a specified user exists in the set, so that users can quickly determine whether the user has liked it.

API required for function implementation

  • SADD key member1 [member2]: Add one or more members (like)
  • SCARD key: Get the number of all members (number of likes)
  • SISMEMBER key member: Determine whether the member exists (whether to like it)
  • SREM key member1 [member2]: Remove one or more members (number of likes)

Redis-cli API operations

127.0.0.1:6379> sadd like:article:1 user1
(integer) 1
127.0.0.1:6379> sadd like:article:1 user2
(integer) 1
# 获取成员数量(点赞数量)
127.0.0.1:6379> SCARD like:article:1
(integer) 2
# 判断成员是否存在(是否点在)
127.0.0.1:6379> SISMEMBER like:article:1 user1
(integer) 1
127.0.0.1:6379> SISMEMBER like:article:1 user3
(integer) 0
# 移除一个或者多个成员(取消点赞)
127.0.0.1:6379> SREM like:article:1 user1
(integer) 1
127.0.0.1:6379> SCARD like:article:1
(integer) 1

SpringBoot operations

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;

/**
 * @author 一行Java
 * @title: LikeMain
 * @projectName ehang-spring-boot
 * @description: TODO
 * @date 2022/7/18 15:38
 */
@Slf4j
@SpringBootTest
public class LikeMain {
    
    
    private final String KEY_LIKE_ARTICLE_PROFIX = "like:article:";

    @Autowired
    RedisTemplate redisTemplate;

    @Test
    void test() {
    
    
        long articleId = 100;
        Long likeNum = like(articleId, 1001, 1002, 2001, 3005, 4003);
        unLike(articleId, 2001);
        likeNum = likeNum(articleId);
        boolean b2001 = isLike(articleId, 2001);
        boolean b3005 = isLike(articleId, 3005);
        log.info("文章:{} 点赞数量:{} 用户2001的点赞状态:{} 用户3005的点赞状态:{}", articleId, likeNum, b2001, b3005);
    }

    /**
     * 点赞
     *
     * @param articleId 文章ID
     * @return 点赞数量
     */
    public Long like(Long articleId, Integer... userIds) {
    
    
        String key = KEY_LIKE_ARTICLE_PROFIX + articleId;
        Long add = redisTemplate.opsForSet().add(key, userIds);
        return add;
    }

    public Long unLike(Long articleId, Integer... userIds) {
    
    
        String key = KEY_LIKE_ARTICLE_PROFIX + articleId;
        Long remove = redisTemplate.opsForSet().remove(key, userIds);
        return remove;
    }

    public Long likeNum(Long articleId) {
    
    
        String key = KEY_LIKE_ARTICLE_PROFIX + articleId;
        Long size = redisTemplate.opsForSet().size(key);
        return size;
    }

    public Boolean isLike(Long articleId, Integer userId) {
    
    
        String key = KEY_LIKE_ARTICLE_PROFIX + articleId;
        return redisTemplate.opsForSet().isMember(key, userId);
    }

}

Ranking list

Rankings, rankings, and hot search lists are functions that many apps and games have, and are often used for user activity promotion, competitive rankings, popular information display, and other functions;

For example, in the hot search list above, the popularity data comes from the contributions of users across the entire network, but users only care about the top 50 most popular items.

The conventional approach is to update the user's ranking, score and other ranking data to the database, and then use Order by + limit to retrieve the top 50 for display. If there are not many participating users and the data is not updated frequently, use There is nothing wrong with the database method, but once explosive hot information appears (for example: mainland China regains the Bay, some xxx goes green, etc.), there will be explosive traffic in a short period of time, and the instantaneous pressure may make the database unable to bear it;

Redis solution : Cache the full page of hot information and use Redis's ordered queue (Sorted Set) to cache the popularity (SCORES), which can instantly relieve the pressure on the database and easily filter out the 50 most popular items;

Commands required for function implementation

  • ZADD key score1 member1 [score2 member2]: Add and set SCORES, support adding multiple at one time;
  • ZREVRANGE key start stop [WITHSCORES]: Sort in descending order according to SCORES;
  • ZRANGE key start stop [WITHSCORES]: Sort in descending order according to SCORES;

Redis-cli operation

# 单个插入
127.0.0.1:6379> ZADD ranking 1 user1  
(integer) 1
# 批量插入
127.0.0.1:6379> ZADD ranking 10 user2 50 user3 3 user4 25 user5
(integer) 4
# 降序排列 不带SCORES
127.0.0.1:6379> ZREVRANGE ranking 0 -1 
1) "user3"
2) "user5"
3) "user2"
4) "user4"
5) "user1"
# 降序排列 带SCORES
127.0.0.1:6379> ZREVRANGE ranking 0 -1 WITHSCORES
 1) "user3"
 2) "50"
 3) "user5"
 4) "25"
 5) "user2"
 6) "10"
 7) "user4"
 8) "3"
 9) "user1"
10) "1"
# 升序
127.0.0.1:6379> ZRANGE ranking 0 -1 WITHSCORES
 1) "user1"
 2) "1"
 3) "user4"
 4) "3"
 5) "user2"
 6) "10"
 7) "user5"
 8) "25"
 9) "user3"
10) "50"

SpringBoot operation

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.DefaultTypedTuple;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.Set;

/**
 * @author 一行Java
 * @title: RankingTest
 * @projectName ehang-spring-boot
 * @description: TODO
 * @date 2022/7/18 15:54
 */
@SpringBootTest
@Slf4j
public class RankingTest {
    
    
    private final String KEY_RANKING = "ranking";
    @Autowired
    RedisTemplate redisTemplate;

    @Test
    void test() {
    
    
        add(1001, (double) 60);
        add(1002, (double) 80);
        add(1003, (double) 100);
        add(1004, (double) 90);
        add(1005, (double) 70);

        // 取所有
        Set<DefaultTypedTuple> range = range(0, -1);
        log.info("所有用户排序:{}", range);

        // 前三名
        range = range(0, 2);
        log.info("前三名排序:{}", range);
    }

    public Boolean add(Integer userId, Double score) {
    
    
        Boolean add = redisTemplate.opsForZSet().add(KEY_RANKING, userId, score);
        return add;
    }

    public Set<DefaultTypedTuple> range(long min, long max) {
    
    
        // 降序
        Set<DefaultTypedTuple> set = redisTemplate.opsForZSet().reverseRangeWithScores(KEY_RANKING, min, max);
        // 升序
        //Set<DefaultTypedTuple> set = redisTemplate.opsForZSet().rangeWithScores(KEY_RANKING, min, max);
        return set;
    }
}

output

所有用户排序:[DefaultTypedTuple [score=100.0, value=1003], DefaultTypedTuple [score=90.0, value=1004], DefaultTypedTuple [score=80.0, value=1002], DefaultTypedTuple [score=70.0, value=1005], DefaultTypedTuple [score=60.0, value=1001]]
前三名排序:[DefaultTypedTuple [score=100.0, value=1003], DefaultTypedTuple [score=90.0, value=1004], DefaultTypedTuple [score=80.0, value=1002]]

PV statistics (incr auto-increment count)

Page View (PV) refers to page views, which is an important standard for measuring traffic and an important basis for data analysis; usually the statistical rule is that if the page is displayed once, add one

Function required commands

  • INCR : Increase the numerical value stored in key by one

Redis-cli operations

127.0.0.1:6379> INCR pv:article:1
(integer) 1
127.0.0.1:6379> INCR pv:article:1
(integer) 2

UV statistics (HeyperLogLog)

Earlier, we introduced the (INCR) method to achieve page PV; in addition to PV, UV (unique visitors) is also a very important statistic;

But if you want to implement UV counting by counting (INCR), it is very troublesome. Before adding, you need to determine whether the user has visited; then the basis for judgment requires additional methods to record.

You might say, isn’t there also Set? Create a collection on one page, add one user (SADD) to it, and when you want to count UVs, just summarize the numbers through SCARD, and you can easily do it; this solution can indeed achieve UV statistical effects, but it ignores the cost. ; If it is an ordinary page, hundreds or thousands of visits may have minimal impact. If once a popular page is visited by tens of millions or hundreds of millions of users, just one page UV will bring a very large impact. Memory overhead, for such precious memory, this is obviously not cost-effective.

At this time, the HeyperLogLog data structure can perfectly solve this problem. It provides an inaccurate deduplication and counting solution. Pay attention! It should be emphasized here that it is not accurate and there will be errors, but the errors will not be very large. The standard error rate is 0.81% . This error rate is tolerable for statistical UV counting; therefore, do not use this data structure Do accurate deduplication counting.

In addition, HeyperLogLog will occupy 12KB of storage space . Although Redis has optimized HeyperLogLog and uses sparse matrix storage when the data is relatively small. Only when the amount of data becomes large and the sparse matrix space occupied exceeds the threshold, it will It will be converted into a dense matrix with a space of 12KB; compared with thousands or hundreds of millions of data, this small 12KB is simply too cost-effective; but it is still recommended not to use it for small amounts of data and frequent creation HeyperLogLog scenario, avoid improper use, causing the adverse effect of increasing resource consumption instead of decreasing.

Commands required for functionality:

  • PFADD key element [element…]: Increase count (statistical UV)
  • PFCOUNT key [key…]: Get count (cargo UV)
  • PFMERGE destkey sourcekey [sourcekey…]: Merge multiple HyperLogLogs into one HyperLogLog (multiple combined statistics)

Redis-cli operations

# 添加三个用户的访问
127.0.0.1:6379> PFADD uv:page:1 user1 user2 user3
(integer) 1
# 获取UV数量
127.0.0.1:6379> PFCOUNT uv:page:1
(integer) 3
# 再添加三个用户的访问  user3是重复用户
127.0.0.1:6379> PFADD uv:page:1 user3 user4 user5
(integer) 1
# 获取UV数量 user3是重复用户 所以这里返回的是5
127.0.0.1:6379> PFCOUNT uv:page:1
(integer) 5

SpringBoot operation HeyperLogLog

Simulate and test 10,000 users accessing the page with ID 2

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;

/**
 * @author 一行Java
 * @title: HeyperLogLog 统计UV
 * @projectName ehang-spring-boot
 * @description: TODO
 * @date 2022/7/19 16:13
 */
@SpringBootTest
@Slf4j
public class UVTest {
    
    
    private final String KEY_UV_PAGE_PROFIX = "uv:page:";

    @Autowired
    RedisTemplate redisTemplate;

    @Test
    public void uvTest() {
    
    
        Integer pageId = 2;
        for (int i = 0; i < 10000; i++) {
    
    
            uv(pageId, i);
        }
        for (int i = 0; i < 10000; i++) {
    
    
            uv(pageId, i);
        }

        Long uv = getUv(pageId);
        log.info("pageId:{} uv:{}", pageId, uv);
    }

    /**
     * 用户访问页面
     * @param pageId
     * @param userId
     * @return
     */
    private Long uv(Integer pageId, Integer userId) {
    
    
        String key = KEY_UV_PAGE_PROFIX + pageId;
        return redisTemplate.opsForHyperLogLog().add(key, userId);
    }

    /**
     * 统计页面的UV
     * @param pageId
     * @return
     */
    private Long getUv(Integer pageId) {
    
    
        String key = KEY_UV_PAGE_PROFIX + pageId;
        return redisTemplate.opsForHyperLogLog().size(key);
    }
}

Log output

pageId:2 uv:10023

Due to errors, the actual number of visits here is 10,000, and the statistics are 23 more. Within the standard error (0.81%) range, plus the UV data does not need to be accurate, so this error is acceptable .

Deduplication (BloomFiler)

Through the above study of HeyperLogLog, we have mastered an inaccurate deduplication and counting scheme, but have you found that it cannot obtain whether a certain user has visited it? Ideally, we hope to have a PFEXISTScommand to determine whether a certain user has visited Whether the key exists, but HeyperLogLog does not; to realize this requirement, BloomFiler must come into play.

  • What is Bloom Filter?

    Bloom Filter is a fast search algorithm for multi-hash function mapping proposed by Bloom in 1970.
    It is usually used in situations where it is necessary to quickly determine whether an element belongs to a set, but it is not strictly required to be 100% correct .
    Implemented based on a probabilistic data structure, it is an interesting and powerful algorithm.

For example : If you write a crawler to crawl all pages on the network, when you get a new page, how do you determine whether this page has been crawled?

Common practice : Every time you crawl a page, insert a row of data into the database and record the URL. Every time you get a new page, go to the database and query it. If it exists, it means it has been crawled;

Disadvantages of the common approach : For a small amount of data, there is no problem using traditional solutions. If it is massive data, the retrieval before each crawl will become slower and slower; if your crawler only cares about the content and does not care much about the source data. , the storage of this part of data will also consume a lot of your physical resources;

At this time, BloomFiler can be used to easily determine whether a certain value exists at the cost of a small memory space.

Similarly, BloomFiler is not so accurate. Under the default parameters, there is an error of about 1%; but BloomFiler allows you to set its error ratio through error_rate (error rate) and initial_size (estimated size)

  • error_rate : error rate, the lower it is, the more space is required;
  • initial_size : The expected number of values ​​to put in. When the actual number to put in is greater than the set value, the error rate will gradually increase; so in order to avoid the error rate, you can make an estimate in advance to avoid another large error;

BloomFiler installation

In order to facilitate testing, Docker is used here for quick installation.

docker run -d -p 6379:6379 redislabs/rebloom

Commands required for functionality

  • bf.add adds a single element
  • bf.madd batch field
  • bf.exists detects whether an element exists
  • bf.mexists batch detection

Redis-cli operation

127.0.0.1:6379> bf.add web:crawler baidu
(integer) 1
127.0.0.1:6379> bf.madd web:crawler tencent bing
1) (integer) 1
2) (integer) 1
127.0.0.1:6379> bf.exists web:crawler baidu
(integer) 1
127.0.0.1:6379> bf.mexists web:crawler baidu 163
1) (integer) 1
2) (integer) 0

SpringBoot integration

  • Tool class RedisBloom

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.data.redis.core.script.DefaultRedisScript;
    import org.springframework.data.redis.core.script.RedisScript;
    import org.springframework.stereotype.Component;
    
    import java.util.Arrays;
    import java.util.List;
    
    /**
     * redis布隆过滤器
     *
     * @author 一行Java
     * @title: RedisBloom
     * @projectName ehang-spring-boot
     * @description: TODO
     * @date 2022/7/19 17:03
     */
    @Component
    public class RedisBloom {
          
          
    
        private static RedisScript<Boolean> bfreserveScript = new DefaultRedisScript<>("return redis.call('bf.reserve', KEYS[1], ARGV[1], ARGV[2])", Boolean.class);
        private static RedisScript<Boolean> bfaddScript = new DefaultRedisScript<>("return redis.call('bf.add', KEYS[1], ARGV[1])", Boolean.class);
        private static RedisScript<Boolean> bfexistsScript = new DefaultRedisScript<>("return redis.call('bf.exists', KEYS[1], ARGV[1])", Boolean.class);
        private static String bfmaddScript = "return redis.call('bf.madd', KEYS[1], %s)";
        private static String bfmexistsScript = "return redis.call('bf.mexists', KEYS[1], %s)";
        @Autowired
        private StringRedisTemplate redisTemplate;
    
        /**
         * 设置错误率和大小(需要在添加元素前调用,若已存在元素,则会报错)
         * 错误率越低,需要的空间越大
         *
         * @param key
         * @param errorRate   错误率,默认0.01
         * @param initialSize 默认100,预计放入的元素数量,当实际数量超出这个数值时,误判率会上升,尽量估计一个准确数值再加上一定的冗余空间
         * @return
         */
        public Boolean bfreserve(String key, double errorRate, int initialSize) {
          
          
            return redisTemplate.execute(bfreserveScript, Arrays.asList(key), String.valueOf(errorRate), String.valueOf(initialSize));
        }
    
        /**
         * 添加元素
         *
         * @param key
         * @param value
         * @return true表示添加成功,false表示添加失败(存在时会返回false)
         */
        public Boolean bfadd(String key, String value) {
          
          
            return redisTemplate.execute(bfaddScript, Arrays.asList(key), value);
        }
    
        /**
         * 查看元素是否存在(判断为存在时有可能是误判,不存在是一定不存在)
         *
         * @param key
         * @param value
         * @return true表示存在,false表示不存在
         */
        public Boolean bfexists(String key, String value) {
          
          
            return redisTemplate.execute(bfexistsScript, Arrays.asList(key), value);
        }
    
        /**
         * 批量添加元素
         *
         * @param key
         * @param values
         * @return 按序 1表示添加成功,0表示添加失败
         */
        public List<Integer> bfmadd(String key, String... values) {
          
          
            return (List<Integer>) redisTemplate.execute(this.generateScript(bfmaddScript, values), Arrays.asList(key), values);
        }
    
        /**
         * 批量检查元素是否存在(判断为存在时有可能是误判,不存在是一定不存在)
         *
         * @param key
         * @param values
         * @return 按序 1表示存在,0表示不存在
         */
        public List<Integer> bfmexists(String key, String... values) {
          
          
            return (List<Integer>) redisTemplate.execute(this.generateScript(bfmexistsScript, values), Arrays.asList(key), values);
        }
    
        private RedisScript<List> generateScript(String script, String[] values) {
          
          
            StringBuilder sb = new StringBuilder();
            for (int i = 1; i <= values.length; i++) {
          
          
                if (i != 1) {
          
          
                    sb.append(",");
                }
                sb.append("ARGV[").append(i).append("]");
            }
            return new DefaultRedisScript<>(String.format(script, sb.toString()), List.class);
        }
    
    }
    
  • test

    import lombok.extern.slf4j.Slf4j;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.data.redis.core.RedisTemplate;
    
    import java.util.List;
    
    /**
     * @author 一行Java
     * @title: BFTest
     * @projectName ehang-spring-boot
     * @description: TODO
     * @date 2022/7/19 17:04
     */
    @SpringBootTest
    @Slf4j
    public class BFTest {
          
          
    
        private final String KEY_WEB_CRAWLER = "web:crawler1";
    
        @Autowired
        RedisBloom bloom;
    
        @Autowired
        RedisTemplate redisTemplate;
    
    
        @Test
        public void test() {
          
          
            Boolean hasKey = redisTemplate.hasKey(KEY_WEB_CRAWLER);
            log.info("bloom hasKey:{}", hasKey);
            if (!hasKey) {
          
          
                // 不存在的时候  再去初始化
                Boolean bfreserve = bloom.bfreserve(KEY_WEB_CRAWLER, 0.0001, 10000);
                log.info("bloom bfreserve:{}", bfreserve);
            }
            List<Integer> madd = bloom.bfmadd(KEY_WEB_CRAWLER, "baidu", "google");
            log.info("bloom bfmadd:{}", madd);
    
            Boolean baidu = bloom.bfexists(KEY_WEB_CRAWLER, "baidu");
            log.info("bloom bfexists baidu:{}", baidu);
    
            Boolean bing = bloom.bfexists(KEY_WEB_CRAWLER, "bing");
            log.info("bloom bfexists bing:{}", bing);
        }
    }
    

    Log output

    com.ehang.redis.bloom_filter.BFTest      : bloom hasKey:false
    com.ehang.redis.bloom_filter.BFTest      : bloom bfreserve:true
    com.ehang.redis.bloom_filter.BFTest      : bloom bfmadd:[1, 1]
    com.ehang.redis.bloom_filter.BFTest      : bloom bfexists baidu:true
    com.ehang.redis.bloom_filter.BFTest      : bloom bfexists bing:false
    

User sign-in (BitMap)

In order to stimulate user activity, many apps often do some activities, such as signing in continuously to receive points/gift packages, etc.

Traditional approach : Every time a user checks in, a piece of check-in data is inserted into the database. When displaying, the check-in data for this month (or a specified period) is obtained to determine whether the user has signed in and the continuous sign-in status; in this way, Simple and easy to understand;

Redis approach : Since there are only two concerns about check-in data: whether to check in (0/1) and continuity, it can be implemented using BitMap (bitmap);

A month’s check-in status can be recorded in 4 bytes (Source: Internet)

As shown in the figure above, 31 days of a month are represented by 31 bits (4 bytes). The offset (offset) represents the current day, 0/1 represents whether the current sign-in is, and continuous sign-in only requires Check the number of consecutive 1 digits from right to left;

Since the maximum limit of the String type is 512M, the conversion to bits is 2^32 bits.

Required commands:

  • SETBIT key offset value: Store a 0 or 1 to the specified position offset

  • GETBIT key offset: Get the bit value of the specified position offset

  • BITCOUNT key [start] [end]: Count the number of bits with a value of 1 in the BitMap

  • BITFIELD : operate (query, modify, increment) the value of the offset at the specified position in the bit array in BitMap

    The most difficult thing to understand here is: BITFIELD. For details, please refer to: https://deepinout.com/redis-cmd/redis-bitmap-cmd/redis-cmd-bitfield.html And this part must be understood, otherwise, the The core part of the requirement cannot be understood;

Requirement: If the current date is August 4th, check the check-in status of this month. The user has checked in on the 1st, 3rd, and 4th.

Redis-cli operation:

# 81号的签到
127.0.0.1:6379> SETBIT RangeId:Sign:1:8899 0 1
(integer) 1

# 83号的签到
127.0.0.1:6379> SETBIT RangeId:Sign:1:8899 2 1
(integer) 1

# 84号的签到
127.0.0.1:6379> SETBIT RangeId:Sign:1:8899 3 1
(integer) 1

# 查询各天的签到情况
# 查询1127.0.0.1:6379> GETBIT RangeId:Sign:1:8899 0
(integer) 1
# 查询2127.0.0.1:6379> GETBIT RangeId:Sign:1:8899 1
(integer) 0
# 查询3127.0.0.1:6379> GETBIT RangeId:Sign:1:8899 2
(integer) 1
# 查询4127.0.0.1:6379> GETBIT RangeId:Sign:1:8899 3
(integer) 1

# 查询指定区间的签到情况
127.0.0.1:6379> BITFIELD RangeId:Sign:1:8899 get u4 0
1) (integer) 11

The check-in status of Nos. 1-4 is: 1011 (binary) ==> 11 (decimal)

Determination of whether to sign in and continuous sign-in

In the sign-in function, the most difficult thing to understand is the judgment of whether to sign in or continuous sign-in. In the SpringBoot code below, it is judged by this: It is a little signFlag >> 1 << 1 != signFlagdifficult to understand, so I will tell you about it in advance;

The above tests the sign-in situation of No. 1-4, and obtains BITFIELDsignFlag = 11 (decimal) = 1011 (binary);

The basis for judging continuous sign-in is: count the number of consecutive BITs that are 1 from right to left . Binary 1011 means that the number of consecutive sign-in days is 2 days. The calculation process of 2 days is as follows:

  • The first step is to get signFlag

  • The second step is the number of days to loop. The above test case is the check-in situation for 4 days, and the for loop is 4 times.

  • The third step is to cycle from right to left to determine

    Continuous check-in : when encountering the first false, terminate and get the number of consecutive days

    Sign-in details : loop through all days, true means you have signed in currently, false means you have not signed in that day;

    first cycle

    signFlag = 1011
    signFlag >> 1   结果: 101
    signFlag << 1   结果:1010
    1010 != signFlag(1011) 结果:true  //4号已签到,说明连续签到1天
    signFlag >>= 1  结果: 101   // 此时signFlag = 101
    

    second loop

    signFlag = 101  // 前一次循环计算的结果
    signFlag >> 1   结果: 10
    signFlag << 1   结果:100
    100 != signFlag(101) 结果:true  //3号已签到,说明连续签到2天
    signFlag >>= 1  结果: 10   // 此时signFlag = 10
    

    third cycle

    signFlag = 10  // 前一次循环计算的结果
    signFlag >> 1   结果: 1
    signFlag << 1   结果:10
    10 != signFlag(10) 结果:false  //2号未签到,说明连续签到从这里就断了 
    signFlag >>= 1  结果: 1   // 此时signFlag = 1
    

    At this step, the first false is encountered, indicating that the continuous sign-in is interrupted;

    Fourth loop:

    signFlag = 1  // 前一次循环计算的结果
    signFlag >> 1   结果: 0
    signFlag << 1   结果: 0
    0 != signFlag(1) 结果:true  //1号已签到
    

    At this point, according to BITFIELDthe obtained 11 (decimal), you can get that No. 1, 3, and 4 have signed in, and No. 2 has not signed in; signed in for 2 consecutive days;

After understanding the above logic, it will be much easier to look at the following SpringBoot code;

SpringBoot implements check-in

There are generally two ways to sign in, monthly (weekly)/customized period. The two methods of sign-in are listed below for your reference:

Sign in monthly

Sign-in tool category:

import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author 一行Java
 * @title: 按月签到
 * @projectName ehang-spring-boot
 * @description: TODO
 * @date 2022/7/18 18:28
 */
@Slf4j
@Service
public class SignByMonthServiceImpl {
    
    

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    private int dayOfMonth() {
    
    
        DateTime dateTime = new DateTime();
        return dateTime.dayOfMonth().get();
    }

    /**
     * 按照月份和用户ID生成用户签到标识 UserId:Sign:560:2021-08
     *
     * @param userId 用户id
     * @return
     */
    private String signKeyWitMouth(String userId) {
    
    
        DateTime dateTime = new DateTime();
        DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyy-MM");

        StringBuilder builder = new StringBuilder("UserId:Sign:");
        builder.append(userId).append(":")
                .append(dateTime.toString(fmt));
        return builder.toString();
    }

    /**
     * 设置标记位
     * 标记是否签到
     *
     * @param key
     * @param offset
     * @param tag
     * @return
     */
    public Boolean sign(String key, long offset, boolean tag) {
    
    
        return this.stringRedisTemplate.opsForValue().setBit(key, offset, tag);
    }


    /**
     * 统计计数
     *
     * @param key 用户标识
     * @return
     */
    public long bitCount(String key) {
    
    
        return stringRedisTemplate.execute((RedisCallback<Long>) redisConnection -> redisConnection.bitCount(key.getBytes()));
    }

    /**
     * 获取多字节位域
     */
    public List<Long> bitfield(String buildSignKey, int limit, long offset) {
    
    
        return this.stringRedisTemplate
                .opsForValue()
                .bitField(buildSignKey, BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(limit)).valueAt(offset));
    }


    /**
     * 判断是否被标记
     *
     * @param key
     * @param offest
     * @return
     */
    public Boolean container(String key, long offest) {
    
    
        return this.stringRedisTemplate.opsForValue().getBit(key, offest);
    }


    /**
     * 用户今天是否签到
     *
     * @param userId
     * @return
     */
    public int checkSign(String userId) {
    
    
        DateTime dateTime = new DateTime();

        String signKey = this.signKeyWitMouth(userId);
        int offset = dateTime.getDayOfMonth() - 1;
        int value = this.container(signKey, offset) ? 1 : 0;
        return value;
    }


    /**
     * 查询用户当月签到日历
     *
     * @param userId
     * @return
     */
    public Map<String, Boolean> querySignedInMonth(String userId) {
    
    
        DateTime dateTime = new DateTime();
        int lengthOfMonth = dateTime.dayOfMonth().getMaximumValue();
        Map<String, Boolean> signedInMap = new HashMap<>(dateTime.getDayOfMonth());

        String signKey = this.signKeyWitMouth(userId);
        List<Long> bitfield = this.bitfield(signKey, lengthOfMonth, 0);
        if (!CollectionUtils.isEmpty(bitfield)) {
    
    
            long signFlag = bitfield.get(0) == null ? 0 : bitfield.get(0);

            DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyy-MM-dd");
            for (int i = lengthOfMonth; i > 0; i--) {
    
    

                DateTime dateTime1 = dateTime.withDayOfMonth(i);
                signedInMap.put(dateTime1.toString(fmt), signFlag >> 1 << 1 != signFlag);
                signFlag >>= 1;
            }
        }
        return signedInMap;
    }


    /**
     * 用户签到
     *
     * @param userId
     * @return
     */
    public boolean signWithUserId(String userId) {
    
    
        int dayOfMonth = this.dayOfMonth();
        String signKey = this.signKeyWitMouth(userId);
        long offset = (long) dayOfMonth - 1;
        boolean re = false;
        if (Boolean.TRUE.equals(this.sign(signKey, offset, Boolean.TRUE))) {
    
    
            re = true;
        }

        // 查询用户连续签到次数,最大连续次数为7天
        long continuousSignCount = this.queryContinuousSignCount(userId, 7);
        return re;
    }

    /**
     * 统计当前月份一共签到天数
     *
     * @param userId
     */
    public long countSignedInDayOfMonth(String userId) {
    
    
        String signKey = this.signKeyWitMouth(userId);
        return this.bitCount(signKey);
    }


    /**
     * 查询用户当月连续签到次数
     *
     * @param userId
     * @return
     */
    public long queryContinuousSignCountOfMonth(String userId) {
    
    
        int signCount = 0;
        String signKey = this.signKeyWitMouth(userId);
        int dayOfMonth = this.dayOfMonth();
        List<Long> bitfield = this.bitfield(signKey, dayOfMonth, 0);

        if (!CollectionUtils.isEmpty(bitfield)) {
    
    
            long signFlag = bitfield.get(0) == null ? 0 : bitfield.get(0);
            DateTime dateTime = new DateTime();
            // 连续不为0即为连续签到次数,当天未签到情况下
            for (int i = 0; i < dateTime.getDayOfMonth(); i++) {
    
    
                if (signFlag >> 1 << 1 == signFlag) {
    
    
                    if (i > 0) break;
                } else {
    
    
                    signCount += 1;
                }
                signFlag >>= 1;
            }
        }
        return signCount;
    }


    /**
     * 以7天一个周期连续签到次数
     *
     * @param period 周期
     * @return
     */
    public long queryContinuousSignCount(String userId, Integer period) {
    
    
        //查询目前连续签到次数
        long count = this.queryContinuousSignCountOfMonth(userId);
        //按最大连续签到取余
        if (period != null && period < count) {
    
    
            long num = count % period;
            if (num == 0) {
    
    
                count = period;
            } else {
    
    
                count = num;
            }
        }
        return count;
    }
}

Test class:

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.Map;

/**
 * @author 一行Java
 * @title: SignTest2
 * @projectName ehang-spring-boot
 * @description: TODO
 * @date 2022/7/19 12:06
 */
@SpringBootTest
@Slf4j
public class SignTest2 {
    
    
    @Autowired
    private SignByMonthServiceImpl signByMonthService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 测试用户按月签到
     */
    @Test
    public void querySignDay() {
    
    
        //模拟用户签到
        //for(int i=5;i<19;i++){
    
    
        redisTemplate.opsForValue().setBit("UserId:Sign:560:2022-08", 0, true);
        //}

        System.out.println("560用户今日是否已签到:" + this.signByMonthService.checkSign("560"));
        Map<String, Boolean> stringBooleanMap = this.signByMonthService.querySignedInMonth("560");
        System.out.println("本月签到情况:");
        for (Map.Entry<String, Boolean> entry : stringBooleanMap.entrySet()) {
    
    
            System.out.println(entry.getKey() + ": " + (entry.getValue() ? "√" : "-"));
        }
        long countSignedInDayOfMonth = this.signByMonthService.countSignedInDayOfMonth("560");
        System.out.println("本月一共签到:" + countSignedInDayOfMonth + "天");
        System.out.println("目前连续签到:" + this.signByMonthService.queryContinuousSignCount("560", 7) + "天");
    }
}

Execution log:

c.e.r.bitmap_sign_by_month.SignTest2     : 560用户今日是否已签到:0
c.e.r.bitmap_sign_by_month.SignTest2     : 本月签到情况:
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-12: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-11: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-10: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-31: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-30: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-19: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-18: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-17: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-16: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-15: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-14: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-13: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-23: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-01:c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-22: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-21: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-20: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-09: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-08: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-29: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-07: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-28: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-06: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-27: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-05: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-26: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-04: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-25: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-03:c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-24: -
c.e.r.bitmap_sign_by_month.SignTest2     : 2022-08-02: -
c.e.r.bitmap_sign_by_month.SignTest2     : 本月一共签到:2c.e.r.bitmap_sign_by_month.SignTest2     : 目前连续签到:1

Sign in at designated time

Sign-in tool category:

package com.ehang.redis.bitmap_sign_by_range;

import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author 一行Java
 * @title: SignByRangeServiceImpl
 * @projectName ehang-spring-boot
 * @description: TODO
 * @date 2022/7/19 12:27
 */
@Slf4j
@Service
public class SignByRangeServiceImpl {
    
    

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    /**
     * 根据区间的id 以及用户id 拼接key
     *
     * @param rangeId 区间ID 一般是指定活动的ID等
     * @param userId  用户的ID
     * @return
     */
    private String signKey(Integer rangeId, Integer userId) {
    
    
        StringBuilder builder = new StringBuilder("RangeId:Sign:");
        builder.append(rangeId).append(":")
                .append(userId);
        return builder.toString();
    }

    /**
     * 获取当前时间与起始时间的间隔天数
     *
     * @param start 起始时间
     * @return
     */
    private int intervalTime(LocalDateTime start) {
    
    
        return (int) (LocalDateTime.now().toLocalDate().toEpochDay() - start.toLocalDate().toEpochDay());
    }

    /**
     * 设置标记位
     * 标记是否签到
     *
     * @param key    签到的key
     * @param offset 偏移量 一般是指当前时间离起始时间(活动开始)的天数
     * @param tag    是否签到  true:签到  false:未签到
     * @return
     */
    private Boolean setBit(String key, long offset, boolean tag) {
    
    
        return this.stringRedisTemplate.opsForValue().setBit(key, offset, tag);
    }

    /**
     * 统计计数
     *
     * @param key 统计的key
     * @return
     */
    private long bitCount(String key) {
    
    
        return stringRedisTemplate.execute((RedisCallback<Long>) redisConnection -> redisConnection.bitCount(key.getBytes()));
    }

    /**
     * 获取多字节位域
     *
     * @param key    缓存的key
     * @param limit  获取多少
     * @param offset 偏移量是多少
     * @return
     */
    private List<Long> bitfield(String key, int limit, long offset) {
    
    
        return this.stringRedisTemplate
                .opsForValue()
                .bitField(key, BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(limit)).valueAt(offset));
    }

    /**
     * 判断是否签到
     *
     * @param key    缓存的key
     * @param offest 偏移量 指当前时间距离起始时间的天数
     * @return
     */
    private Boolean container(String key, long offest) {
    
    
        return this.stringRedisTemplate.opsForValue().getBit(key, offest);
    }

    /**
     * 根据起始时间进行签到
     *
     * @param rangeId
     * @param userId
     * @param start
     * @return
     */
    public Boolean sign(Integer rangeId, Integer userId, LocalDateTime start) {
    
    
        int offset = intervalTime(start);
        String key = signKey(rangeId, userId);
        return setBit(key, offset, true);
    }

    /**
     * 根据偏移量签到
     *
     * @param rangeId
     * @param userId
     * @param offset
     * @return
     */
    public Boolean sign(Integer rangeId, Integer userId, long offset) {
    
    
        String key = signKey(rangeId, userId);
        return setBit(key, offset, true);
    }

    /**
     * 用户今天是否签到
     *
     * @param userId
     * @return
     */
    public Boolean checkSign(Integer rangeId, Integer userId, LocalDateTime start) {
    
    
        long offset = intervalTime(start);
        String key = this.signKey(rangeId, userId);
        return this.container(key, offset);
    }

    /**
     * 统计当前月份一共签到天数
     *
     * @param userId
     */
    public long countSigned(Integer rangeId, Integer userId) {
    
    
        String signKey = this.signKey(rangeId, userId);
        return this.bitCount(signKey);
    }

    public Map<String, Boolean> querySigned(Integer rangeId, Integer userId, LocalDateTime start) {
    
    
        int days = intervalTime(start);
        Map<String, Boolean> signedInMap = new HashMap<>(days);

        String signKey = this.signKey(rangeId, userId);
        List<Long> bitfield = this.bitfield(signKey, days + 1, 0);
        if (!CollectionUtils.isEmpty(bitfield)) {
    
    
            long signFlag = bitfield.get(0) == null ? 0 : bitfield.get(0);

            DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
            for (int i = days; i >= 0; i--) {
    
    
                LocalDateTime localDateTime = start.plusDays(i);
                signedInMap.put(localDateTime.format(fmt), signFlag >> 1 << 1 != signFlag);
                signFlag >>= 1;
            }
        }
        return signedInMap;
    }

    /**
     * 查询用户当月连续签到次数
     *
     * @param userId
     * @return
     */
    public long queryContinuousSignCount(Integer rangeId, Integer userId, LocalDateTime start) {
    
    
        int signCount = 0;
        String signKey = this.signKey(rangeId, userId);
        int days = this.intervalTime(start);
        List<Long> bitfield = this.bitfield(signKey, days + 1, 0);

        if (!CollectionUtils.isEmpty(bitfield)) {
    
    
            long signFlag = bitfield.get(0) == null ? 0 : bitfield.get(0);
            DateTime dateTime = new DateTime();
            // 连续不为0即为连续签到次数,当天未签到情况下
            for (int i = 0; i < dateTime.getDayOfMonth(); i++) {
    
    
                if (signFlag >> 1 << 1 == signFlag) {
    
    
                    if (i > 0) break;
                } else {
    
    
                    signCount += 1;
                }
                signFlag >>= 1;
            }
        }
        return signCount;
    }
}

Test tool class:

package com.ehang.redis.bitmap_sign_by_range;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;

/**
 * @author 一行Java
 * @title: SignTest
 * @projectName ehang-spring-boot
 * @description: TODO
 * @date 2022/7/18 16:11
 */
@SpringBootTest
@Slf4j
public class SignTest {
    
    
    @Autowired
    SignByRangeServiceImpl signByRangeService;


    @Test
    void test() {
    
    
        DateTimeFormatter isoDateTime = DateTimeFormatter.ISO_DATE_TIME;
        // 活动开始时间
        LocalDateTime start = LocalDateTime.of(2022, 8, 1, 1, 0, 0);
        Integer rangeId = 1;
        Integer userId = 8899;
        log.info("签到开始时间: {}", start.format(isoDateTime));
        log.info("活动ID: {} 用户ID: {}", rangeId, userId);

        // 手动指定偏移量签到
        signByRangeService.sign(rangeId, userId, 0);

        // 判断是否签到
        Boolean signed = signByRangeService.checkSign(rangeId, userId, start);
        log.info("今日是否签到: {}", signed ? "√" : "-");

        // 签到
        Boolean sign = signByRangeService.sign(rangeId, userId, start);
        log.info("签到操作之前的签到状态:{} (-:表示今日第一次签到,√:表示今天已经签到过了)", sign ? "√" : "-");

        // 签到总数
        long countSigned = signByRangeService.countSigned(rangeId, userId);
        log.info("总共签到: {} 天", countSigned);

        // 连续签到的次数
        long continuousSignCount = signByRangeService.queryContinuousSignCount(rangeId, userId, start);
        log.info("连续签到: {} 天", continuousSignCount);

        // 签到的详情
        Map<String, Boolean> stringBooleanMap = signByRangeService.querySigned(rangeId, userId, start);
        for (Map.Entry<String, Boolean> entry : stringBooleanMap.entrySet()) {
    
    
            log.info("签到详情> {} : {}", entry.getKey(), (entry.getValue() ? "√" : "-"));
        }
    }
}

Output log:

签到开始时间: 2022-08-01T01:00:00
活动ID: 1 用户ID: 8899
今日是否签到:
签到操作之前的签到状态:√ (-:表示今日第一次签到,√:表示今天已经签到过了)
总共签到: 3
连续签到: 2
签到详情> 2022-08-01 :
签到详情> 2022-08-04 :
签到详情> 2022-08-03 :
签到详情> 2022-08-02 : -

GEO search nearby

Many lifestyle apps have a function to search for nearby businesses, such as Meituan’s search for nearby businesses;

Net map

If you want to implement a search function based on longitude and latitude, it is very troublesome; but Redis has added Redis GEO in version 3.2, which is used to store address location information and support range searches; based on GEO, you can easily and Quickly develop a nearby search function;

GEO API and Redis-cli operations:

  • geoadd : Add location coordinates.

    127.0.0.1:6379> GEOADD drinks 116.62445 39.86206 starbucks 117.3514785 38.7501247 yidiandian 116.538542 39.75412 xicha
    (integer) 3
    
  • geopos : Get location coordinates.

    127.0.0.1:6379> GEOPOS drinks starbucks
    1) 1) "116.62445157766342163"
       2) "39.86206038535793539"
    127.0.0.1:6379> GEOPOS drinks starbucks yidiandian mxbc
    1) 1) "116.62445157766342163"
       2) "39.86206038535793539"
    2) 1) "117.35148042440414429"
       2) "38.75012383773680114"
    3) (nil)
    
  • geodist : Calculates the distance between two locations.

    Unit parameters:

    • m: Meter, default unit.
    • km: kilometers.
    • mi: miles.
    • ft: feet.
    127.0.0.1:6379> GEODIST drinks starbucks yidiandian
    "138602.4133"
    127.0.0.1:6379> GEODIST drinks starbucks xicha
    "14072.1255"
    127.0.0.1:6379> GEODIST drinks starbucks xicha m
    "14072.1255"
    127.0.0.1:6379> GEODIST drinks starbucks xicha km
    "14.0721"
    
  • georadius : Get a set of geographical locations within a specified range based on the latitude and longitude coordinates given by the user.

    Parameter Description

    • m: Meter, default unit.
    • km: kilometers.
    • mi: miles.
    • ft: feet.
    • WITHDIST: While returning the position element, the distance between the position element and the center is also returned.
    • WITHCOORD: Returns the longitude and latitude of the location element as well.
    • WITHHASH: Returns the original geohash encoded ordered set score of the position element as a 52-bit signed integer. This option is mainly used for low-level applications or debugging, and has little practical effect.
    • COUNT limits the number of records returned.
    • ASC: Search results are sorted from nearest to farthest.
    • DESC: Search results are sorted from farthest to nearest.
    127.0.0.1:6379> GEORADIUS drinks 116 39 100 km WITHDIST
    1) 1) "xicha"
       2) "95.8085"
    127.0.0.1:6379> GEORADIUS drinks 116 39 100 km WITHDIST WITHCOORD
    1) 1) "xicha"
       2) "95.8085"
       3) 1) "116.53854042291641235"
          2) "39.75411928478748536"
    127.0.0.1:6379> GEORADIUS drinks 116 39 100 km WITHDIST WITHCOORD WITHHASH
    1) 1) "xicha"
       2) "95.8085"
       3) (integer) 4069151800882301
       4) 1) "116.53854042291641235"
          2) "39.75411928478748536"
    
    127.0.0.1:6379> GEORADIUS drinks 116 39 120 km WITHDIST WITHCOORD  COUNT 1
    1) 1) "xicha"
       2) "95.8085"
       3) 1) "116.53854042291641235"
          2) "39.75411928478748536"
    
    127.0.0.1:6379> GEORADIUS drinks 116 39 120 km WITHDIST WITHCOORD  COUNT 1 ASC
    1) 1) "xicha"
       2) "95.8085"
       3) 1) "116.53854042291641235"
          2) "39.75411928478748536"
    
    127.0.0.1:6379> GEORADIUS drinks 116 39 120 km WITHDIST WITHCOORD  COUNT 1 DESC
    1) 1) "starbucks"
       2) "109.8703"
       3) 1) "116.62445157766342163"
          2) "39.86206038535793539"
    
  • georadiusbymember : Get the geographical location collection within the specified range based on a location stored in the location collection.

    The function is similar to the georadius above, except that the georadius is centered on the longitude and latitude coordinates, this one is centered on a certain location;

  • geohash : Returns the geohash value of one or more location objects.

    127.0.0.1:6379> GEOHASH drinks starbucks xicha
    1) "wx4fvbem6d0"
    2) "wx4f5vhb8b0"
    

SpringBoot operations

An example of operating GEO through SpringBoot is as follows

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.geo.*;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.List;

/**
 * @author 一行Java
 * @title: GEOTest
 * @projectName ehang-spring-boot
 * @description: TODO
 * @date 2022/7/28 17:29
 */
@SpringBootTest
@Slf4j
public class GEOTest {
    
    
    private final String KEY = "geo:drinks";

    @Autowired
    RedisTemplate redisTemplate;

    @Test
    public void test() {
    
    
        add("starbucks", new Point(116.62445, 39.86206));
        add("yidiandian", new Point(117.3514785, 38.7501247));
        add("xicha", new Point(116.538542, 39.75412));

        get("starbucks", "yidiandian", "xicha");

        GeoResults nearByXY = getNearByXY(new Point(116, 39), new Distance(120, Metrics.KILOMETERS));
        List<GeoResult> content = nearByXY.getContent();
        for (GeoResult geoResult : content) {
    
    
            log.info("{}", geoResult.getContent());
        }

        GeoResults nearByPlace = getNearByPlace("starbucks", new Distance(120, Metrics.KILOMETERS));
        content = nearByPlace.getContent();
        for (GeoResult geoResult : content) {
    
    
            log.info("{}", geoResult.getContent());
        }

        getGeoHash("starbucks", "yidiandian", "xicha");

        del("yidiandian", "xicha");
    }

    private void add(String name, Point point) {
    
    
        Long add = redisTemplate.opsForGeo().add(KEY, point, name);
        log.info("成功添加名称:{} 的坐标信息信息:{}", name, point);
    }


    private void get(String... names) {
    
    
        List<Point> position = redisTemplate.opsForGeo().position(KEY, names);
        log.info("获取名称为:{} 的坐标信息:{}", names, position);
    }

    private void del(String... names) {
    
    
        Long remove = redisTemplate.opsForGeo().remove(KEY, names);
        log.info("删除名称为:{} 的坐标信息数量:{}", names, remove);
    }

    /**
     * 根据坐标 获取指定范围的位置
     *
     * @param point
     * @param distance
     * @return
     */
    private GeoResults getNearByXY(Point point, Distance distance) {
    
    
        Circle circle = new Circle(point, distance);
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.
                newGeoRadiusArgs().
                includeDistance(). // 包含距离
                includeCoordinates(). // 包含坐标
                sortAscending(). // 排序 还可选sortDescending()
                limit(5); // 获取前多少个
        GeoResults geoResults = redisTemplate.opsForGeo().radius(KEY, circle, args);
        log.info("根据坐标获取:{} {} 范围的数据:{}", point, distance, geoResults);
        return geoResults;
    }

    /**
     * 根据一个位置,获取指定范围内的其他位置
     *
     * @param name
     * @param distance
     * @return
     */
    private GeoResults getNearByPlace(String name, Distance distance) {
    
    
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.
                newGeoRadiusArgs().
                includeDistance(). // 包含距离
                includeCoordinates(). // 包含坐标
                sortAscending(). // 排序 还可选sortDescending()
                limit(5); // 获取前多少个
        GeoResults geoResults = redisTemplate.opsForGeo()
                .radius(KEY, name, distance, args);
        log.info("根据位置:{} 获取: {} 范围的数据:{}", name, distance, geoResults);
        return geoResults;
    }

    /**
     * 获取GEO HASH
     *
     * @param names
     * @return
     */
    private List<String> getGeoHash(String... names) {
    
    
        List<String> hash = redisTemplate.opsForGeo().hash(KEY, names);
        log.info("names:{} 对应的hash:{}", names, hash);
        return hash;
    }
}

Execution log:

成功添加名称:starbucks 的坐标信息信息:Point [x=116.624450, y=39.862060]
成功添加名称:yidiandian 的坐标信息信息:Point [x=117.351479, y=38.750125]
成功添加名称:xicha 的坐标信息信息:Point [x=116.538542, y=39.754120]

获取名称为:[starbucks, yidiandian, xicha] 的坐标信息:[Point [x=116.624452, y=39.862060], Point [x=117.351480, y=38.750124], Point [x=116.538540, y=39.754119]]

根据坐标获取:Point [x=116.000000, y=39.000000] 120.0 KILOMETERS 范围的数据:GeoResults: [averageDistance: 102.8394 KILOMETERS, results: GeoResult [content: RedisGeoCommands.GeoLocation(name=xicha, point=Point [x=116.538540, y=39.754119]), distance: 95.8085 KILOMETERS, ],GeoResult [content: RedisGeoCommands.GeoLocation(name=starbucks, point=Point [x=116.624452, y=39.862060]), distance: 109.8703 KILOMETERS, ]]
RedisGeoCommands.GeoLocation(name=xicha, point=Point [x=116.538540, y=39.754119])
RedisGeoCommands.GeoLocation(name=starbucks, point=Point [x=116.624452, y=39.862060])

根据位置:starbucks 获取: 120.0 KILOMETERS 范围的数据:GeoResults: [averageDistance: 7.03605 KILOMETERS, results: GeoResult [content: RedisGeoCommands.GeoLocation(name=starbucks, point=Point [x=116.624452, y=39.862060]), distance: 0.0 KILOMETERS, ],GeoResult [content: RedisGeoCommands.GeoLocation(name=xicha, point=Point [x=116.538540, y=39.754119]), distance: 14.0721 KILOMETERS, ]]
RedisGeoCommands.GeoLocation(name=starbucks, point=Point [x=116.624452, y=39.862060])
RedisGeoCommands.GeoLocation(name=xicha, point=Point [x=116.538540, y=39.754119])

names:[starbucks, yidiandian, xicha] 对应的hash:[wx4fvbem6d0, wwgkqqhxzd0, wx4f5vhb8b0]

删除名称为:[yidiandian, xicha] 的坐标信息数量:2

Simple current limit

In order to ensure the safe and stable operation of the project and prevent the entire system from being brought down by malicious users or abnormal traffic, current limiting is generally added, such as the common ones, , to achieve current limiting control; if the project uses Redis, you can also use sentialRedis hystrix, To implement a simple current limiting function;

Function required commands

  • INCR : Increase the numerical value stored in key by one
  • Expire : Set the validity period of the key

Redis-cli operation

127.0.0.1:6379> INCR r:f:user1
(integer) 1
# 第一次 设置一个过期时间
127.0.0.1:6379> EXPIRE r:f:user1 5
(integer) 1
127.0.0.1:6379> INCR r:f:user1
(integer) 2
# 等待5s 再次增加 发现已经重置了
127.0.0.1:6379> INCR r:f:user1
(integer) 1

SpringBoot example:

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.concurrent.TimeUnit;

/**
 * @author 一行Java
 * @title: 基于Redis的简单限流
 * @projectName ehang-spring-boot
 * @description: TODO
 * @date 2022/8/2 9:43
 */
@SpringBootTest
@Slf4j
public class FreqTest {
    
    
    // 单位时间(秒)
    private static final Integer TIME = 5;
    // 允许访问上限次数
    private static final Integer MAX = 100;

    @Autowired
    RedisTemplate redisTemplate;

    @Test
    public void test() throws Exception {
    
    
        String userName = "user1";

        int tag = 1;

        boolean frequency = frequency(userName);
        log.info("第{}次是否放行:{}", tag, frequency);
        for (int i = 0; i < 100; i++) {
    
    
            tag += 1;
            frequency(userName);
        }
        frequency = frequency(userName);
        log.info("第{}次是否放行:{}", tag, frequency);

        Thread.sleep(5000);
        frequency = frequency(userName);
        log.info("模拟等待5s后,第{}次是否放行:{}", tag, frequency);
    }

    /**
     * 校验访问频率
     *
     * @param uniqueId 用于限流的唯一ID 可以是用户ID、或者客户端IP等
     * @return true:放行  false:拦截
     */
    private boolean frequency(String uniqueId) {
    
    
        String key = "r:q:" + uniqueId;
        Long increment = redisTemplate.opsForValue().increment(key);
        if (increment == 1) {
    
    
            redisTemplate.expire(key, TIME, TimeUnit.SECONDS);
        }

        if (increment <= MAX) {
    
    
            return true;
        }

        return false;
    }
}

Running log:

user1 第1次请求是否放行:true
user1 第101次请求是否放行:false
模拟等待5s后,user1 第101次请求是否放行:true

Global ID

In distributed systems, a global unique ID is required in many scenarios. Since Redis is an application independent of business services, Incratomic operations can be used to generate a globally unique incremental ID.

Function required commands

  • INCR : Increase the numerical value stored in key by one

Redis-cli client test

127.0.0.1:6379> incr g:uid
(integer) 1
127.0.0.1:6379> incr g:uid
(integer) 2
127.0.0.1:6379> incr g:uid
(integer) 3

Simple distributed lock

In a distributed system, many operations require the use of distributed locks to prevent concurrent operations from causing problems; because redis is independent of other services outside the distributed system, redis can be used to implement a simple imperfect Distributed lock;

Function required commands

  • SETNX key does not exist, set it; key exists, do not set it

    # 加锁
    127.0.0.1:6379> SETNX try_lock 1
    (integer) 1
    # 释放锁
    127.0.0.1:6379> del try_lock
    (integer) 1
    

  • set key value [ex seconds] [nx | xx]

    Although the above method can add locks, it is not difficult to find that deadlocks can easily occur; for example, after user a locks, the system suddenly hangs up. At this time, user a will never release the lock he holds. , thus leading to deadlock; for this, we can use the expiration time of redis to prevent deadlock problems

    set try_lock 1 ex 5 nx
    

imperfect lock

Although the above solution solves the deadlock problem, it also brings a new problem. If the execution time is longer than the automatic release time (for example, the automatic release is 5 seconds, but the business execution takes 8 seconds), then in At the 5th second, the lock is automatically released. At this time, other threads can obtain the lock normally. The simple process is as follows:

At this time, the time interval of the same color part is executed by multiple threads at the same time. And there is no perfect solution to this problem under this scheme , it can only be avoided as much as possible :

  • Method 1: Set the value to a random number (such as 1234). When the program releases the lock, it checks whether the lock was added by itself; for example, the lock released by thread A at the 8th second was added by thread B. At this time, the lock is released. When , you can check whether the value is the value you originally set (1234). If it is, release it; if not, just ignore it;
  • Method 2: Only use this solution for businesses with relatively small time consumption , and try to avoid the execution time exceeding the automatic release time of the lock.

Recommendations from people you know/friends

You will see friend recommendations in applications such as Alipay, Douyin, and QQ;

Friend recommendations are often based on your friend network, recommending people you may know to you and allowing you to add friends. If you randomly find someone to recommend to you in the system, the possibility of you knowing them is very small. , the purpose of recommendation is lost at this time;

For example, A and B are friends, and B and C are friends. At this time, the probability of A and C knowing each other is relatively high, so you can recommend friends between A and C;

Based on this logic, you can use the Set collection of Redis to cache each user's friend list, and then use the difference set method to implement friend recommendation;

Commands required for functionality

  • SADD key member [member...]: Add elements to the collection and cache the friend list
  • SDIFF key [key…]: Take the difference between the two sets and find the users that can be recommended

Redis-cli client test

# 记录 用户1 的好友列表
127.0.0.1:6379> SADD fl:user1 user2 user3
(integer) 2
# 记录 用户2 的好友列表
127.0.0.1:6379> SADD fl:user2 user1 user4
(integer) 2

# 用户1 可能认识的人 ,把自己(user1)去掉,user4是可能认识的人
127.0.0.1:6379> SDIFF fl:user2 fl:user1
1) "user1"
2) "user4"

# 用户2 可能认识的人 ,把自己(user2)去掉,user3是可能认识的人
127.0.0.1:6379> SDIFF fl:user1 fl:user2
1) "user3"
2) "user2"

However, this is only one factor in the recommendation mechanism, and other conditions can be used to enhance the accuracy of recommendations;

publish/subscribe

Publish/subscribe is a commonly used mode; in a distributed system, if you need to sense some changes in real time, for example: some configuration changes require real-time synchronization, you can use the publish and subscribe functions.

Commonly used APIs

  • PUBLISH channel message: Push the message to the specified channel
  • SUBSCRIBE channel [channel ...]: Subscribe to information from a given channel or channels

Redis-cli operation

As shown in the figure above, multiple clients on the left have subscribed to the channel. When the client on the right sends a message to the channel, the client on the left can receive the corresponding message.

message queue

When it comes to message queues, Kafka, RabbitMQ, etc. are commonly used. In fact, Redis can also implement a message queue using List;

Instructions required for functionality

  • RPUSH key value1 [value2]: Add one or more values ​​to the list;
  • BLPOP key1 [key2] timeout: Remove and get the first element of the list. If there is no element in the list, the list will be blocked until the wait times out or a pop-up element is found;
  • BRPOP key1 [key2] timeout: Remove and get the last element of the list. If there is no element in the list, the list will be blocked until the wait times out or a pop-up element is found.

Dependency adjustments:

Starting from version 2.0, Spring Boot has replaced the default Redis client Jedis with Lettuce. When testing this blocking, a timeout exception will occur; a good solution has not been found io.lettuce.core.RedisCommandTimeoutException: Command timed out after 1 minute(s), so lettuce is replaced with Jedis here. There is no problem. The configuration of pom.xml is as follows:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </exclusion>
        <exclusion>
            <artifactId>lettuce-core</artifactId>
            <groupId>io.lettuce</groupId>
        </exclusion>
    </exclusions>
</dependency>
<!-- jedis客户端 -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>
<!-- spring2.X集成redis所需common-pool2,使用jedis必须依赖它-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

Test code:

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

/**
 * @author 一行Java
 * @title: QueueTest
 * @projectName ehang-spring-boot
 * @description: TODO
 * @date 2022/8/5 14:27
 */
@SpringBootTest
@Slf4j
public class QueueTest {
    
    
    private static final String REDIS_LP_QUEUE = "redis:lp:queue";
    private static final String REDIS_RP_QUEUE = "redis:rp:queue";

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    /**
     * 先进后出队列
     */
    @Test
    public void rightMonitor() {
    
    
        while (true) {
    
    
            Object o = stringRedisTemplate.opsForList().rightPop(REDIS_LP_QUEUE, 0, TimeUnit.SECONDS);
            log.info("先进后出队列 接收到数据:{}", o);
        }
    }

    /**
     * 先进先出队列
     */
    @Test
    public void leftMonitor() {
    
    
        while (true) {
    
    
            Object o = stringRedisTemplate.opsForList().leftPop(REDIS_RP_QUEUE, 0, TimeUnit.SECONDS);
            log.info("先进先出队列 接收到数据:{}", o);
        }
    }
}
  • First in first out test effect

  • First in, last out test effect

However, in scenarios where the reliability of messages is relatively high, it is recommended to use a professional message queue framework. After the value is popped, the corresponding value no longer exists in the List. If the program crashes at this time, the message will appear. Lost, reliability cannot be guaranteed; although there are strategies to ensure the reliability of the message, for example, while popping up, save it to another queue (BRPOPLPUSH), and after success, remove it from another queue. When the message If you handle a failure or exception and then re-enter the queue for execution, the gain outweighs the loss.

Data sharing (session sharing)

Since Redis can persist data, it can be used to realize data sharing between modules; SpringBoot Session uses this mechanism to realize Session sharing;

  • rely

    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
    </dependency>
    
  • Enable session sharing

    @Configuration
    @EnableRedisHttpSession
    public class RedisSessionConfig {
          
          
    }
    
  • test code

    package com.ehang.redis.controller;
    
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.servlet.http.HttpServletRequest;
    import java.util.Enumeration;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * @author session 共享
     * @title: RedisSessionController
     * @projectName ehang-spring-boot
     * @description: TODO
     * @date 2022/8/5 15:58
     */
    @RestController
    @RequestMapping("session")
    public class RedisSessionController {
          
          
    
        /**
         * 设置session的值
         * @param request
         * @return
         */
        @GetMapping("set")
        public Map set(HttpServletRequest request) {
          
          
            String id = request.getSession().getId();
            Map<String, String> vas = new HashMap<>();
    
            String key = "key";
            String value = "value";
            vas.put("id", id);
            vas.put(key, value);
            // 自定义session的值
            request.getSession().setAttribute(key, value);
    
            return vas;
        }
    
        /**
         * 获取session的值
         * @param request
         * @return
         */
        @GetMapping("get")
        public Map get(HttpServletRequest request) {
          
          
            Map<String, Object> vas = new HashMap<>();
    
            // 遍历所有的session值
            Enumeration<String> attributeNames = request.getSession().getAttributeNames();
            while (attributeNames.hasMoreElements()) {
          
          
                String k = attributeNames.nextElement();
                Object va = request.getSession().getAttribute(k);
                vas.put(k, va);
            }
    
            vas.put("id", request.getSession().getId());
    
            return vas;
        }
    }
    
  • test

    Open two services and answer 8080 and 8081 respectively. 8080 calls the assignment interface and 8081 calls the acquisition interface. As shown in the figure below, you can see that the two services share a session data;

  • Data saved in Redis

    127.0.0.1:6379> keys spring:*
    1) "spring:session:sessions:expires:6f1d7d53-fe01-4e80-9e6a-5ff54fffa92a"
    2) "spring:session:expirations:1659688680000"
    3) "spring:session:sessions:6f1d7d53-fe01-4e80-9e6a-5ff54fffa92a"
    

Product filter

Mall applications will have a product filtering function similar to the one shown below to help users quickly search for ideal products;

Assume that iPhone 100 and Huawei Mate 5000 have been released and are available in major shopping malls. Next, we will implement the above product filtering function through Redis set;

Function required commands

  • SADD key member [member …]: Add one or more elements
  • SINTER key [key …]: Returns the intersection of all given sets

Redis-cli client test

# 将iphone100 添加到品牌为苹果的集合
127.0.0.1:6379> sadd brand:apple iphone100
(integer) 1

# 将meta5000 添加到品牌为苹果的集合
127.0.0.1:6379> sadd brand:huawei meta5000
(integer) 1

# 将 meta5000 iphone100 添加到支持5T内存的集合
127.0.0.1:6379> sadd ram:5t iphone100 meta5000
(integer) 2

# 将 meta5000 添加到支持10T内存的集合
127.0.0.1:6379> sadd ram:10t meta5000
(integer) 1

# 将 iphone100 添加到操作系统是iOS的集合
127.0.0.1:6379> sadd os:ios iphone100
(integer) 1

# 将 meta5000 添加到操作系统是Android的集合
127.0.0.1:6379> sadd os:android meta5000
(integer) 1

# 将 iphone100 meta5000 添加到屏幕为6.0-6.29的集合中
127.0.0.1:6379> sadd screensize:6.0-6.29 iphone100 meta5000
(integer) 2

# 筛选内存5T、屏幕在6.0-6.29的机型
127.0.0.1:6379> sinter ram:5t screensize:6.0-6.29
1) "meta5000"
2) "iphone100"

# 筛选内存10T、屏幕在6.0-6.29的机型
127.0.0.1:6379> sinter ram:10t screensize:6.0-6.29
1) "meta5000"

# 筛选内存5T、系统为iOS的机型
127.0.0.1:6379> sinter ram:5t screensize:6.0-6.29 os:ios
1) "iphone100"

# 筛选内存5T、屏幕在6.0-6.29、品牌是华为的机型
127.0.0.1:6379> sinter ram:5t screensize:6.0-6.29 brand:huawei
1) "meta5000"

shopping cart

Product cache

In e-commerce projects, product information will be cached, especially for popular products, which have more users. Since the product results are more complex, store information, product information, title, description, detail pictures, and cover pictures; in order to facilitate management and Operations are generally stored in the form of Hash (key is the product ID, field is used to store various parameters, and value stores the value of the pair)

shopping cart

When the product information is cached, what the shopping cart needs to do is to record the product ID and the quantity to be purchased through Hash (the key is the user information, the field is the product ID, and the value is used to record the quantity purchased);

Function required commands

  • HSET key field value: Set the value of the field field in the hash table key to value;
  • HMSET key field1 value1 [field2 value2]: Set multiple field-value (field-value) pairs to the hash table key at the same time.
  • HGET key field: Get the value of the specified field stored in the hash table.
  • HGETALL key: Get all fields and values ​​of the specified key in the hash table
  • HINCRBY key field increment: Add increment to the integer value of the specified field in the hash table key.
  • HLEN key: Get the number of fields in the hash table

Redis-cli client test

# 购物车添加单个商品
127.0.0.1:6379> HSET sc:u1 c001 1
(integer) 1
# 购物车添加多个商品
127.0.0.1:6379> HMSET sc:u1 c002 1 coo3 2
OK
# 添加商品购买数量
127.0.0.1:6379> HINCRBY sc:u1 c002 1
(integer) 2
# 减少商品的购买数量
127.0.0.1:6379> HINCRBY sc:u1 c003 -1
(integer) 1
# 获取单个的购买数量
127.0.0.1:6379> HGET sc:u1 c002
"2"
# 获取购物车的商品数量
127.0.0.1:6379> HLEN sc:u1
(integer) 3
# 购物车详情
127.0.0.1:6379> HGETALL sc:u1
1) "c001"
2) "1"
3) "c002"
4) "2"
5) "coo3"
6) "2"

Cancel orders regularly (key expiration monitoring)

E-commerce businesses generally have the function of automatically canceling orders if they are not paid for 30 minutes. At this time, you need to use the scheduled task framework. Quartz, xxl-job, and elastic-job are more commonly used Java scheduled tasks; we can also The order cancellation function is implemented through Redis's scheduled expiration and expired key monitoring;

  • Redis key expiration reminder configuration

    Modify redis related event configuration. Find the redis configuration file redis.conf and check the notify-keyspace-events configuration item. If not, add notify-keyspace-events Ex. If there is a value, add Ex. The relevant parameters are explained as follows:

    • K:keyspace event, the event is published with keyspace@ as the prefix

    • E:keyevent event, the event is published with keyevent@ as the prefix

    • g: General, non-specific types of commands, such as del, expire, rename, etc.

    • $:String specific command

    • l: List specific commands

    • s: Collection of specific commands

    • h:Hash specific command

    • z: Ordered collection specific command

    • x: Expiration event, this event is generated when a key expires and is deleted

    • e: Eviction event, this event is generated when a key is deleted due to maxmemore policy

    • A: Alias ​​for g$lshzxe, so "AKE" means all events

  • Add a listener for RedisKeyExpirationListener

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.listener.RedisMessageListenerContainer;
    
    @Configuration
    public class RedisListenerConfig {
          
          
    
        @Bean
        RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
          
          
    
            RedisMessageListenerContainer container = new RedisMessageListenerContainer();
            container.setConnectionFactory(connectionFactory);
            return container;
        }
    }
    

    KeyExpirationEventMessageListenerThe interface monitors expiration events of all dbskeyevent@*:expired"

    package com.ehang.redis.config;
    
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.data.redis.connection.Message;
    import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
    import org.springframework.data.redis.listener.RedisMessageListenerContainer;
    import org.springframework.stereotype.Component;
    
    /**
     * 监听所有db的过期事件__keyevent@*__:expired"
     *
     * @author 一行Java
     * @title: RedisKeyExpirationListener
     * @projectName ehang-spring-boot
     * @description: TODO
     * @date 2022/8/5 16:36
     */
    @Component
    @Slf4j
    public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
          
          
    
        public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
          
          
            super(listenerContainer);
        }
    
        /**
         * 针对 redis 数据失效事件,进行数据处理
         *
         * @param message
         * @param pattern
         */
        @Override
        public void onMessage(Message message, byte[] pattern) {
          
          
    
            // 获取到失效的 key,进行取消订单业务处理
            // 由于这里是监听了所有的key,如果只处理特定数据的话,需要做特殊处理
            String expiredKey = message.toString();
            log.info("过期的Key:{}", expiredKey);
        }
    }
    
  • test

    In order to quickly verify the effect, the expiration time is adjusted to 2 seconds;

    Note that after expiration, the Key in Redis no longer exists. Therefore, the order number must be used as the key and cannot be saved as a value. Otherwise, after monitoring the expired Key, the expired order number will not be obtained;

  • Not recommended

    Based on this mechanism, timeout cancellation of orders can indeed be achieved, but it is still not recommended. It is only used as an idea; the main reasons are as follows:

    1. The expiration deletion strategy of redis is to use scheduled offline scanning, or lazy detection and deletion during access. There is no way to ensure timeliness. It is possible that the key has expired, but Redis has not scanned it, resulting in a delay in notification;
    2. Messages are sent and forgotten (fire and forget), which does not guarantee the reachability of the message. If the service is not online or abnormal at this time, the notification will no longer be received;

Logistics information (timeline)

When sending express delivery or online shopping, if we check the logistics information, we will be shown where the express arrived at xxx. This is a typical timeline list;

The database method is to insert an information record with time for each change, and then sort the generation time according to time and ID (ID is required. If there are two identical times, pure time sorting will cause the order to be incorrect). Wire;

We can also implement the timeline function through Redis' List. Since List uses a two-way linked list, ascending and descending order timelines can be satisfied normally;

  • RPUSH key value1 [value2]: Add one or more values ​​to the list, (ascending timeline)
  • LPUSH key value1 [value2]: Insert one or more values ​​into the head of the list (descending timeline)
  • LRANGE key start stop: Get elements within the specified range of the list

Redis-cli client test

  • Ascending order

    127.0.0.1:6379> RPUSH time:line:asc 20220805170000
    (integer) 1
    127.0.0.1:6379> RPUSH time:line:asc 20220805170001
    (integer) 2
    127.0.0.1:6379> RPUSH time:line:asc 20220805170002
    (integer) 3
    127.0.0.1:6379> LRANGE time:line:asc 0 -1
    1) "20220805170000"
    2) "20220805170001"
    3) "20220805170002"
    
  • descending order

    127.0.0.1:6379> LPUSH time:line:desc 20220805170000
    (integer) 1
    127.0.0.1:6379> LPUSH time:line:desc 20220805170001
    (integer) 2
    127.0.0.1:6379> LPUSH time:line:desc 20220805170002
    (integer) 3
    127.0.0.1:6379> LRANGE time:line:desc 0 -1
    1) "20220805170002"
    2) "20220805170001"
    3) "20220805170000"
    

Okay, that’s it for introducing the wonderful uses of Redis; with the application of these scenarios, next time the interviewer asks you what Redis has done besides caching, I believe it shouldn’t be a problem if you chat for an hour.

Test source code: https://github.com/vehang/ehang-spring-boot/tree/main/spring-boot-011-redis

Most of the use cases are in the test directory

It’s not easy to organize. I look forward to your likes, shares, and forwarding...

Guess you like

Origin blog.csdn.net/lupengfei1009/article/details/126266870