SpringBoot integrates Redis—cache penetration solution and actual sentinel mode

Table of contents

1. Environment preparation

1) pom.xml introduces Redis dependencies

2) Demonstrate business scenarios

2. SpringBoot integrates Redis stand-alone mode

1) Generate entity beans and data persistence layer through MyBatis reverse engineering

2) Configure redis connection information in application.yml

3) Start the redis service

4) XinTuProductRedisController class

5) Implementation of XinTuProductRedisService

6) Startup class SpringbootApplication

7) Start the SpringBootCase application and access the test

8) Open the Redis client

3. Cache penetration phenomenon

1) Penetration testing

XinTuRedisPenetrateController test class. 

2) Start the application, browser access test

3) Problems caused by

4) Solution

4. SpringBoot integrates Redis sentinel mode (one master, three slaves and three sentries)

5. Sentinel configuration

1) Verify master-slave data synchronization

2) Master node election


 

The following cases are still completed on the basis of the SpringBootCase project. (Redis uses Redis-x64-3.2.100 version)

1. Environment preparation

1) pom.xml introduces Redis dependencies

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

2) Demonstrate business scenarios

According to the function of the total number of goods, first search from the Redis cache, if not found, then search from the MySQL database, and then put the data into the Redis cache.

2. SpringBoot integrates Redis stand-alone mode

1) Generate entity beans and data persistence layer through MyBatis reverse engineering

Entity class:

package com.xintu.demo.entity;

import java.util.Date;

public class TProduct {
    private Integer id;

    private Integer categoryId;

    private String itemType;

    private String title;

    private String sellPoint;

    private String price;

    private Integer num;

    private String image;

    private Integer status;

    private Integer priority;

    private String createdUser;

    private Date createdTime;

    private String modifiedUser;

    private Date modifiedTime;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public Integer getCategoryId() {
        return categoryId;
    }

    public void setCategoryId(Integer categoryId) {
        this.categoryId = categoryId;
    }

    public String getItemType() {
        return itemType;
    }

    public void setItemType(String itemType) {
        this.itemType = itemType == null ? null : itemType.trim();
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title == null ? null : title.trim();
    }

    public String getSellPoint() {
        return sellPoint;
    }

    public void setSellPoint(String sellPoint) {
        this.sellPoint = sellPoint == null ? null : sellPoint.trim();
    }

    public String getPrice() {
        return price;
    }

    public void setPrice(String price) {
        this.price = price == null ? null : price.trim();
    }

    public Integer getNum() {
        return num;
    }

    public void setNum(Integer num) {
        this.num = num;
    }

    public String getImage() {
        return image;
    }

    public void setImage(String image) {
        this.image = image == null ? null : image.trim();
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public Integer getPriority() {
        return priority;
    }

    public void setPriority(Integer priority) {
        this.priority = priority;
    }

    public String getCreatedUser() {
        return createdUser;
    }

    public void setCreatedUser(String createdUser) {
        this.createdUser = createdUser == null ? null : createdUser.trim();
    }

    public Date getCreatedTime() {
        return createdTime;
    }

    public void setCreatedTime(Date createdTime) {
        this.createdTime = createdTime;
    }

    public String getModifiedUser() {
        return modifiedUser;
    }

    public void setModifiedUser(String modifiedUser) {
        this.modifiedUser = modifiedUser == null ? null : modifiedUser.trim();
    }

    public Date getModifiedTime() {
        return modifiedTime;
    }

    public void setModifiedTime(Date modifiedTime) {
        this.modifiedTime = modifiedTime;
    }
}

Data layer Mapper: 

package com.xintu.demo.mapper;

import com.xintu.demo.entity.TProduct;
import com.xintu.demo.entity.TProductExample;
import java.util.List;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper //方式一:添加@Mapper注解,等同于主类上加@MapperScan("com.demo.demo.mapper")
public interface TProductMapper {
    long countByExample(TProductExample example);

    int deleteByExample(TProductExample example);

    int deleteByPrimaryKey(Integer id);

    int insert(TProduct record);

    int insertSelective(TProduct record);

    List<TProduct> selectByExample(TProductExample example);

    TProduct selectByPrimaryKey(Integer id);

    int updateByExampleSelective(@Param("record") TProduct record, @Param("example") TProductExample example);

    int updateByExample(@Param("record") TProduct record, @Param("example") TProductExample example);

    int updateByPrimaryKeySelective(TProduct record);

    int updateByPrimaryKey(TProduct record);
}

2) Configure redis connection information in application.yml

The complete application.yml configuration file is as follows:

#spring:
#  profiles:
#      active: test #激活对应环境配置,以测试环境为例
server:
  port: 8888 # 设置内嵌Tomcat端口号
  servlet:
    context-path: /springbootcase # 设置项目上下文根路径,这个在请求访问的时候需要用到

test:
  site: 35xintu.com #测试站点
  user: xintu #测试用户

spring:
  datasource: # mysql相关配置
    url: jdbc:mysql://localhost:3306/xintu?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: xxx #根据自己的本地配置情况设置
  devtools:
    restart:
      enabled: true  #设置开启热部署
      additional-paths: src/main/java #重启目录
      exclude: WEB-INF/** #排除一些不需要自动重启的资源
      log-condition-evaluation-delta: false #关闭在什么情况下重启的日志记录,需要时可以打开

  thymeleaf:
    cache: false #使用Thymeleaf模板引擎,关闭缓存

  redis: #配置redis连接信息(单机模式)
    host: 192.168.92.134
    port: 6379
    password: #根据自己的本地配置情况设置


#在application.yml配置文件中指定映射文件的位置,这个配置只有接口和映射文件不在同一个包的情况下,才需要指定:
mybatis:
  mapper-locations: classpath:mapper/*.xml


3) Start the redis service

4) XinTuProductRedisController class

package com.xintu.demo.controller;

import com.xintu.demo.service.XinTuProductRedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

/**
 * @author XinTu
 * @classname XinTuProductRedisController
 * @description TODO
 * @date 2023年05月05日 5:21
 */

@RestController
public class XinTuProductRedisController {
    @Autowired
    private XinTuProductRedisService productRedisService;

    @GetMapping(value = "/productredis/allProductNumber")
    public String allProductNumber(HttpServletRequest request) {
        Long allProductNumber = productRedisService.allProduct();
        return "商品数量:" + allProductNumber;
    }
}

5) Implementation of XinTuProductRedisService

package com.xintu.demo.service;

import com.xintu.demo.entity.TProductExample;
import com.xintu.demo.mapper.TProductMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

/**
 * @author XinTu
 * @classname XinTuProductRedisService
 * @description TODO
 * @date 2023年05月05日 5:22
 */
@Service
public class XinTuProductRedisService {

    @Autowired
    private TProductMapper productMapper;

    // 注入 spring data当中的 RedisTemplate 类
    @Autowired
    private RedisTemplate redisTemplate;

    public Long allProduct() {
        //设置redisTemplate对象key的序列化方式
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //从redis缓存中获取总商品数
        Long productCount = (Long) redisTemplate.opsForValue().get("product_count");
        System.out.println("查询Redis数据库..."+productCount);
        //判断是否为空
        if (null == productCount) { //去mysql数据库查询,并存放到redis缓存中
            System.out.println("查询MySQL数据库...");
            TProductExample example = new TProductExample();
            productCount = productMapper.countByExample(example);
            redisTemplate.opsForValue().set("product_count",
                    productCount, 1, TimeUnit.SECONDS); // 会影响缓存穿透执行时长
        }
        return productCount;
    }

}

6) Startup class SpringbootApplication

package com.xintu.demo;

import com.xintu.demo.config.XinTuConfigInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@EnableTransactionManagement //开启事务
@RestController
@SpringBootApplication
public class SpringbootApplication {

	@Autowired
	private XinTuConfigInfo configInfo; //测试@ConfigurationProperties

	@Value("${test.site}")
	private String site;

	@Value("${test.user}")
	private String user;
	public static void main(String[] args) {
		SpringApplication.run(SpringbootApplication.class, args);
	}

	@GetMapping("/hello")
	public String hello(@RequestParam(value = "name", defaultValue = "World") String name) {
		return String.format("欢迎 %s 来到<a href=\"http://www.35xintu.com\">35新途</a>!", name);
	}

	@GetMapping("/value")
	public String testValue() { //测试 @Value 注解
		return String.format("欢迎 %s 来到<a href=\"http://www.35xintu.com\">%s</a>!" , user,site);
	}

	@GetMapping("/config")
	public String config() { //测试 @ConfigurationProperties 注解
		System.out.println("hello");
		return String.format("欢迎 %s 来到<a href=\"http://www.35xintu.com\">%s</a>!" , configInfo.getUser(),configInfo.getSite());
	}

}

7) Start the SpringBootCase application and access the test

http://localhost:8888/springbootcase/productredis/allProductNumber 

8) Open the Redis client

启动命令:redis-cli.exe

查询命令:get product_count

3. Cache penetration phenomenon

1) Penetration testing

XinTuRedisPenetrateController test class. 

package com.xintu.demo.controller;

import com.xintu.demo.service.XinTuProductRedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author XinTu
 * @classname XinTuRedisPenetrateController
 * @description 模拟缓存穿透
 * @date 2023年05月05日 6:00
 */

@RestController
public class XinTuRedisPenetrateController {
    @Autowired
    private XinTuProductRedisService productRedisService;

    @GetMapping(value = "/productredispenetrate/allProductNumber")
    public String allProductNumber(HttpServletRequest request) {
        Long allProductNumber = productRedisService.allProduct();
        //线程池个数,一般建议是CPU内核数 或者 CPU内核数据*2
        ExecutorService executorService = Executors.newFixedThreadPool(8);
        for (int i = 0; i < 2000; i++) {
            executorService.submit(new Runnable() {
                @Override public void run() {
                    productRedisService.allProduct();
                }
            });
        }
        return "商品数量:" + productRedisService.allProduct();
    }
}

2) Start the application, browser access test

3) Problems caused by

Multiple threads are querying the database. This phenomenon is called cache penetration. If the concurrency is relatively large, the pressure on the database is too great, which may cause the database to go down.

4) Solution

Solution 1: add synchronization lock

Modify the code in StudentServiceImpl

public Long allProduct() {
        //设置redisTemplate对象key的序列化方式
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //从redis缓存中获取总商品数
        Long productCount = (Long) redisTemplate.opsForValue().get("product_count");
        System.out.println("查询Redis数据库..."+productCount);
        //判断是否为空
        if (null == productCount) { //去mysql数据库查询,并存放到redis缓存中
            //设置同步代码块
            synchronized (this) { //加同步锁
                productCount = (Long) redisTemplate.opsForValue().get("product_count");
                if (null == productCount) { // 双重验证
                    System.out.println("查询MySQL数据库...");
                    TProductExample example = new TProductExample();
                    productCount = productMapper.countByExample(example);
                    redisTemplate.opsForValue().set("product_count",
                            productCount, 1, TimeUnit.SECONDS); // 会影响缓存穿透执行时长
                }
            }
        }
        return productCount;
}

Start the application, test the browser access, and view the console output. Only the first thread queries the database, and the other threads query the Redis cache. The small problem solved in this way is that the first batch of users who come in will have a wait, but the impact can be neglect.

① Why do two-layer verification?

To prevent the thread from obtaining the cpu execution permission, other threads have already put the data in Redis, so judge again;

The scope of synchronized cannot be expanded, because if there is data in the Redis cache, the threads should not be synchronized, otherwise the efficiency will be affected.

② Is adding synchronization lock the best solution?

However, in cluster mode, this method still has problems. At this time, it is necessary to consider the use of redis distributed locks. You can study the specific solutions by yourself.

4. SpringBoot integrates Redis sentinel mode (one master, three slaves and three sentries)

6379 is the master node, 6380 and 6381 are slave nodes.

Modify the port numbers in each redis.windows.conf and redis.windows-service.conf to: 6379 (main node remains unchanged), 6380, 6381.

The slave node configuration file needs to be added:

slaveof localhost 6379

From the overall configuration file of the node:

# 端口配置
port 6380
# 日志文件名
logfile "redis_log_6380.log"
# rdb持久化文件名字
dbfilename "dump6380.rdb"
# 本地ip
bind 127.0.0.1
# 绑定主从关系【该设置说明端口6380的服务为从机,它的主机为:6379】

# 从机是否只能读 默认是yes
slave-read-only no

4. Start three Redis servers separately

The master node starts,

start from the node,

Verify the master node,

Slave node authentication,

5. Sentinel configuration

#Sentry mode redis cluster configuration (sentinel mode)

  redis: #配置redis连接信息(单机模式)
    host: localhost
#    port: 6379 #f哨兵模式下不要写端口号
#    password: 123456
    sentinel:  #哨兵模式redis集群配置(哨兵模式)
      master: mymaster #与哨兵中的sentinel monitor xxx 保持一致
      nodes: localhost:26379,localhost:26380,localhost:26381

Sentinel can directly copy the Redis file directory. Pharaoh here is divided into Redis-Sentinel-26379, Redis-Sentinel-26380, Redis-Sentinel-26381.

Add the sentinel.conf file to the three sentinel nodes respectively, and the content of the file is as follows:

# 哨兵sentinel实例运行的端口 默认26379
port 26379

# 保护模式
protected-mode no

# 本地ip
bind 127.0.0.1

# 哨兵监听的主服务器 后面的1表示主机挂掉以后进行投票,只需要2票就可以从机变主机
sentinel myid 9c65a6f7aad9e2419a6abce1ce56ff28cb81df34

# 设置主机的密码(无密码可以省略)
# sentinel auth-pass mymaster 35xintu

# 设置未得到主机响应时间,此处代表5秒未响应视为宕机
sentinel monitor mymaster 127.0.0.1 6380 2

# 设置等待主机活动时间,此处代表15秒主机未活动,则重新选举主机
sentinel down-after-milliseconds mymaster 5000

# 设置重新选举主机后,同一时间同步数据的从机数量,此处代表重新选举主机后,每次2台从机同步主机数据,直到所有从机同步结束
sentinel failover-timeout mymaster 15000

Now we start 3 Sentinels.

Note that when starting the redis active/standby cluster, you must first start the master, then start the slave, and the sentinel can be started first.

Start sentinel command: redis-server.exe sentinel.conf --sentinel 

After the points are started successfully, the test can be carried out.

1) Verify master-slave data synchronization

Client connection command: redis-cli.exe -h 127.0.0.1 -p 6380.

2) Master node election

Stop the master node:

Verify that the master node is down,

In sentry mode, re-election is carried out,

Then look at the SpringBoot console, the page is switched to 6380. 

At this point, it means that the sentinel mode has taken effect. The principle part of the master-slave replication and sentinel mechanism will be analyzed in detail in the following redis-related courses. This article focuses on the actual combat of SpringBoot integrated Redis.

above! You can follow and continue to output high-quality content!

Guess you like

Origin blog.csdn.net/wangyongfei5000/article/details/130539589