精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

3s → 30ms!SpringBoot樹形結構“開掛”實錄:一次查詢提速100倍

開發 前端
產品經理拿著電腦跑過來的時候,我仿佛看到了他頭頂的烏云:“這要是上線給客戶用,客戶不得以為咱們系統是用算盤寫的?

兄弟們,今天跟大家聊個咱們后端開發繞不開的坑 —— 樹形結構查詢。別慌,不是來勸退的,是來分享我上周剛踩完坑、把查詢耗時從 3 秒干到 30 毫秒的 “開掛” 經歷,相當于給系統裝了個火箭推進器,看完保準你也能抄作業。

先跟大家還原下當時的 “災難現場”:公司最近在做一個權限管理系統,里面的菜單結構是典型的樹形 —— 一級菜單下面掛二級,二級下面還有三級,像棵倒過來的圣誕樹。一開始測試環境數據少,沒覺得有啥問題,結果上周灰度發布給運營部門用,好家伙,運營同學點 “菜單管理” 按鈕,咖啡都沖好了,頁面還在那兒轉圈圈,控制臺一看,接口響應時間:3021ms!

產品經理拿著電腦跑過來的時候,我仿佛看到了他頭頂的烏云:“這要是上線給客戶用,客戶不得以為咱們系統是用算盤寫的?” 得,加班是跑不了了,接下來就是我跟這個樹形結構死磕的三天,最終把耗時壓到了 30ms 以內。下面就把整個優化過程拆解開,用大白話跟大家嘮明白,每個步驟都帶實操代碼,小白也能看懂。

一、先搞懂:為啥樹形結構查詢這么 “慢”?

在說優化之前,咱們得先弄明白一個事兒:樹形結構到底難在哪兒?平時咱們查個列表,select * from table where id = ? 一下就出來了,為啥到樹形這兒就卡殼了?

其實核心問題就一個:樹形結構是 “父子嵌套” 的,而數據庫是 “平面存儲” 的。就像你把一棵大樹砍成一節節的木頭堆在地上,想重新拼成樹,就得知道每節木頭的爹是誰、兒子是誰,這就需要不斷 “找關系”。

咱們先看看最初的 “爛代碼” 是咋寫的 —— 當時圖省事,直接用了遞歸查數據庫,代碼長這樣:

// 最初的爛代碼:遞歸查詢數據庫
@Service
public class MenuServiceImpl implements MenuService {
    @Autowired
    private MenuMapper menuMapper;
    // 獲取所有菜單樹形結構
    @Override
    public List<MenuVO> getMenuTree() {
        // 1. 先查一級菜單(parent_id = 0)
        List<MenuDO> rootMenus = menuMapper.selectByParentId(0);
        // 2. 遞歸給每個一級菜單查子菜單
        return rootMenus.stream().map(this::buildMenuTree).collect(Collectors.toList());
    }
    // 遞歸構建子菜單
    private MenuVO buildMenuTree(MenuDO parentMenu) {
        MenuVO menuVO = new MenuVO();
        BeanUtils.copyProperties(parentMenu, menuVO);
        
        // 致命操作:每次遞歸都查一次數據庫!
        List<MenuDO> childMenus = menuMapper.selectByParentId(parentMenu.getId());
        if (!childMenus.isEmpty()) {
            menuVO.setChildren(childMenus.stream().map(this::buildMenuTree).collect(Collectors.toList()));
        }
        return menuVO;
    }
}

現在回頭看這段代碼,我自己都想抽自己兩嘴巴子。當時覺得 “遞歸多優雅啊”,結果忽略了一個致命問題:每遞歸一次,就查一次數據庫!咱們算筆賬:如果一級菜單有 5 個,每個一級菜單下面有 10 個二級菜單,每個二級下面又有 10 個三級菜單,那總共要查多少次數據庫?1(查一級)+5(查二級)+5*10(查三級)=56 次!這還只是菜單不多的情況,要是菜單層級再多、數量再大,數據庫直接就被這輪番查詢 “干懵了”,響應時間能不高嗎?

而且數據庫的 “IO 操作” 本身就是個慢家伙 —— 內存操作是毫秒級甚至微秒級,而數據庫查詢要走網絡、要讀磁盤,一次查詢幾百毫秒,幾十次疊加下來,3 秒真不算夸張。

二、第一波優化:把數據庫 “拉黑名單”,內存里拼樹!

既然問題出在 “頻繁查數據庫”,那解決思路就很明確:先把所有數據一次性從數據庫撈出來,再在內存里拼樹形結構。就像你要拼樂高,先把所有零件都倒在桌子上,再慢慢拼,總比拼一步去抽屜里拿一次零件快吧?

說干就干,咱們先改 Service 層代碼,核心就兩步:1. 一次性查全所有菜單數據;2. 用內存遞歸(或者循環)拼出樹形結構。

第一步:改寫 Mapper,查全所有數據

先給 MenuMapper 加個查詢所有菜單的方法,就一句 SQL:

// MenuMapper.java
public interface MenuMapper {
    // 原來的根據parentId查
    List<MenuDO> selectByParentId(Long parentId);
    
    // 新增:查所有菜單
    List<MenuDO> selectAllMenus();
}

對應的 XML 也簡單,不用加任何條件:

<!-- MenuMapper.xml -->
<select id="selectAllMenus" resultType="com.example.demo.entity.MenuDO">
    select id, parent_id, menu_name, menu_url, icon, sort from sys_menu
</select>

第二步:內存里拼樹形結構

這一步是關鍵,咱們要把查出來的所有菜單,在內存里按 parentId 的關系組裝成樹。這里有兩種方式:遞歸和循環,遞歸寫起來簡單,但如果菜單層級特別深(比如超過 100 層),可能會棧溢出,所以我這里用循環的方式,更穩妥。

先定義個工具類,專門用來組裝樹形結構,以后其他樹形需求也能復用:

// 樹形結構組裝工具類
public class TreeUtils {
    /**
     * 組裝樹形結構
     * @param allNodes 所有節點列表
     * @param rootParentId 根節點的parentId(這里菜單根節點是0)
     * @return 組裝好的樹形結構
     */
    public static <T extends TreeNode> List<T> buildTree(List<T> allNodes, Long rootParentId) {
        // 1. 先把所有節點存到Map里,key是節點ID,value是節點對象,方便快速查找
        Map<Long, T> nodeMap = new HashMap<>();
        for (T node : allNodes) {
            nodeMap.put(node.getId(), node);
        }
        // 2. 遍歷所有節點,給每個節點找爸爸,把自己加到爸爸的children里
        List<T> rootNodes = new ArrayList<>();
        for (T node : allNodes) {
            Long parentId = node.getParentId();
            // 如果是根節點,直接加入根節點列表
            if (rootParentId.equals(parentId)) {
                rootNodes.add(node);
                continue;
            }
            // 不是根節點,找自己的父節點
            T parentNode = nodeMap.get(parentId);
            if (parentNode != null) {
                // 父節點的children如果為空,初始化一下
                if (parentNode.getChildren() == null) {
                    parentNode.setChildren(new ArrayList<>());
                }
                // 把當前節點加到父節點的children里
                parentNode.getChildren().add(node);
            }
        }
        return rootNodes;
    }
}
// 注意:這里需要一個TreeNode接口,讓MenuVO實現,統一規范
public interface TreeNode {
    Long getId();
    Long getParentId();
    List<? extends TreeNode> getChildren();
    void setChildren(List<? extends TreeNode> children);
}
// MenuVO實現TreeNode接口
public class MenuVO implements TreeNode {
    private Long id;
    private Long parentId;
    private String menuName;
    private String menuUrl;
    private String icon;
    private Integer sort;
    // 子菜單列表
    private List<MenuVO> children;
    //  getter和setter省略...
    // 實現TreeNode接口的方法
    @Override
    public Long getId() {
        return this.id;
    }
    @Override
    public Long getParentId() {
        return this.parentId;
    }
    @Override
    public List<? extends TreeNode> getChildren() {
        return this.children;
    }
    @Override
    public void setChildren(List<? extends TreeNode> children) {
        this.children = (List<MenuVO>) children;
    }
}

然后改寫 Service 層,用這個工具類來組裝樹形:

@Service
public class MenuServiceImpl implements MenuService {
    @Autowired
    private MenuMapper menuMapper;
    @Override
    public List<MenuVO> getMenuTree() {
        // 1. 一次性查全所有菜單數據(只查一次數據庫!)
        List<MenuDO> allMenus = menuMapper.selectAllMenus();
        // 2. 把MenuDO轉成MenuVO(DO是數據庫實體,VO是返回給前端的視圖對象,解耦)
        List<MenuVO> allMenuVOs = allMenus.stream().map(menuDO -> {
            MenuVO menuVO = new MenuVO();
            BeanUtils.copyProperties(menuDO, menuVO);
            return menuVO;
        }).collect(Collectors.toList());
        // 3. 用工具類組裝樹形結構,根節點parentId是0
        return TreeUtils.buildTree(allMenuVOs, 0L);
    }
}

改完之后,咱們測一下耗時 —— 原來的 3 秒直接降到了 300ms 左右!足足快了 10 倍!這一步的核心就是減少數據庫 IO,把最耗時的 “多次查庫” 變成 “一次查庫”,剩下的操作都在內存里完成,速度自然就上來了。不過別著急慶祝,300ms 雖然比 3 秒好太多,但離 “優秀” 還有距離。產品經理雖然不催了,但我自己知道,還能再優化!

三、第二波優化:給數據 “裝個緩存”,直接從內存讀!

咱們再想想,300ms 的耗時主要花在哪兒了?雖然只查一次數據庫,但數據庫查詢還是要走網絡、讀磁盤,比如這次查所有菜單,數據庫可能要花 200ms 左右,剩下的 100ms 是內存組裝的時間。那能不能把 “查數據庫 + 內存組裝” 的結果直接存起來,下次要的時候直接拿?

當然可以!這就是咱們后端開發的 “萬金油”——緩存。這里我用 Redis 做緩存,因為 Redis 是內存數據庫,查數據比 MySQL 快好幾個數量級,而且 SpringBoot 整合 Redis 也特別簡單。

第一步:整合 Redis 依賴

先在 pom.xml 里加 Redis 的依賴,SpringBoot 有現成的 starter:

<!-- SpringBoot Redis依賴 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 阿里巴巴的FastJSON,用來序列化對象 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.32</version>
</dependency>

然后在 application.yml 里配置 Redis:

spring:
  redis:
    host: 127.0.0.1  # 你的Redis地址
    port: 6379       # 端口
    password: 123456 # 密碼(沒設的話可以不寫)
    database: 0      # 數據庫索引,默認0
    timeout: 3000ms  # 連接超時時間
    lettuce:
      pool:
        max-active: 8  # 最大連接數
        max-idle: 8    # 最大空閑連接
        min-idle: 2    # 最小空閑連接

第二步:配置 Redis 序列化

Redis 默認的序列化方式是 JDK 序列化,會把對象轉成一堆亂碼,而且占空間大,所以咱們用 FastJSON 來序列化,這樣存到 Redis 里的是 JSON 字符串,可讀性高,也省空間。

寫個 RedisConfig 配置類:

@Configuration
@EnableCaching // 開啟緩存支持
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // 配置FastJSON序列化器
        GenericFastJsonRedisSerializer fastJsonSerializer = new GenericFastJsonRedisSerializer();
        // key用String序列化
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        // value用FastJSON序列化
        redisTemplate.setValueSerializer(fastJsonSerializer);
        redisTemplate.setHashValueSerializer(fastJsonSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
    // 配置緩存管理器,設置默認緩存過期時間
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                // 緩存過期時間:30分鐘(根據業務調整,菜單不常變,設長點沒問題)
                .entryTtl(Duration.ofMinutes(30))
                // 序列化方式
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer()))
                // 允許為空
                .disableCachingNullValues();
        // 初始化緩存管理器
        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(config)
                .build();
    }
}

第三步:給 Service 方法加緩存

這一步最簡單,只需要在 getMenuTree 方法上加個@Cacheable注解,指定緩存的 key,就能自動把方法的返回結果存到 Redis 里,下次調用的時候直接從緩存拿,不執行方法體了。

@Service
publicclass MenuServiceImpl implements MenuService {

    @Autowired
    private MenuMapper menuMapper;

    // value:緩存的名稱,key:緩存的鍵(這里用"menu:tree",好識別)
    @Cacheable(value = "menuCache", key = "'menu:tree'", unless = "#result == null")
    @Override
    public List<MenuVO> getMenuTree() {
        // 下面的代碼跟之前一樣,但是只有第一次會執行,之后從緩存拿
        List<MenuDO> allMenus = menuMapper.selectAllMenus();
        List<MenuVO> allMenuVOs = allMenus.stream().map(menuDO -> {
            MenuVO menuVO = new MenuVO();
            BeanUtils.copyProperties(menuDO, menuVO);
            return menuVO;
        }).collect(Collectors.toList());

        return TreeUtils.buildTree(allMenuVOs, 0L);
    }
}

加完緩存之后,咱們再測一次耗時 —— 第一次調用的時候,還是 300ms 左右(因為要查庫、組裝、存緩存),第二次調用直接跳到了30ms 以內!有時候甚至能到 10 幾 ms!這就是緩存的威力,直接把 “查庫 + 組裝” 的步驟跳過了,從 Redis 里拿現成的 JSON 字符串,反序列化成對象就返回,速度能不快嗎?不過這里有個坑要跟大家說一下:緩存更新。如果菜單數據改了(比如新增、刪除、修改菜單),緩存里的數據就會變成 “臟數據”,前端看到的還是舊的菜單。所以咱們得在修改菜單的方法里,把緩存清掉,讓下次查詢重新走查庫 + 組裝的流程,更新緩存。

比如新增菜單的方法:

@Service
publicclass MenuServiceImpl implements MenuService {

    @Autowired
    private MenuMapper menuMapper;

    // 新增菜單:加@CacheEvict,清除緩存
    @CacheEvict(value = "menuCache", key = "'menu:tree'")
    @Override
    public void addMenu(MenuDTO menuDTO) {
        MenuDO menuDO = new MenuDO();
        BeanUtils.copyProperties(menuDTO, menuDO);
        menuMapper.insert(menuDO);
    }

    // 修改菜單:同樣清除緩存
    @CacheEvict(value = "menuCache", key = "'menu:tree'")
    @Override
    public void updateMenu(MenuDTO menuDTO) {
        MenuDO menuDO = new MenuDO();
        BeanUtils.copyProperties(menuDTO, menuDO);
        menuMapper.updateById(menuDO);
    }

    // 刪除菜單:也清除緩存
    @CacheEvict(value = "menuCache", key = "'menu:tree'")
    @Override
    public void deleteMenu(Long id) {
        menuMapper.deleteById(id);
    }

    // getMenuTree方法不變...
}

@CacheEvict注解的作用就是 “清除緩存”,當執行 add、update、delete 方法的時候,會自動把 key 為 “menu:tree” 的緩存刪掉,下次調用 getMenuTree 的時候,就會重新查庫、組裝、存新的緩存,這樣數據就不會臟了。

四、進階優化:數據庫也能 “拼樹”,CTE 了解一下?

到這里,30ms 的耗時已經完全滿足業務需求了,但作為一個有追求的 Java 程序員,咱們得知道還有沒有其他玩法。比如,能不能讓數據庫直接返回樹形結構,不用在 Java 代碼里組裝?

答案是可以的,用數據庫的CTE(公共表表達式) ,也就是 “遞歸查詢”。不同數據庫的語法有點不一樣,我這里用 MySQL 8.0 為例(MySQL 5.7 及以下不支持 CTE,得用自連接,麻煩一點)。

用 CTE 在數據庫層面查樹形結構

先寫個 CTE 的 SQL,直接查詢出所有菜單的樹形結構,包括層級、父菜單名稱這些信息:

WITH RECURSIVE menu_tree AS (
    -- 1. 錨點成員:查詢根節點(parent_id = 0)
    SELECT
        id, 
        parent_id, 
        menu_name, 
        menu_url, 
        icon, 
        sort, 
        1ASlevel, -- 層級,根節點是1級
        menu_name AS full_name -- 完整名稱,根節點就是自己的名稱
    FROM sys_menu 
    WHERE parent_id = 0

    UNION ALL

    -- 2. 遞歸成員:查詢子節點,跟錨點成員關聯
    SELECT
        m.id, 
        m.parent_id, 
        m.menu_name, 
        m.menu_url, 
        m.icon, 
        m.sort, 
        mt.level + 1ASlevel, -- 子節點層級比父節點+1
        CONCAT(mt.full_name, ' > ', m.menu_name) AS full_name -- 完整名稱拼接父節點名稱
    FROM sys_menu m
    INNERJOIN menu_tree mt ON m.parent_id = mt.id -- 關聯父節點
)
-- 3. 查詢遞歸結果
SELECT * FROM menu_tree ORDERBYlevel, sort;

這個 SQL 的邏輯跟咱們在 Java 里組裝樹形結構差不多:先查根節點,然后遞歸查子節點,把父節點的信息帶過來,還能順便計算層級、拼接完整名稱,特別方便。

在 MyBatis 里用 CTE

咱們把這個 SQL 寫到 MenuMapper 里,直接讓數據庫返回帶層級的列表,然后在 Java 里只需要簡單處理一下,不用再循環組裝了:

// MenuMapper.java
publicinterface MenuMapper {
    // 原來的方法...
    
    // 新增:用CTE查詢樹形結構
    List<MenuTreeVO> selectMenuTreeByCTE();
}

// 對應的MenuTreeVO,多了level和fullName字段
publicclass MenuTreeVO {
    private Long id;
    private Long parentId;
    privateString menuName;
    privateString menuUrl;
    privateString icon;
    private Integer sort;
    private Integer level; // 層級
    privateString fullName; // 完整名稱(如:系統管理 > 菜單管理 > 新增菜單)

    // getter和setter省略...
}

XML 文件里寫 CTE 的 SQL:

<!-- MenuMapper.xml -->
<select id="selectMenuTreeByCTE" resultType="com.example.demo.vo.MenuTreeVO">
    WITH RECURSIVE menu_tree AS (
        SELECT 
            id, 
            parent_id, 
            menu_name, 
            menu_url, 
            icon, 
            sort, 
            1 AS level, 
            menu_name AS full_name 
        FROM sys_menu 
        WHERE parent_id = 0

        UNION ALL

        SELECT 
            m.id, 
            m.parent_id, 
            m.menu_name, 
            m.menu_url, 
            m.icon, 
            m.sort, 
            mt.level + 1 AS level, 
            CONCAT(mt.full_name, ' > ', m.menu_name) AS full_name 
        FROM sys_menu m
        INNER JOIN menu_tree mt ON m.parent_id = mt.id
    )
    SELECT 
        id, 
        parent_id, 
        menu_name, 
        menu_url, 
        icon, 
        sort, 
        level, 
        full_name 
    FROM menu_tree 
    ORDER BY level, sort;
</select>

然后在 Service 里調用這個方法,加緩存:

@Service
public class MenuServiceImpl implements MenuService {

    @Autowired
    private MenuMapper menuMapper;

    // 用CTE查詢的方法,同樣加緩存
    @Cacheable(value = "menuCache", key = "'menu:tree:cte'", unless = "#result == null")
    @Override
    public List<MenuTreeVO> getMenuTreeByCTE() {
        // 直接返回數據庫查詢的結果,不用在Java里組裝了
        returnmenuMapper.selectMenuTreeByCTE();
    }

    // 原來的方法...
}

這種方式的優點是Java 代碼更簡潔,把組裝樹形的邏輯交給了數據庫,而且數據庫在處理這類遞歸查詢的時候,優化做得也不錯。不過缺點是數據庫耦合度高,如果以后換數據庫(比如從 MySQL 換成 Oracle),CTE 的語法可能要改,而且如果菜單數據量特別大(比如 10 萬級以上),數據庫遞歸查詢也可能會有性能問題。所以在實際項目里,到底用 “Java 內存組裝” 還是 “數據庫 CTE”,要看你的具體場景:數據量不大、想降低數據庫耦合度,就用 Java 內存組裝;數據量中等、想簡化 Java 代碼,就用 CTE。

五、再榨一榨:那些能再快 10ms 的小技巧

到這里,咱們的查詢耗時已經降到 30ms 以內了,但還有一些小細節能再優化一下,雖然提升可能只有幾毫秒,但積少成多,而且能體現咱們的專業性。

1. 數據庫索引優化

不管是原來的遞歸查庫,還是現在的查全表,parent_id這個字段都是高頻查詢字段,所以給parent_id加個索引,能讓數據庫查得更快。

給 sys_menu 表的 parent_id 字段建索引:

ALTER TABLE sys_menu ADD INDEX idx_sys_menu_parent_id (parent_id);

建完索引之后,原來的selectByParentId方法和 CTE 里的關聯查詢,速度都會快一點,尤其是數據量比較大的時候,效果更明顯。

2. 減少返回字段

咱們之前的 SQL 里用的是select *,會把表所有字段都查出來,但前端可能只需要 id、parentId、menuName、menuUrl、icon、sort 這幾個字段,像創建時間、修改時間這些字段,前端用不上,查出來就是浪費帶寬和內存。

所以把 SQL 里的select *改成具體的字段:

<!-- 原來的selectAllMenus -->
<select id="selectAllMenus" resultType="com.example.demo.entity.MenuDO">
    select id, parent_id, menu_name, menu_url, icon, sort from sys_menu
</select>

這樣查出來的數據量更小,網絡傳輸更快,內存占用也更少,組裝樹形結構的時候也能快一點。

3. 用并行流代替普通流(謹慎用)

在把 MenuDO 轉成 MenuVO 的時候,咱們用的是普通流stream(),如果菜單數量特別多(比如 1 萬以上),可以試試并行流parallelStream(),利用多核 CPU 的優勢,加快轉換速度。

// 普通流
List<MenuVO> allMenuVOs = allMenus.stream().map(...).collect(...);

// 并行流
List<MenuVO> allMenuVOs = allMenus.parallelStream().map(...).collect(...);

不過要注意,并行流會占用更多的 CPU 資源,而且如果流操作里有線程不安全的代碼(比如用了非線程安全的集合),會出問題,所以要謹慎使用,先測試再上線。

4. 緩存預熱

咱們的緩存是 “懶加載” 的,第一次調用接口的時候才會生成緩存,所以第一次調用的耗時還是比較高(300ms 左右)。如果想讓用戶每次調用都很快,可以做緩存預熱—— 項目啟動的時候,就主動調用 getMenuTree 方法,把緩存生成好。

寫個啟動類,實現 CommandLineRunner 接口:

@Component
public class CacheWarmUpRunner implements CommandLineRunner {

    @Autowired
    private MenuService menuService;

    @Override
    public void run(String... args) throws Exception {
        // 項目啟動時,主動調用getMenuTree,生成緩存
        menuService.getMenuTree();
        System.out.println("菜單緩存預熱完成!");
    }
}

這樣項目一啟動,緩存就有了,用戶第一次調用接口的時候,直接從緩存拿,耗時也是 30ms 以內,體驗更好。

六、總結:從 3s 到 30ms,到底做了什么?

最后,咱們來回顧一下整個優化過程,其實核心思路就三個:減少數據庫 IO、利用緩存、優化細節。

  1. 第一步:從 “多次查庫” 到 “一次查庫”:把遞歸查庫改成一次性查全所有數據,在內存里組裝樹形結構,耗時從 3s 降到 300ms,快了 10 倍。
  2. 第二步:加 Redis 緩存:把組裝好的樹形結構存到 Redis 里,下次直接拿,耗時從 300ms 降到 30ms 以內,又快了 10 倍。
  3. 第三步:進階優化:用 CTE 讓數據庫直接返回樹形結構,給 parent_id 加索引,減少返回字段,做緩存預熱,讓速度再快一點。

整個過程沒有用什么特別高深的技術,都是咱們平時工作中能用到的基礎知識點,但就是這些基礎知識點的組合,讓查詢速度提升了 100 倍。這也告訴咱們,做性能優化不用一開始就上高大上的技術,先定位到瓶頸(比如這里的多次查庫),然后用最簡單的方法解決,往往效果最好。

責任編輯:武曉燕 來源: 石杉的架構筆記
相關推薦

2025-08-18 03:00:22

Spring樹形結構分類樹

2025-09-16 09:27:33

2013-02-28 10:35:59

hadoop大數據Hortonworks

2022-08-09 09:10:31

TaichiPython

2024-05-07 14:09:54

Meta模型token

2014-08-29 09:09:33

2018-03-28 14:10:10

GoPython代碼

2020-02-23 17:15:29

SQL分析查詢

2021-03-18 15:29:10

人工智能機器學習技術

2017-02-08 14:16:17

C代碼終端

2019-01-21 11:17:13

CPU優化定位

2020-07-31 17:30:26

騰訊黑鯊游戲手機

2020-08-10 11:00:02

Python優化代碼

2023-03-16 16:18:09

PyTorch程序人工智能

2022-10-10 09:10:07

命令磁盤排查

2025-09-12 16:45:51

SQL數據庫

2016-03-29 21:46:50

騰訊

2018-02-13 14:56:24

戴爾

2018-07-06 10:49:01

數據
點贊
收藏

51CTO技術棧公眾號

国产精品一区专区| 色婷婷亚洲mv天堂mv在影片| 天天免费综合色| 精选一区二区三区四区五区| 日本中文字幕电影在线免费观看| 欧美色图五月天| 动漫精品一区二区| 亚洲蜜桃在线| www.我爱av| 久久久国产精品一区二区中文| 自拍偷拍亚洲精品| 成人欧美精品一区二区| 日韩久久一区二区三区| 亚洲视频一区二区免费在线观看| 国产精品视频入口| 中国a一片一级一片| 欧美午夜不卡| 伊人精品在线观看| 免费看黄色片的网站| 国产精品久久久久久吹潮| 一区二区三区在线免费视频| 欧美日本亚洲| 精品人妻一区二区三区蜜桃 | 久久精品日产第一区二区三区精品版 | 加勒比中文字幕精品| 欧美三级在线播放| 乱妇乱女熟妇熟女网站| 最新超碰在线| 国产精品毛片大码女人| 日本在线成人一区二区| 日批视频免费播放| 国产成人精品亚洲日本在线桃色| 国产精品老女人精品视频| 日本在线免费观看| 欧美日韩综合| 久久夜色精品国产欧美乱| 亚洲欧美va天堂人熟伦| 一区二区导航| 日韩精品亚洲元码| 欧美在线一级片| 99久久香蕉| 日韩欧美激情四射| 天堂av2020| 亚洲网站免费| 欧美放荡的少妇| 日日干夜夜操s8| 99蜜月精品久久91| 91久久精品日日躁夜夜躁欧美| 成人免费观看视频在线观看| 麻豆免费版在线观看| 亚洲一区二区三区国产| 日本一级黄视频| 人人超在线公开视频| 亚洲欧美另类图片小说| 老司机午夜免费福利视频| 免费在线毛片网站| 中文字幕一区二区5566日韩| 亚洲国产精品一区二区第一页 | 日韩av在线播放资源| 中文字幕18页| 欧美激情影院| 亚洲男人天堂2023| 夫妇交换中文字幕| 五月天久久网站| 美女久久久久久久久久久| 欧美日韩免费一区二区| 黑丝一区二区| 91av国产在线| 成人毛片一区二区三区| 免费在线成人网| 91精品视频专区| www视频在线| 成人免费黄色大片| 鲁丝一区二区三区免费| 97人人在线| 亚洲乱码中文字幕综合| 真实国产乱子伦对白视频| 电影k8一区二区三区久久| 黑人巨大精品欧美一区二区免费| 国产xxxxx在线观看| 国产福利一区二区三区在线播放| 欧美久久久久久久久| gogo亚洲国模私拍人体| 任我爽精品视频在线播放| 亚洲区中文字幕| 91香蕉一区二区三区在线观看| 你懂的国产精品| 91精品国产亚洲| 中文在线字幕免费观| 国内精品在线播放| 久久亚洲午夜电影| 欧美一区二区三区在线观看免费| 亚洲自拍偷拍综合| 女人另类性混交zo| 精品国产伦一区二区三区观看说明| 精品日韩成人av| www.中文字幕av| 99精品视频在线观看播放| 欧美激情一级欧美精品| av图片在线观看| 国产在线国偷精品免费看| 国产一区自拍视频| 色视频在线免费观看| 亚洲.国产.中文慕字在线| 亚洲色图38p| 综合激情五月婷婷| 中文字幕日韩电影| 久久精品国产成人av| 精品一区二区三区在线视频| 久久国产一区| 色呦呦久久久| 欧美高清一级片在线| 精品成人av一区二区三区| 国产精品豆花视频| 国产精品视频网址| 四虎精品在永久在线观看| 亚洲男女一区二区三区| 日韩一级片播放| 理论片一区二区在线| 欧美成人一区在线| 一区二区三区黄| 久久久精品国产99久久精品芒果| 真人做人试看60分钟免费| 粉嫩一区二区| 亚洲级视频在线观看免费1级| 疯狂试爱三2浴室激情视频| 石原莉奈在线亚洲二区| 精品久久中出| 国内高清免费在线视频| 91精品视频网| 999精品久久久| 久久经典综合| 美乳视频一区二区| av资源网在线播放| 精品对白一区国产伦| 青青草原在线免费观看视频| 精品综合免费视频观看| 视频一区二区三区在线观看| 电影网一区二区| 亚洲精品一区久久久久久| 天天操天天射天天爽| 国产黄人亚洲片| 国产女主播av| 国产精品毛片无码| 久久五月情影视| 国产精品一级视频| 亚洲日本在线观看| 玖玖爱视频在线| 99久久精品网站| 国产欧美精品一区二区三区-老狼 国产欧美精品一区二区三区介绍 国产欧美精品一区二区 | 精品成人自拍视频| 久久久久久com| 黄色三级网站在线观看| 亚洲国产日韩精品| a天堂视频在线观看| 在线视频亚洲| 青青成人在线| 日韩欧美2区| 主播福利视频一区| 国产wwwxxx| 亚洲五码中文字幕| av在线播放网址| 亚洲在线黄色| 午夜精品亚洲一区二区三区嫩草| 欧美日韩尤物久久| 久久综合久久美利坚合众国| 国产ts人妖调教重口男| 亚洲成人动漫在线观看| 精品黑人一区二区三区观看时间| 亚洲中字黄色| 性欧美精品一区二区三区在线播放| 国产精品99精品一区二区三区∴| 精品国产一区二区三区久久狼黑人| 国产区精品在线| 亚洲国产毛片aaaaa无费看| 五十路六十路七十路熟婆| 日韩精品成人一区二区三区| 亚洲一二三区在线| 在这里有精品| 欧亚精品中文字幕| 婷婷免费在线视频| 欧美精品一区二区蜜臀亚洲| 五月婷婷激情视频| 中文字幕视频一区二区三区久| 乳色吐息在线观看| 性色一区二区| 一区二区不卡在线视频 午夜欧美不卡' | 亚洲综合日韩欧美| 激情久久综合| 婷婷精品国产一区二区三区日韩| 精品国产亚洲一区二区三区| 欧美亚洲视频在线看网址| 老司机精品视频在线观看6| 亚洲国产精品99| 中文字幕第315页| 亚洲国产精品自拍| 国产黄色录像视频| 成人动漫一区二区| 亚洲a级黄色片| 999亚洲国产精| 一区二区三区在线视频111| 成人在线超碰| 国产精品日韩一区| 欧美videosex性欧美黑吊| 中文精品99久久国产香蕉| 人妻无码中文字幕免费视频蜜桃| 欧美中文字幕久久| 国产精品suv一区二区| 欧美国产激情二区三区| 久久人妻少妇嫩草av蜜桃| 琪琪一区二区三区| 欧美性潮喷xxxxx免费视频看| 欧美日中文字幕| 国产欧美一区二区三区另类精品| 欧美高清你懂的| 国产99久久精品一区二区永久免费| 激情av在线播放| www.欧美精品| 国产福利在线| 日韩精品欧美激情| 亚洲精品字幕在线观看| 在线成人高清不卡| 天天干天天操天天操| 欧美日韩精品在线视频| 久久在线视频精品| 亚洲欧美日韩国产综合在线| 免费在线观看a视频| 91网上在线视频| 日本人添下边视频免费| 国产精品18久久久久久久久| gai在线观看免费高清| 久久精品观看| 激情五月开心婷婷| 国产情侣一区| 久久视频这里有精品| 亚洲天堂黄色| 日本中文字幕亚洲| 亚洲一级影院| www.在线观看av| 欧美精品成人| 黄色一级大片免费| 午夜精品久久| 国产性生活免费视频| 你懂的亚洲视频| 国产精品无码电影在线观看| 欧美区国产区| 69sex久久精品国产麻豆| 国内精品久久久久久久97牛牛| 国产尤物av一区二区三区| 欧美在线观看天堂一区二区三区| 激情六月天婷婷| 欧美成人嫩草网站| 久久精品无码中文字幕| 樱桃成人精品视频在线播放| 99久久国产综合精品五月天喷水| 亚洲成人在线| 2022亚洲天堂| 日韩精品欧美精品| 国产免费中文字幕| 国产精品1区2区| 在线视频 日韩| 久久久久久久久久看片| 正在播放国产对白害羞| 亚洲欧洲成人精品av97| 加勒比婷婷色综合久久| 亚洲国产日韩在线一区模特| 精品欧美一区二区三区免费观看| 色婷婷综合久久久中文一区二区| 乱子伦一区二区三区| 欧美日韩免费不卡视频一区二区三区| 一本色道久久综合无码人妻| 91精品国产手机| 国模无码一区二区三区| 亚洲女人天堂成人av在线| 777电影在线观看| 色综合久久88| 欧美magnet| 91精品视频专区| 精品成人自拍视频| 亚洲高清视频一区| 一区三区视频| 九九视频精品在线观看| 寂寞少妇一区二区三区| 91精品啪在线观看国产| 国产午夜精品久久| 卡通动漫亚洲综合| 精品日韩中文字幕| 艳妇乳肉豪妇荡乳av| 欧美不卡视频一区| 国产成人天天5g影院在线观看| 麻豆国产精品va在线观看不卡| 国产传媒av在线| 成人久久精品视频| 亚洲午夜久久| 日本高清视频免费在线观看| 久久这里只有| 性xxxxxxxxx| 中文字幕成人在线观看| 欧美一二三区视频| 这里只有精品视频在线观看| 午夜视频福利在线观看| 久久精品国产精品| 亚洲欧洲美洲av| 99超碰麻豆| 色综合天天爱| 欧美成人xxxxx| 国产乱码精品一区二区三区忘忧草 | 国产又粗又硬又长| 久久婷婷激情| 中文字幕a在线观看| 亚洲视频中文字幕| 天堂网一区二区| 亚洲国产精品一区二区久| 精品美女在线观看视频在线观看| 国产69久久精品成人| 1204国产成人精品视频| 亚洲综合第一| 日韩中文字幕av电影| 亚洲人人夜夜澡人人爽| 亚洲一区二区三区四区的| 国产美女永久免费| 中文字幕精品在线| 美女写真久久影院| 欧美成人蜜桃| 99精品福利视频| 韩国三级视频在线观看| 亚洲精品视频在线看| 国产又粗又猛视频| 中文字幕亚洲第一| 老司机成人影院| 久久精品日产第一区二区三区精品版 | 亚洲综合欧美日韩| 日韩高清不卡一区二区| 麻豆av免费观看| 精品欧美国产一区二区三区| 亚洲国产精品国自产拍久久| 美女久久久久久久久久久| 9999在线精品视频| 中文字幕人成一区| 久久99国产乱子伦精品免费| 国产破处视频在线观看| 欧美日韩久久不卡| 午夜老司机在线观看| 国产精品美女在线| 久久网站免费观看| 国产免费中文字幕| 亚洲理论在线观看| www.色播.com| 久久久久久久久电影| 国产+成+人+亚洲欧洲在线| 2019日韩中文字幕mv| 成人高清视频在线| 亚洲男人的天堂在线视频| 亚洲精品资源美女情侣酒店| 台湾佬中文娱乐久久久| 日韩伦理一区二区三区av在线| 日韩在线一二三区| 国产极品视频在线观看| 在线成人小视频| 日本精品600av| 国产视频一区二区不卡| 新67194成人永久网站| 免费黄色在线网址| 91精品国产欧美一区二区| 色婷婷视频在线观看| 国产欧美日韩亚洲| 三级在线观看一区二区| 99久久99久久精品免费| 欧美一区二区三级| а√在线天堂官网| 日韩欧美视频一区二区| 韩国一区二区视频| 亚洲精品视频在线观看免费视频| 精品无人区太爽高潮在线播放| 秋霞国产精品| 成人手机在线播放| 2017欧美狠狠色| 97在线播放免费观看| 久久久久免费视频| 国产欧美一区| 天堂网成人在线| 黄色成人在线播放| 午夜视频成人| 国产区一区二区| 蜜桃av噜噜一区二区三区小说| 久热这里有精品| 日韩精品视频在线观看网址| 欧美啪啪网站| 九色自拍视频在线观看| 亚洲国产精品v| 国产91免费在线观看| 国产精品 欧美在线| 中文视频一区| 欧美成人国产精品一区二区| 欧美一区二区免费视频| 最新日韩三级| 欧美黄色免费网址| 国产精品网曝门| 天堂v视频永久在线播放| 成人黄色免费片|