20 usage scenarios of Redis

This article introduces the usage scenarios of Redis other than caching.

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

 

1 cache

This article assumes that you already know about Redis and know some of the most basic uses of Redis. If you don’t know the basic API of Redis, you can take a look at the rookie tutorial: https://www.runoob.com/redis, then the cache part And the demonstration of the basic API, so I won’t explain too much;

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

structure type the value stored by the structure structured literacy
String string Can be a string, integer or float Operate on the entire string or a part of the string; perform self-increment or self-decrement operations on integers or floating-point numbers;
List list A linked list, each node on the linked list contains a string Perform push and pop operations on both ends of the linked list, read single or multiple elements; find or delete elements according to the value;
Set collection an unordered collection containing strings A collection of strings, including basic methods: check whether it exists, add, get, delete; also include calculation intersection, union, difference, etc.
Hash An unordered hash table containing key-value pairs Contains methods: add, get, delete a single element
Zset ordered set Like a hash, it is 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; methods include: add, get, delete a single element, and get elements by score range or member
  • rely

    All the following use cases that pass the SpringBoot test need to introduce Redis dependencies

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

2 lottery

Once upon a time, lottery drawing was a popular way for Internet APPs to promote and attract new users. If there is no good plan for holidays, let’s draw a lottery! A bunch of users participate, and then a few lucky users are randomly selected to give physical/virtual prizes; at this time, developers need to write a lottery algorithm to realize the lucky user selection; in fact, we can use the collection of Redis ( Set), you can easily implement the lottery function;

API required for function realization

  • SADD  key member1 [member2]: add one or more participating users;

  • SRANDMEMBER  KEY [count]: Randomly return one or more users;

  • SPOP  key: Randomly return one or more users and delete the returned users;

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

Redis-cli operation

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;
    }

}

3Set realizes the function of like/favorite

Apps with interactive attributes generally have functions such as like/favorite/like to enhance the interaction between users.

Traditional implementation : After the user likes, record a piece of data in the database, and generally record a number of likes/favorites in the theme library for easy display;

Redis solution : Based on the Redis collection (Set), record the user data of favorites and likes corresponding to each post/article. At the same time, set also provides checking whether there is a specified user in the collection, and the user can quickly determine whether the user has liked it

API required for function realization

  • 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 a member exists (like or not)

  • SREM  key member1 [member2] : Remove one or more members (number of likes)

Redis-cli API operation

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

Spring Boot 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.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);
    }

}

4 Leaderboards

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

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

The conventional method is to update the ranking data such as the user's ranking and score to the database, and then take out the top 50 display through Order by + limit when querying. If there are not many participating users and the data is not updated frequently, use There is nothing wrong with the way of the database, but once there is explosive hot news (for example: the mainland regains the Gulf, some xxx is green, etc.), there will be explosive traffic in a short period of time, and the instantaneous pressure may make the database overwhelmed;

Redis solution : cache the hot news on the whole page, and use Redis's ordered queue (Sorted Set) to cache the popularity (SCORES), which can instantly relieve the pressure on the database, and at the same time easily filter out the 50 most popular articles;

Commands required for function realization

  • 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"

Spring Boot 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], DefaultTyped

 

5PV statistics (incr self-increment count)

Page View (PV) refers to the number of page views, which is an important criterion for measuring traffic and a very important basis for data analysis; usually, the statistical rule is that once a page is displayed, it will add one

Commands required for functionality

  • INCR : Increment the digital value stored in the key by one

Redis-cli operation

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

6UV statistics (HeyperLogLog)

Previously, it was introduced to realize page PV through (INCR); in addition to PV, UV (independent visitor) is also a very important statistical data;

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

You might say, isn't there still a Set! Make a collection on a page, come to a user to insert (SADD) a user into it, and when you want to count UVs, you can easily get it done by summarizing the quantity through SCARD; this solution can indeed achieve the statistical effect of UVs, but ignores the cost ; If it is an ordinary page, hundreds or thousands of visits may have little impact. If you encounter a popular page and tens of millions or hundreds of millions of users visit it, just one page UV will bring a very large impact. Memory overhead, which is obviously not cost-effective for such precious memory.

At this time, the HeyperLogLog data structure can perfectly solve this problem. It provides an imprecise deduplication counting scheme, pay attention! It is emphasized here that it is inaccurate and there will be errors, but the error 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 is used for accurate deduplication counting.

In addition, HeyperLogLog will take up 12KB of storage space . Although Redis has optimized HeyperLogLog, when the stored data is relatively small, it uses sparse matrix storage. Only when the amount of data becomes larger and the space occupied by the sparse matrix exceeds the threshold, the It will be converted into a dense matrix with a space of 12KB; compared to 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 In the scenario of HyperLogLog, avoid improper use, resulting in adverse effects of resource consumption increasing instead of decreasing.

Commands required for functionality:

  • PFADD  key element [element ...]: increase count (statistic UV)

  • PFCOUNT  key [key...]: get count (cargo UV)

  • PFMERGE  destkey sourcekey [sourcekey ...]: Merge multiple HyperLogLogs into one HyperLogLog (multiple combined for statistics)

Redis-cli operation

# 添加三个用户的访问
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 operates HeyperLogLog

Simulation test 10000 users visit 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 the error, the actual number of visits here is 10,000, and the number of visits is 23 more. It is within the standard error (0.81%), and the UV data does not have to be accurate, so this error is acceptable. .

7 Deduplication (BloomFiler)

Through the study of HeyperLogLog above, we have mastered an inaccurate deduplication counting scheme, but have we found that he has no way to obtain whether a user has visited; ideally, we hope to have a PFEXISTScommand to judge a certain Whether the key exists, but HeyperLogLog does not; to achieve this requirement, BloomFiler has to be used.

  • What is Bloom Filter?

    Bloom Filter is a fast lookup 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 collection, but it is not strictly required to be 100% correct . It is an interesting and powerful algorithm based on a probabilistic data structure.

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

Common method : every time a page is crawled, insert a row of data into the database, record the URL, and every time you get a new page, check it in the database, if it exists, it means it has been crawled;

Disadvantages of the common method : small amount of data, there is no problem with the traditional solution. If it is a large amount of 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 easily determine whether a value exists at the cost of a small memory space.

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

  • error_rate : The error rate, the lower it is, the more space it needs;

  • initial_size : The estimated number of values ​​to be placed. When the actual number of values ​​is greater than the set value, the error rate will gradually increase; so in order to avoid the error rate, you can estimate it in advance to avoid another large error;

BloomFiler installation

For the convenience of testing, here is a quick installation using Docker

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

Spring Boot 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
    

8 User sign-in (BitMap)

In order to stimulate user activity, many apps often do some activities, such as continuous sign-in to collect points/gift packages, etc.

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

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

One month's check-in situation is recorded in 4 bytes (Source: Internet)

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

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

Required command:

  • SETBIT  key offset value: store a 0 or 1 in 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, self-increment) the value of the specified position offset 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 There is no way to understand the core part of the requirement;

Requirement: If it is currently August 4th, check the check-in status of this month, and the user has checked in on the 1st, 3rd, and 4th respectively

Redis-cli operation:

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

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

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

# 查询各天的签到情况
# 查询1号
127.0.0.1:6379> GETBIT RangeId:Sign:1:8899 0
(integer) 1
# 查询2号
127.0.0.1:6379> GETBIT RangeId:Sign:1:8899 1
(integer) 0
# 查询3号
127.0.0.1:6379> GETBIT RangeId:Sign:1:8899 2
(integer) 1
# 查询4号
127.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 situation of 1-4 is: 1011 (binary) ==> 11 (decimal)

Whether to sign in, continuous sign-in judgment

In the sign-in function, the most difficult thing to understand is the judgment of whether to sign in or not, and the continuous sign-in. In the following SpringBoot code, it is judged by this: It is a little signFlag >> 1 << 1 != signFlagdifficult to understand. Let me tell you in advance here;

The sign-in situation of No. 1-4 was tested above, and the BITFIELDobtained signFlag = 11 (decimal) = 1011 (binary);

The basis for judging continuous sign-in is: calculate the number of BITs that are 1 consecutively from right to left , binary 1011 means that the number of consecutive sign-in days is 2 days, and the calculation process of 2 days is as follows:

  • The first step is to get signFlag

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

  • The third step is to judge from right to left

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

    Check-in details : Cycle through all the days, true means you are currently signed in, and false means you have not checked in on that day;

    first cycle

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

    second cycle

    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 point, 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 system), you can get that No. 1, 3, and 4 have signed in, and No. 2 has not signed in; sign in for 2 consecutive days;

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

SpringBoot implements sign-in

There are generally two ways to sign in, monthly (weekly) / custom cycle, and the two ways of sign-in are listed below for your reference:

Sign in by month

Sign-in tool class:

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     : 本月一共签到:2天
c.e.r.bitmap_sign_by_month.SignTest2     : 目前连续签到:1天

Sign in at a specified time

Sign-in tool class:

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 : -

9GEO search nearby

Many lifestyle apps have a nearby search function, such as Meituan searching for nearby businesses;

Network map

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

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 position 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 : Computes the distance between two locations.

    Unit parameters:

    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"
    
    • m : meter, the default unit.

    • km: kilometer.

    • mi: miles.

    • ft: feet.

  • georadius : According to the longitude and latitude coordinates given by the user, obtain the collection of geographic locations within the specified range.

    Parameter Description

    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"
    
    • m : meter, the default unit.

    • km: kilometer.

    • mi: miles.

    • ft: feet.

    • WITHDIST: While returning the position element, the distance between the position element and the center is also returned.

    • WITHCOORD: The longitude and latitude of the location element are also returned.

    • WITHHASH: In the form of a 52-bit signed integer, return the ordered set score of the position element encoded by the original geohash. This option is mainly used for low-level applications or debugging, and it has little effect in practice.

    • COUNT limits the number of records returned.

    • ASC: Search results are sorted from closest to farthest.

    • DESC: Search results are sorted from far to near.

  • georadiusbymember : According to a location stored in the location collection, get the geographic location collection within the specified range.

    The function is similar to the georadius above, except that georadius is centered on the latitude and longitude coordinates, and 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"
    

Spring Boot operation

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

10 simple current limiting

In order to ensure the safe and stable operation of the project and prevent the entire system from being overwhelmed by malicious users or abnormal traffic, a current limit is generally sentialadded hystrix. To achieve a simple current limiting function;

Commands required for functionality

  • INCR : Increment the digital value stored in the 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

Spring Boot 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

11 global ID

In a distributed system, a globally unique ID is required in many scenarios. Since Redis is independent of other applications of business services, Incratomic operations can be used to generate a globally unique incremental ID.

Commands required for functionality

  • INCR : Increment the digital value stored in the 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

12 simple distributed lock

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

Commands required for functionality

  • 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 be locked, it is not difficult to find that deadlock is easy to occur; for example, after user a locks, the system suddenly hangs up, and at this time a will never release the lock he holds , resulting in a deadlock; for this, we can use the expiration time of redis to prevent the deadlock problem

    set try_lock 1 ex 5 nx
    

imperfect lock

Although the above solution solves the deadlock problem, it 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, and other threads can get the lock normally at this time. 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. Moreover, 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 (eg: 1234). When the program releases the lock, check whether it is a lock added by itself; for example, the lock released by thread A in the 8th second is added by thread B. When the value is set, you can check whether the value is the value you originally set (1234). If it is, it will be released, and if it is not, it will be ignored;

  • Method 2, choose this solution only for businesses with relatively small time consumption , and try to avoid the execution time exceeding the automatic release time of the lock as much as possible

13 People I know/recommended by friends

In Alipay, Douyin, QQ and other applications, you will see friend recommendations;

Friend recommendation is often based on your friend network. It recommends people you may know to you and lets you 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, A and C have a relatively high probability of knowing each other, so they can be recommended as friends between A and C;

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

Commands required for functionality

  • SADD  key member [member ...]: add elements to the collection, cache the friend list

  • SDIFF  key [key ...]: Take the difference between two sets and find out the users who 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 a factor in the recommendation mechanism, and other conditions can be used to enhance the accuracy of the recommendation;

14 publish/subscribe

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

Common APIs

  • PUBLISH  channel message: Push the message to the specified channel

  • SUBSCRIBE  channel [channel ...]: Subscribe to information on the 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.

15 message queue

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

Instructions required for functionality

  • RPUSH  key value1 [value2]: add one or more values ​​to the list;

  • BLPOP  key1 [key2] timeout: Move out 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: Move out 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.

Dependent adjustments:

Starting from version 2.0, Spring Boot replaces the default Redis client Jedis with Lettuce. When testing this block, a timeout exception occurs; no good solution is found, so io.lettuce.core.RedisCommandTimeoutException: Command timed out after 1 minute(s)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);
        }
    }
}
  • FIFO test effect

  • First in last out test results

However, for scenarios that require high reliability of messages, it is recommended to use a professional message queue framework. When the value is popped up, the corresponding value does not exist in the List. If the program crashes at this time, a 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 remove it from another queue after success, when the message Handling failures or exceptions, and then re-entering the queue for execution, just doing so is not worth the candle.

16 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, respectively answer 8080 and 8081, 8080 calls the assignment interface, 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"
    

17 Product screening

Mall-like applications will have a product screening function similar to the one shown in the figure below to help users quickly search for ideal products;

Suppose now that iphone 100 and Huawei mate 5000 have been released and launched in major shopping malls; the above-mentioned product screening function will be realized through the set of Redis;

Commands required for functionality

  • SADD  key member [member ...]: add one or more elements

  • SINTER  key [key ...]: returns the intersection of all sets given

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"

18 shopping cart

Product cache

In e-commerce projects, product information will be cached, especially for popular products, and there are many visiting users. Because the product results are more complicated, store information, product information, title, description, detail map, cover image; for the convenience of management and Operations are generally stored in the form of Hash (the key is the product ID, the field is used to store various parameters, and the value is used to store the corresponding value)

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 purchased quantity);

Commands required for functionality

  • 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 stored in the specified field 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"

19 Scheduled order cancellation (key expiration monitoring)

E-commerce businesses generally have the function of automatically canceling the order if the order is not paid for 30 minutes. At this time, a scheduled task framework is needed. Quartz, xxl-job, and elastic-job are commonly used Java scheduled tasks; we can also The order cancellation function is realized through the timing expiration of Redis and the monitoring of the expired key;

  • Redis key expiration reminder configuration

    Modify redis related event configuration. Find the redis configuration file redis.conf, 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 as follows:

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

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

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

    • $: string specific command

    • l: list specific commands

    • s: Collection specific commands

    • h:hash specific command

    • z: ordered set specific command

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

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

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

  • Add the listener of 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;
        }
    }
    

    KeyExpirationEventMessageListener The interface listens to all db expiration events keyevent@*: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 here;

    Note that after the 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 the expired Key is monitored, the expired order number will not be obtained;

  • not recommended

    Based on this set of mechanisms, it is indeed possible to cancel the order overtime, but it is not recommended to use it. This is just an idea; the main reasons are as follows:

    1. Redis's expired deletion strategy is to use regular offline scanning, or lazy detection and deletion during access, and there is no way to guarantee timeliness. It is possible that the key has expired, but Redis has not scanned it, resulting in a delay in notification;

    2. The message is sent and forgotten (fire and forget), and the reachability of the message is not guaranteed. If the service is not online or abnormal at this time, the notification will never be received again;

20 Logistics Information (Timeline)

When sending a courier or online shopping, querying logistics information will show us where the courier has arrived at xxx. This is a typical timeline list;

The method of the database is to insert an information record with time every time it is changed, and then sort the generated time according to the time and ID (ID is required, if two identical times appear, simply sorting by time will cause the order to be wrong) Wire;

We can also implement the timeline function through the List of Redis. Since the List uses a doubly linked list, the ascending and descending timelines can be normally satisfied;

  • 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 the 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"

 

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

Roll it up, technology knows no bounds!

Guess you like

Origin blog.csdn.net/baidu_38493460/article/details/130500729