Location>code7788 >text

Solution: Common Redis Caching Scenarios in Java Internet Projects

Popularity:443 ℃/2024-09-23 08:30:43

catalogs
  • preamble
  • I. Common key-value
  • II. Timeliness
  • III. Counter-related
  • IV. High real-time
  • V. Leaderboard Series
  • VI. Summary of articles

preamble

In the author's 3 years of Java front-line development experience, especially some mobile, user-heavy Internet projects, often used to Redis as a basic tool for caching middleware to solve some specific problems.

The following is a summary of some common Redis cache application scenarios, such as the common String type Key-Value, the high timeliness requirements of the scene, the hash structure of the scene and the high real-time requirements of the scene, basically covering all the five basic types of Redis.

If you've been using Redis as a caching middleware in your projects, then you're not familiar with the following. If you're new to the industry and haven't encountered Redis as a caching middleware yet, then it's okay, this article will help you to some extent.

For some basic concepts about caching, you can see here for another review:/CodeBlogMan/p/18022719

I. Common key-value

The first introduction is some common String type key-value structure scenarios in project development, such as:

  • User login information stored using the jsonStr structure, including: cell phone number, token, unique uuid, nickname, etc;
  • jsonStr Structure information of a popular product, including: product name, unique id, merchant, price, etc;
  • Distributed locks of type String with expiration time, including: lock timeout time, randomly generated value, determining the success of locking, releasing the lock, etc.

Here's a simple demo to show how to get user login information.

@RestController
@RequestMapping("/member")
public class MemberController {

    @Resource
    private MemberService memberService;

    /**
     * pass (a bill or inspection etc) userUuid Get Member Information
     * @param userUuid
     * @return Member Information
     */
    @GetMapping("/info")
    public Response<MemberVO> getMemberInfo(@RequestParam(value = "userUuid") String userUuid) {
        return ((userUuid));
    }

}
    @Resource
    private RedisTemplate<String, String> redisTemplate;

    private final String MEMBER_INFO_USER_UUID_KEY = "";
    
    @Override
    public MemberVO info(String userUuid) {
        //Check the cache first.
        String memberStr = ().get(RedisKey.MEMBER_INFO_USER_UUID_KEY.concat(userUuid));
        if ((memberStr)){
            return (memberStr, );
        }
        //Cache not rechecking the database
        LambdaQueryWrapper<Member> wrapper = new LambdaQueryWrapper<>();
        (Member::getMemberUuid, userUuid)
                .eq(Member::getEnableStatus, NumberUtils.INTEGER_ZERO)
                .eq(Member::getDataStatus, NumberUtils.INTEGER_ZERO);
        return (wrapper).convertExt();
    }
/**
 * Partial core attributes only
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class MemberVO extends BaseVO {
    /**
     * user-uniqueuuid
     */
    private String memberUuid;
    /**
     * log in token
     */
    private String token;
    /**
     * user nickname
     */
    private String nickName;
    /**
     * telephone number
     */
    private String mobile;
    /**
     * Avatar address
     */
    private String avatarImg;
    /**
     * distinguishing between the sexes:1-male,2-female,3-unknown
     */
    private Integer gender;
}

II. Timeliness

While developing we often come across time-sensitive scenarios with high expiration time requirements from the business, for example:

  • For example, a preview link for a project or event is set to expire after 30 minutes, i.e. access to the preview link is not allowed after half an hour;
  • Jump from the web to the app client to access some specific content, open the client with a share passphrase copied from the clipboard, and set it to expire after 60 minutes;
  • Coupons collected by users in the client or small programs are put into "My Coupons" after collection, and different types of coupons are set to have different expiration times, such as a points coupon that expires after 30 days.

Here are two examples. The demos are simple but runnable, not pseudo-code:

    /**
     * Event preview link expires in 30 minutes.
     */
    @Test
    public void testPreviewLinkExpire(){
        String baseUrl = "http://localhost:8089/initial/ealbum/preview/detail";
        
        String projectUuid = ();
        //Here is the signature of the link, if the signature is expired, then it means the whole link is expired.
        String tempLinkSign = DigestUtils.md5Hex(projectUuid);
        String signKey = RedisKey.INITIAL_EALBUM_TEMP_LINK_SIGN_KEY.concat(".") .concat(());
        ().set(signKey, tempLinkSign, (30)); String sign = ().
        String sign = ().get(signKey);
        if ((sign)){
            // Splicing the temp address
            StringBuilder tempLinkUrl = new StringBuilder(baseUrl)
                    .append("?projectId=").append(projectId)
                    .append("&sign=").append(sign);
            ("Print to see the preview address: {}", tempLinkUrl);
        }
        throw new BusinessException("Failed to generate preview link!") ;
    }
    /**
     * Clipboard password expires in 1 hour.
     */
    @Test
    public void testClipboardTextKey(){
        // here first produce a random uuid as an example
        String articleId = ();
        String redisKey = RedisKey.CLIP_BOARD_TEXT_KEY.concat(".") .concat(articleId);
        String value = (()
                .articleId(articleId)
                .articleTitle("Test Title")
                .copyright(NumberUtils.INTEGER_ZERO).build()););
        // a very simple structure, is the common String type key-value, but the timeliness requirements, an hour after the direct expiration of
        ().set(redisKey, value, (1)); //Here go get the value and determine the value.
        // here to get the value, to determine whether the expiration date
        ClipboardVO vo = (().get(redisKey))
                .filter(StringUtils::isNotBlank)
                .map(val -> (val, ))
                .orElse(null);
        ("Print out the returned vo: {}", vo);
    }

III. Counter-related

The counter is also a common Redis application scenario, such as the following:

  • Number of likes/favorites: the number of likes for an article and the number of favorites for an article can be synchronized to the article table as an attribute;

  • Article comment count: String structure, redis-key can be the article id, redis-value is the comment count, can also be synchronized to the article table as a property;

  • Number of unread messages: Hash structure, constant redis-key, user-identification as hash-key, quantity as hash-value, need to be synchronized to the notification table;

  • The number of items added to the shopping cart: using BoundHash structure, redis-key is the user ID, hash-key is the item id identifier, hash-value is the quantity, which needs to be synchronized to the shopping cart table.

Below are two examples of it, part of the entity, DTO/VO and so on is not written, we can mean on the good, the important thing is the idea:

    /**
     * Count of the number of unread notifications by the user
     */
    @Test
    public void testNoticeCountNum() {
        // On the hot key here scenario is a bit insufficient: because the number of notifications will only be called when starting the app and clicking the button, and not so frequently, QPS tens of thousands of should not be a problem!
        //But there is the possibility of a large key, then: 1, regularly clean the cache, with the database to solve; 2, Hash bottom will compress the data; 3, depending on the size of the occupied memory or the number of elements; 4, data slicing
        NoticeAddDTO dto = ()
                .targetUserUuid("123qwe456rty789uio")
                .superType(NumberUtils.INTEGER_TWO)
                .subType(5)
                .content("content content").build();
        //No matter what type of business system what type of message, first into the database
        Notification notification = (dto);; //1.
        //1, according to the tab type of the message to do, you can for each type of a new Redis-Key to store separately, so it is feasible, from a certain point of view is to split Key
        //2, think about it or use the Hash structure, with a List or String can solve the problem of type, but it is difficult to take the value according to the user.
        HashOperations<String, String, Integer> hashOperations = ();
        // Since the cache is cleared every hour, the current unread count needs to be checked against the database.
        (notification); if (NumberUtils.INTILS.NET)
        if (NumberUtils.INTEGER_ONE.equals(()) && NumberUtils.INTEGER_ONE.equals(())) {
            // Messages generated by the business system, +1 for the number of unread messages;
            Long increment = (RedisKey.INITIAL_NOTICE_COMMENT_NUM_PERFIX,(), 1);
            ("Number of new comment tab messages added: {}", increment);
        }
        if (NumberUtils.INTEGER_TWO.equals(())) {
            Long increment = (RedisKey.INITIAL_NOTICE_NUM_PERFIX, (), 1);
            ("Number of notification tab messages added: {}", increment);
        }
        // Business system withdraws message, unread count -1
        if (NumberUtils.INTEGER_TWO.equals(())) {
            //Redis does not exist to do self-subtracting one, get is -1; so there must be a value, do not judge the empty just to determine the positive and negative
            Long num = (RedisKey.INITIAL_NOTICE_NUM_PERFIX, (), -1);
            ("Number of notify tab messages after withdrawal: {}", num);
            int intNUm = ();
            // To avoid negative numbers, take 0 as a limit to compare
            int result = intNUm < NumberUtils.INTEGER_ZERO ? NumberUtils.INTEGER_ZERO : intNUm;
            (RedisKey.INITIAL_NOTICE_NUM_PERFIX, (), result);
        }
    }
    /**
     * Count of items added to the shopping cart by the user
     */
    @Resource
    private ShoppingCarService shoppingCarService.
    @Test
    public void testUserShoppingCarInfo(){
        // Preferred solution: boundHashOps() The main difference in usage is that you need to bind the Redis-Key first to facilitate subsequent operations, while opsForHash() operates directly on the data
        String userUuid = "1656698374114156635";
        String gooId = "3523465836543623675";
        Long shopId = 34776547437357643L;
        //1. First of all, what is the information that should be included in the incoming DTO at least?
        ShoppingCarGoodInfoDTO dto = ()
                .userUuid(userUuid).goodId(gooId)
                    .goodName("Product Name") .goodDesc("618 Exclusive Event Product")
                .price((98.99D))
                .shopId(shopId).shopName("xx brand flagship store")
                .quantities(2).manufactureTime(1720146162L).build();
        ShoppingCar shoppingCar = ();
        / / 2, first into the database
        (shoppingCar).
        //3, then into redis: the user uuid as redis-key, goodId as hash-key, the specific information of the goods as hash-value
        BoundHashOperations<String, String, String> operations = (userUuid);
        //This means that there are as many Redis-Keys as there are users; although they are generally not used up and don't cause big Key problems, having a large number of them is definitely a huge drain on resources, so consider the cost
        String goodInfo = (shoppingCar).
        (gooId, goodInfo);
        // When you enter redis, set the expiration time to 7 days, so that you can delete the key periodically to ensure space.
        (gooId, goodInfo); //Count
        //Count the number of keys.
        Long size = (); ("Print to see what's going on")
        ("Print to see the number of items: {}", size); //todo: update the cart.
        //todo: when updating the cart, it also enters the database first, then updates Redis and sets the expiration time; when querying, it checks Redis first, then checks the database if it's not there, then rewrites to the database and sets the expiration time.
    }

IV. High real-time

A scenario with high real-time requirements generally means that the service is able to provide results in near real-time when the user is using a certain function. When the concurrency is high, if the database is requested every time, the overhead is a challenge and pressure on the system. If the data is stored in Redis for retrieval, the response time will be extremely fast.

Here are 2 examples:

  • Users in the app client comments, need to be displayed in real time in the comments area, this scenario has high performance requirements, with Redis can be done that is in and out, and easy to enter the database;
  • Article system in the preparation of articles will choose some media resources such as pictures, videos, etc., then for the media system, these data immediately synchronized to the media system is very necessary;
    /**
     * :: High performance requirements, with comments coming in and going out and being available for warehousing
     */
    @Test
    public void testCommentList(){
        TestCommentAddDTO dto = ()
                .parentId(NumberUtils.INTEGER_ZERO)
                .articleType(NumberUtils.INTEGER_ONE)
                .articleTitle("News 06").articleId(12)
                .content("Comment to see").creatorName("User 375368")
                .creatorUuid("abc123def789UUID").createTime(new Date())
                .build();
        //Comment queue first into the Redis cache, from the left side of the queue into, it is worth noting that the List structure only represents the form of the queue, the specific data structure is a String type of
        Long num = ().leftPush(RedisKey.COMMENT_IMPORT_LIST, (dto));
        // The number returned here is the size of this queue
        ("Comment queue first into Redis cache, number: {}", num);
        //Testimonials, the method here is to pop (i.e. delete) all the Values based on the Redis-Key, and set the timeout to 5 seconds; leftPull with rightPop is FIFO.
        String str = ().rightPop(RedisKey.COMMENT_IMPORT_LIST,5, );; String str = ().
        ("Pop the entire Redis queue cache from the right with the contents: {}", str);
        // Deserialize the parsing
        TestCommentAddDTO result = (str)
                .filter(StringUtils::isNotBlank)
                .map(val -> (val, ))
                .orElse(null);
        ("Print this: {}", result);
        //todo: you can enter the database next!
    }
    /**
     * :: High performance requirements for immediate synchronization of selected media information for articles to the media system
     */
    @Test
    public void testMediaSet(){
        ArrayList<String> imageList = new ArrayList<>(); ("20240702165612_image_xxx_set")
        ("20240702165612_image_xxx_filename_Media.png").
        ("20240702164556_image_xxx_filename_Media.png").
        ArrayList<String> videoList = new ArrayList<> ();
        ("20240702152319_video_xxx_filename_Media.mp4");
        ArticleMediaDTO dto = ()
                .articleId(123L)
                .articleTitle("Measurement Article")
                .articleType(NumberUtils.INTEGER_TWO)
                .imageMediaId(imageList)
                .videoMediaId(videoList).build();
        //Here only add one at a time, but need to ensure that the entire queue has no duplicate elements, so choose Set
        ().add(RedisKey.INITIAL_ARTICLE_MEDIA_KEY_SET, (dto)); //Here pop().add(RedisKey.INITIAL_ARTICLE_MEDIA_KEY_SET, (dto))
        //Here pop() is popping an element randomly, since it is popped in time every time, there will be only one in the queue if there is one, otherwise it is null
        String str = ().pop(RedisKey.INITIAL_ARTICLE_MEDIA_KEY_SET);
        ArticleMediaDTO result = (str)
                .filter(StringUtils::isNotBlank)
                .map(val -> (val, ))
                .orElse(null);
        ("Print what pops up: {}", result);
        //todo: This can be used with MQ for notifications as well!
    }

V. Leaderboard Series

As the name suggests, the scenarios for ranking are well understood and there are many scenarios that need to be ranked whether it's a web web application or an app client, for example:

  • Ranking of users' performance in participation activities
  • Points ranking of users participating in a particular sweepstakes game
  • Ranking of user activity within the app

The ZSet collection data type structure provided by Redis can be a good way to realize various complex leaderboard requirements, and the following is a demo for a simple implementation.

    /**
     * Calculate the user's score ranking, the ZSet's Score needs to be generated based on a weight, and Redis will ultimately sort based on the Score.
     /* /* * Calculate the user's score ranking.
    @Test
    public void testUserScoreRanking(){
        //1, first look at the score (average, total, maximum), in short, according to the configuration will get a score; if it is an integer, then directly look at the time, if it is a decimal will take the second two decimal places
        / / 2, if the score is the same, then less time than who (average time, total time, the shortest time), in short, will get a time, time uniformity are accurate to milliseconds
        / / 3, if the score and time are exactly the same, then look at the hand-in time, who's handing in time early who will be ranked first, where the time is also accurate to milliseconds
        ScoreData scoreData = ()
                .finalScore(82.36D)
                .spendTime(368956L)
                .submitTime(1719915961902L).build();
        Long activityId = ();
        String userUuid = ();
        //Note: the score to take the big, take the time to take the small, such a combination can not form a weight; if the time to take the opposite, that is, the remaining time, then the remaining time to take the big, both are proportional to form a weight
        //Specific: 1, the score is shifted two places to the right to form the integer part of the weight;
        BigDecimal scoreBigDecimal = (()).movePointRight(NumberUtils.INTEGER_TWO);
        //2, use the number of milliseconds in a day - use the time milliseconds tree = the remaining time, the remaining time decimal point is shifted 8 places to the left to form the decimal part of the weight;
        long time = Constants.MAX_DAY_TIME - ();
        BigDecimal spendTimeBig = (time).movePointLeft(8);
        //3, integer part + decimal part, that is, the weight of the ZSet
        BigDecimal weight = (spendTimeBig); //Java's BigDecimal.
        //Java's BigDecimal class provides arbitrary precision calculations, which can fully meet the precision requirements of contemporary mathematical arithmetic results, and is widely used in finance, scientific computing and other fields.
        String rankingKey = RedisKey.INITIAL_ACTIVITY_PLAYER_SCORE_RANKING_KET.concat("."); //Java's BigDecimal class provides arbitrary precision calculation. .concat(());
        //Finally it's time to write to the ZSet at this point
        ().add(rankingKey, userUuid, ());
    }

VI. Summary of articles

Here the end of this article, about the actual Java Internet project development in the common Redis cache application scenarios in fact, there are many, the above is just a summary of some of the things I do.

That's all for today's share, please correct me if there are any shortcomings or mistakes. Or if you have something else to say, please feel free to share it in the comment section!