[訴訟] SpringBoot は Redis を統合してキャッシュされたページング データ クエリを実現します

この記事を正式に読む前に、同時実行性が高い場合にホームページのリスト データをどのように処理するかという質問を想像してください。

ここに画像の説明を挿入

タオバオのホームページと同様に、これらの商品はデータベースからチェックアウトされますか? 答えは決してそうではありません。同時実行性が高い場合、データベースはそれを処理できません。では、C 側で大量の同時実行性をどのように処理すればよいでしょうか?これには Redis を使用できます。Redis はメモリベースであることがわかっています。 NoSQLデータベース。オペレーティング システムを学習すると、メモリはディスクよりもはるかに効率的であることがわかります。そのため、Redis はメモリに基づいており、データベースはディスクに基づいています。

Tmall Juhuasuanに類似した商品のリストもあります。

ここに画像の説明を挿入

ホームページ データのページネーションには Redis を使用する必要があることがわかりました。そのためには Redis のデータ構造を使用する必要があります。

Redis には 5 つの基本的なデータ構造があります。ここではページネーションにリスト型を使用します。

Redis では、List (リスト) 型は、要素の挿入順序に従ってソートされた文字列のリストです。新しい要素をリストの先頭 (左) または末尾 (右) に追加できます。

それでは、ケースを使用して、ホームページ上のホット データをクエリのために Redis に配置する方法を練習しましょう。

SpringBoot は RedisTemplate を統合しますが、ここではあまり紹介しませんが、統合するためのオンラインのブログ投稿を見つけることができます。

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

ProductService を記述し、データ ページング メソッドを設定します。

public interface ProductService {
    
    

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

}

ProductServiceImpl 実装クラスを作成します。

/**
 * @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;
    }
}

はい、コントローラーを作成してテストします。

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

初めてアクセスするときは、まず Redis にアクセスしてクエリを実行し、存在しないことを確認してから DB を確認し、キャッシュするデータ ページを Redis に置きます。
ここに画像の説明を挿入
ここに画像の説明を挿入
ここに画像の説明を挿入

2回目の訪問時。Redis に直接アクセスしてください

ここに画像の説明を挿入
ここに画像の説明を挿入
ここに画像の説明を挿入

Redis と DB クエリを比較した結果、Redis からの取得には 18ms しかかからず、パブリック DB からの取得には 300ms かかることがわかり、Redis の強みの 1 つが示されました。

次に、クエリ ロジックに問題があるかどうかを観察してみましょう。

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

想像してみてください。ある時点で Redis のキャッシュに障害が発生し、大量のリクエストがすべて DB 上で見つかった場合、これも大惨事になります。したがって、これにはすぐにキャッシュの故障の問題が伴います。

キャッシュの内訳を解決する

  • オプション 1: 期限切れにしない
    • ホットスポット データは事前​​に期限切れになるように設定されておらず、キャッシュはバックグラウンドで非同期に更新されます。
  • 解決策 2: ミューテックスまたはキューを追加する
    • 実際、キャッシュペネトレーションはキャッシュペネトレーションと似ていると理解しているので、ミューテックスを追加し、1つのスレッドが通常どおりデータベースをリクエストできるようにし、他のスレッドは待機できます(ここではスレッドプールを使用して処理できます)。キャッシュを作成した後、他のスレッドのリクエストはキャッシュするだけです。

ここでは、キーが期限切れにならないように、最初の方法を採用します。

それでは、これは非常に簡単で、キーを定期的に更新するスケジュールされたタスクを設定するだけだ、と言う人もいるかもしれません。そこで、次のようなタイミングジョブのコードを書きました。

// 定时任务,每隔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)
}

ただし、スケジュールされたタスクのコードを追加しても、キャッシュ破壊の問題が発生することに気づいたかどうかはわかりません。古いデータの削除と新しいデータの保存という 2 つのコマンドは非アトミック操作であるため、時間間隔が生じます。代わりに文字列構造のストレージを使用する場合は、古い値を直接上書きでき、原子性の問題はありませんが、ビジネス要件ではページングをサポートする必要があるため、使用できるのはリスト構造のみです。

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

解決

  • ビジネス アーキテクチャはデータのダウングレードとポケット化に重点を置いているため、キャッシュの内訳もこのソリューションを考慮できるため、時間の余地があります。

  • データの 2 つのコピーがキャッシュされます。1 つはリスト構造 (最初に削除してから新しい値を設定)、もう 1 つは文字列構造 (古い値を直接上書き) です。

    • クエリを実行する場合、最初にリスト構造がクエリされます。そうでない場合は、String 構造がリストに解析され、メモリ ページングが実行されます。一般に、データ量は大きくありません。
// 定时任务,每隔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)
}

クエリを実行する場合は、まずリスト構造を確認し、リスト構造にデータがない場合は String 型のデータを確認します。

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、これで記事全体のケース統合は終わりです。ブロガーがうまく書いている場合は、3 つのリンクを付けることを忘れないでください。

ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/weixin_47533244/article/details/131296554