[Case combat] SpringBoot integrates Redis to realize cached paging data query

Before officially reading this article, imagine a question, how to do the home page list data in the case of high concurrency?

insert image description here

Similar to the Taobao homepage, are these products checked out from the database? The answer is definitely not. In the case of high concurrency, the database cannot handle it, so how do we handle the large amount of concurrency on the C side? We can use Redis for this. We know that Redis is a memory-based NoSQL database. We all know that after learning the operating system, memory is much more efficient than disk, so Redis is based on memory, while the database is based on disk.

There is also a list of products similar to Tmall Juhuasuan.

insert image description here

We now know that we need to use Redis to paginate the home page data, so we should use the data structure of Redis to do it.

Redis has 5 basic data structures, here we use the list type for pagination.

In Redis, the List (list) type is a list of strings sorted according to the insertion order of the elements. You can add new elements to the head (left) or tail (right) of the list.

Ok, then let's use a case to practice how to put the hot data on the home page into Redis for query.

SpringBoot integrates RedisTemplate and I won’t introduce too much here. You can find a blog post online to integrate it.

<!-- 创建SpringBoot项目加入redis的starter依赖 -->
<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Write ProductService, set data paging method.

public interface ProductService {
    
    

    Map<String,Object> productListPage(int current, int size) throws InterruptedException;

}

Write the ProductServiceImpl implementation class.

/**
 * @author lixiang
 * @date 2023/6/18 21:01
 */
@Service
@Slf4j
public class ProductServiceImpl implements ProductService {
    
    

    private static final String PRODUCT_LIST_KEY = "product:list";

    private static final List<Product> PRODUCT_LIST;

    //模拟从数据库中查出来的数据
    static {
    
    
        PRODUCT_LIST = new ArrayList<>();
        for (int i = 1; i <= 100; i++) {
    
    
            Product product = new Product();
            product.setId(UUID.randomUUID().toString().replace("-", ""));
            product.setName("商品名称:" + i);
            product.setDesc("商品描述:" + i);
            product.setPrice(new BigDecimal(i));
            product.setInventory(2);
            PRODUCT_LIST.add(product);
        }
    }

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public Map<String, Object> productListPage(int current, int size) throws InterruptedException {
    
    

        //从缓存中拿到分页数据
        List<Product> productList = getProductListByRedis(current, size);

        if (productList == null || productList.size() == 0) {
    
    
            log.info("当前缓存中无分页数据,当前页:" + current + ",页大小:" + size);
            //从数据库中拿到分页数据
            productList = getProductListByDataSource(current, size);
        }
        Map<String, Object> resultMap = new HashMap<>();
        //计算当前总页数
        int totalPage = (PRODUCT_LIST.size() + size - 1) / size;
        resultMap.put("total", PRODUCT_LIST.size());
        resultMap.put("data", productList);
        resultMap.put("pages", totalPage);
        return resultMap;
    }

    private List<Product> getProductListByRedis(int current, int size) {
    
    
        log.info("从Redis取出商品信息列表,当前页:" + current + ",页大小:" + size);
        // 计算总页数
        int pages = pages(size);
        // 起始位置
        int start = current <= 0 ? 0 : (current > pages ? (pages - 1) * size : (current - 1) * size);
        // 终止位置
        int end = start+size-1;
        List<Product> list = redisTemplate.opsForList().range(PRODUCT_LIST_KEY, start, end);
        List<Product> productList = list;
        return productList;
    }

    /**
     * 获取商品信息集合
     *
     * @return
     */
    private List<Product> getProductListByDataSource(int current, int size) throws InterruptedException {
    
    
        //模拟从DB查询需要300ms
        Thread.sleep(300);
        log.info("从数据库取出商品信息列表,当前页:" + current + ",页大小:" + size);
        // 计算总页数
        int pages = pages(size);
        // 起始位置
        int start = current <= 0 ? 0 : (current > pages ? (pages - 1) * size : (current - 1) * size);
        //数据缓存到redis中
        redisTemplate.opsForList().rightPushAll(PRODUCT_LIST_KEY, PRODUCT_LIST);
        //设置当前key过期时间为1个小时
        redisTemplate.expire(PRODUCT_LIST_KEY,1000*60*60, TimeUnit.MILLISECONDS);
        return PRODUCT_LIST.stream().skip(start).limit(size).collect(Collectors.toList());
    }

    /**
     *  获取总页数
     * @param size
     * @return
     */
    private Integer pages(int size){
    
    
        int pages = PRODUCT_LIST.size() % size == 0 ? PRODUCT_LIST.size() / size : PRODUCT_LIST.size() / size + 1;
        return pages;
    }
}

ok, then write the controller and test it.

@RestController
@RequestMapping("/api/v1/product")
public class ProductController {
    
    

    @Autowired
    private ProductService productService;

    @GetMapping("/page")
    public Map<String,Object> page(@RequestParam("current") int current,@RequestParam("size") int size){
    
    
        Map<String, Object> stringObjectMap;
        try {
    
    
            stringObjectMap = productService.productListPage(current, size);
        } catch (InterruptedException e) {
    
    
            stringObjectMap = new HashMap<>();
        }
        return stringObjectMap;
    }
}

When accessing for the first time, first go to Redis to query and find that there is no, then check the DB and put the data page to be cached in Redis.
insert image description here
insert image description here
insert image description here

When visiting for the second time. Just visit Redis directly

insert image description here
insert image description here
insert image description here

Through the comparison of Redis and DB query, we found that it only takes 18ms to get it from Redis, and it takes 300ms to get it from the public DB, which shows one of the strengths of Redis.

Then let's observe the query logic, will there be any problems.

    public Map<String, Object> productListPage(int current, int size) throws InterruptedException {
    
    

        //从缓存中拿到分页数据
        List<Product> productList = getProductListByRedis(current, size);

        if (productList == null || productList.size() == 0) {
    
    
            log.info("当前缓存中无分页数据,当前页:" + current + ",页大小:" + size);
            //从数据库中拿到分页数据
            productList = getProductListByDataSource(current, size);
        }
    }

Imagine, if at a certain moment, the cache in Redis fails, and a large number of requests are all found on the DB, it will also bring a disaster. So this soon involves a problem of cache breakdown .

Solve cache breakdown

  • Option 1: never expire
    • The hotspot data is not set to expire in advance, and the cache is updated asynchronously in the background.
  • Solution 2: add mutex or queue
    • In fact, I understand that cache penetration is similar to cache penetration, so add a mutex, let one thread request the database normally, and other threads can wait (here you can use the thread pool to process), after creating the cache, let other threads request Just cache it.

Here we adopt the first method, so that the key will never expire.

Then some people may say, this is very simple, then I just set up a scheduled task to refresh the key regularly. So I wrote the following timing job code.

// 定时任务,每隔30分钟,从数据库中读取商品列表,存储到缓存里面
priviate static final String PRODUCT_LIST_KEY = "product:list";

@Scheduled(cron = "0 */30 * * * ?")
public void loadActivityProduct() {
    
    
  //从数据库中查询参加活动的商品列表
  List<Product> productList = productMapper.queryAcitvityProductList();
  //删除旧的
  redisTemplate.delete(PRODUCT_LIST_KEY);
  //存储新的
  redis.opsForList.leftPushAll(PRODUCT_LIST_KEY, productList)
}

However, I don’t know if you have noticed that even if we add the code of the scheduled task, the problem of cache breakdown will occur. Because the two commands of deleting old data and storing new data are non-atomic operations, there is a time interval. If you use the string structure for storage, you can directly overwrite the old value, and there is no atomicity problem, but the business requirement needs to support paging, so you can only use the list structure.

	//就在我删除旧的key的时候,这会还没有往redis中放入,大的并发量进来导致请求都跑到了数据库上,造成缓存击穿。
	//删除旧的
  redisTemplate.delete(PRODUCT_LIST_KEY);
  //存储新的
  redis.opsForList.leftPushAll(PRODUCT_LIST_KEY, productList)

solution

  • The business architecture emphasizes downgrading and pocketing data, so can cache breakdown also consider this solution, space for time

  • Two copies of data are cached, one is a List structure (delete first, then set a new value), and the other is a String structure (directly overwrite the old value)

    • When querying, the list structure is firstly queried. If not, the String structure is parsed into a list, and memory paging is performed. Generally, the amount of data is not large.
// 定时任务,每隔30分钟,从数据库中读取商品列表,存储到缓存里面
priviate static final String PRODUCT_LIST_KEY = "product:list";
priviate static final String PRODUCT_LIST_KEY_STR = "product:liststr";

@Scheduled(cron = "0 */30 * * * ?")
public void loadActivityProduct() {
    
    
  //从数据库中查询参加活动的商品列表
  List<Product> productList = productMapper.queryAcitvityProductList();
  
  //先缓存一份String类型的数据,直接set,如果要分页则解析成list再返回
  redis.opsForValue.set(PRODUCT_LIST_KEY_STR, JSON.toString(productList))
  
  //删除旧的
  redisTemplate.delete(PRODUCT_LIST_KEY);
  //存储新的
  redis.opsForList.leftPushAll(PRODUCT_LIST_KEY, productList)
}

When querying, first check the list structure. If there is no data in the list structure, check the data of String type.

priviate static final String PRODUCT_LIST_KEY = "product:list";
priviate static final String PRODUCT_LIST_KEY_STR = "product:liststr";

// 将商品列表从 Redis 缓存中读取
public List<Product> getProductListFromCache(int begin, int end) {
    
    
    List<Product> list = new ArrayList();

    //从缓存里分页获取
    list = redisTemplate.opsForList().range(PRODUCT_LIST_KEY, begin,end)
    if (productListStr != null) {
    
    
      return list;
    } else {
    
    
        // 缓存A中不存在商品列表,则从缓存B读取
        String productStrList = redis.opsForValue.get(PRODUCT_LIST_KEY_STR);
        // 缓存中存在商品列表,将 JSON 字符串转换为对象
        List<Product> productList = JSON.parseArray(productStrList, Product.class);
        //分页计算
        list = CommonUtil.pageList(productList,begin, end);
       return list;
    }
}

OK, this is the end of the case integration for the whole article. If the blogger writes well, remember to give it a three-link! ! !

insert image description here

Guess you like

Origin blog.csdn.net/weixin_47533244/article/details/131296554