精品欧美一区二区三区在线观看 _久久久久国色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技術棧公眾號

精品视频在线观看一区二区| 999国产精品视频| 日韩中文字幕亚洲一区二区va在线| 欧美日韩精品一区二区在线播放| 欧美成人激情视频| 国产免费成人在线| 亚洲免费不卡视频| 99久久亚洲精品蜜臀| 色妞www精品视频| 韩国成人一区| 日韩精品一区二区av| 电影一区中文字幕| 国产精品成人网| 国产福利成人在线| 少妇一级淫片免费放播放| 成人在线观看免费网站| 残酷重口调教一区二区| 五月天久久比比资源色| 国产激情一区二区三区在线观看| 爱爱视频免费在线观看| 国产激情久久| 国产精品丝袜在线| 国产美女扒开尿口久久久| 手机看片福利视频| 经典三级一区二区| 国产人妖乱国产精品人妖| 国产成人精品视频在线| 校园春色 亚洲| 91精品久久久久久综合五月天| 亚洲婷婷国产精品电影人久久| 国产在线观看不卡| 精品无码一区二区三区蜜臀| 中文字幕日本一区| 亚洲视频一二三区| 日韩成人在线资源| 欧美日韩 一区二区三区| 精品中文视频| 亚洲一区二区免费视频| 国产欧美日韩视频一区二区三区| 国产午夜久久久| 爱高潮www亚洲精品| 亚洲妇熟xx妇色黄| 欧美下载看逼逼| 波多野结衣理论片| 亚洲欧美日韩在线观看a三区| 亚洲精品一区二区网址| 欧美日韩在线观看不卡| 九七电影韩国女主播在线观看| 国产毛片精品国产一区二区三区| 久久这里有精品视频| 精品人妻在线视频| 国偷自产一区二区免费视频| 国产日韩精品一区二区三区| 国产欧美一区二区白浆黑人| 91黑人精品一区二区三区| 久久伊人亚洲| 美女久久久久久久| 国产精品白丝喷水在线观看| 噜噜噜狠狠夜夜躁精品仙踪林| 日本黄色一区二区| 国产熟女高潮视频| 国产精品毛片久久久久久久久久99999999| 亚洲欧洲精品一区二区三区| 国产一区二区黄色| 欧美一级特黄aaaaaa| 免费黄网站欧美| 久久久久久久成人| 国产真人做爰视频免费| 久久精品国产www456c0m| 日韩有码在线观看| 亚洲AV无码片久久精品| 欧美一级大片在线视频| 一级日本不卡的影视| 天天综合色天天综合色hd| 亚洲美女福利视频| 91欧美激情一区二区三区成人| 国产欧美一区二区三区久久人妖| 亚洲天堂aaa| 国产视频欧美| 欧美激情图片区| 女人十八岁毛片| 欧美一区久久| www国产91| 久久国产精品波多野结衣| 日本一区二区免费高清| 日韩av有码在线| 无码国产精品久久一区免费| 97欧美成人| 51久久夜色精品国产麻豆| 男女av免费观看| 日韩毛片免费看| 欧洲亚洲国产日韩| 欧美日韩在线视频一区二区三区| www.在线视频| 香港成人在线视频| 97久久国产亚洲精品超碰热| 日韩av一卡| 亚洲国产精品嫩草影院| 欧美激情精品久久久久久小说| av老司机在线观看| 亚洲乱码一区二区三区在线观看| 亚洲午夜精品国产| 午夜伦全在线观看| 国产精品久99| 国产精品国产亚洲精品看不卡| 超鹏97在线| 亚洲成国产人片在线观看| 少妇激情一区二区三区| 欧洲亚洲两性| 一本久久综合亚洲鲁鲁五月天| 日日碰狠狠添天天爽超碰97| 亚洲区欧洲区| 一区二区三区欧美亚洲| 免费在线精品视频| 天堂网在线资源| 精品一区二区精品| 国产欧美一区二区三区四区 | 日韩av毛片| 欧美无乱码久久久免费午夜一区| 成人黄色片视频| 成人直播在线观看| 欧美成人精品一区| 一级黄在线观看| 激情图片小说一区| 鲁丝片一区二区三区| 天堂中文在线8| 99久久精品国产毛片| 久久精品国产综合精品| 天堂中文在线资| 一区二区三区四区在线播放| 天天干天天草天天| 四虎在线精品| 亚洲一区999| 亚洲区一区二区三| 日韩avvvv在线播放| 91精品久久久久久久久久入口| 亚洲系列在线观看| 国产女人18毛片水真多成人如厕| 日本国产在线播放| 国产伦精品一区二区三区在线播放 | 日韩无码精品一区二区三区| 最新日韩av| 91精品国产自产91精品| youjizz在线视频| 日韩中文字幕一区二区三区| 国精产品99永久一区一区| 欧美性受ⅹ╳╳╳黑人a性爽| 精品久久久香蕉免费精品视频| 亚洲综合伊人久久| 欧美日韩看看2015永久免费| 亚洲人a成www在线影院| 中文字幕在线2021| 国模少妇一区二区三区| 男女啪啪的视频| 成人三级高清视频在线看| 91福利区一区二区三区| 能免费看av的网站| 日本在线不卡视频一二三区| 亚洲免费不卡| 黄页在线观看免费| 欧美影院一区二区三区| 中文字幕第24页| 怡红院精品视频在线观看极品| 国产精品成人va在线观看| 精品人妻无码一区二区| 国产日本一区二区| 天天干天天干天天干天天干天天干| 欧美丝袜激情| 91久热免费在线视频| 奇米影视888狠狠狠777不卡| 中文字幕亚洲区| av噜噜在线观看| 一个色免费成人影院| 久久久精品国产网站| 欧美一级特黄视频| 国产黄色91视频| 日本在线播放不卡| 99er精品视频| 欧美精品videos另类日本| 亚欧洲精品视频| 亚洲免费av观看| 亚洲精品乱码久久| 日韩电影免费在线| 红桃一区二区三区| 天天做夜夜做人人爱精品| 九九久久精品一区| 中文字幕乱码一区二区| 26uuu精品一区二区三区四区在线| 国产一区亚洲二区三区| 亚洲经典一区| 成人激情在线播放| 国产777精品精品热热热一区二区| 亚洲欧美国产精品久久久久久久| 久久久久久久伊人| 久久久久久久久久美女| 黄页网站大全在线观看| 国产精品午夜一区二区三区| 欧美亚洲国产视频| 污视频在线免费| 欧美日韩一级片在线观看| 欧美成人国产精品一区二区| 国产精品日本欧美一区二区三区| 少妇精品久久久久久久久久| julia中文字幕一区二区99在线| 国产精品777| 成人女同在线观看| 日韩一区av在线| 无码国产精品一区二区色情男同| 欧美日韩免费观看一区三区| 天堂在线免费观看视频| 亚洲精品视频一区二区| 国产伦理片在线观看| 粉嫩av一区二区三区在线播放| 日本一区二区免费高清视频| 亚洲区小说区图片区qvod| 粉嫩av一区二区三区免费观看| 欧美成a人片在线观看久| 久久久久久久久中文字幕| 欧美一区二区三区在线观看免费| 精品视频在线视频| 国产午夜性春猛交ⅹxxx| 亚洲精品视频在线看| 东京热无码av男人的天堂| 蜜臀av一区二区三区| 5月婷婷6月丁香| 国产精品啊啊啊| 精品在线不卡| 国产精东传媒成人av电影| 亚洲精品免费av| 91在线超碰| 久久亚洲综合国产精品99麻豆精品福利 | 亚洲人成在线网站| 亚洲人在线视频| 二区三区在线视频| 日韩一区二区三区高清免费看看| 欧美日韩精品在线观看视频| 国产精品久久久久久久久免费桃花| 人人人妻人人澡人人爽欧美一区| 91美女片黄在线观看91美女| av漫画在线观看| 粉嫩一区二区三区性色av| 一区二区在线免费观看视频| 亚洲一区视频| 黄色www网站| 国产一区导航| 国产美女三级视频| 日产欧产美韩系列久久99| www.色偷偷.com| 免费视频一区二区| jizz欧美性11| 国产在线一区观看| av网站在线观看不卡| 亚洲在线黄色| 成人精品视频一区二区| 日韩电影在线免费看| 九九热免费精品视频| 久久成人av少妇免费| 阿v天堂2018| 欧美日韩有码| 国产精品国产三级国产专区53 | 欧美区国产区| 欧美极品一区| 欧美日韩激情| 在线观看欧美亚洲| 中国av一区| 日韩欧美一区二区在线观看| 久久国产精品成人免费观看的软件| 亚洲精品成人久久久998| 香蕉综合视频| 国产精品入口芒果| 亚州av乱码久久精品蜜桃 | 高h视频在线观看| 欧美丰满少妇xxxxx| 99中文字幕一区| 亚洲精品www久久久| 99国产成人精品| 欧美午夜片在线看| 国产精品一区二区三区在线免费观看| 欧美日韩亚洲一区二区三区| 我家有个日本女人| 欧美日韩人人澡狠狠躁视频| 黄色污污视频软件| 精品成人国产在线观看男人呻吟| www.国产com| 7777精品久久久大香线蕉| 欧美一级性视频| 日韩在线观看网站| 国产黄在线观看| 亚洲乱亚洲乱妇无码| 日本免费在线观看| 午夜精品在线视频| 国产蜜臀av在线播放| 国产成人综合亚洲| 中文字幕一区二区三区中文字幕 | 青青草综合网| 99热久久这里只有精品| 日韩av一区二区三区| 中文字幕三级电影| 国产一区二区三区香蕉| 精品国产人妻一区二区三区| 粉嫩高潮美女一区二区三区| 亚洲天堂岛国片| 午夜精品福利一区二区三区av| 五月激情丁香网| 亚洲精品黄网在线观看| av大片在线| 欧美激情欧美激情在线五月| 亚洲成人人体| 国产一区二区三区四区hd| 国产精品精品| 国产精品乱码久久久久| 美女视频一区免费观看| 国产91对白刺激露脸在线观看| 精品亚洲成av人在线观看| 亚洲一区二区自偷自拍| 午夜精品视频一区| 精品国自产在线观看| 日韩在线观看免费av| 日韩电影网站| 精品伊人久久大线蕉色首页| 红桃视频国产精品| 中文字幕avav| 99久久99久久精品免费观看| 男人操女人的视频网站| 欧美午夜片在线看| 成年人在线视频| 久久久av网站| 国产一区二区三区四区五区3d| 国产情人节一区| 欧美日韩国产在线观看网站| 日韩一级在线免费观看| 91色在线porny| 精品在线播放视频| 亚洲国语精品自产拍在线观看| 日本一级理论片在线大全| 91久久精品久久国产性色也91| 日韩在线高清| 黑人粗进入欧美aaaaa| 欧美激情一区三区| 国产成人精品一区二区色戒| 国产一区二区三区视频在线观看| 中文一区一区三区高中清不卡免费| 国产私拍一区| 国产一级一区二区| 国产一二三四五区| 欧美亚洲一区二区在线| 成人三级黄色免费网站| 国产女同一区二区| 五月天久久网站| 亚洲黄色片免费看| 一区二区成人在线| 欧美一区,二区| 热久久这里只有精品| www.欧美| 玖玖精品在线视频| 成人久久18免费网站麻豆 | 91地址最新发布| 亚洲va久久| 91福利国产成人精品播放| 亚洲欧洲色图综合| 午夜精品久久久久久久96蜜桃| 久久久天堂国产精品女人| 91国内外精品自在线播放| 亚洲国产一区二区在线| 国产一区二区在线看| 久一区二区三区| 欧美精品久久一区| 精品av中文字幕在线毛片| 色综合久久88| 天堂av一区二区三区在线播放 | 日本免费久久| 免费看av软件| 成人av在线播放网站| 97在线观看视频免费| 日韩视频免费观看高清完整版在线观看 | 欧美日韩国产一中文字不卡| 福利在线观看| 亚洲综合av影视| 亚洲一区日韩| 日本福利片在线观看| 日韩成人在线视频观看| 久久亚洲人体| 免费毛片网站在线观看| 欧美国产一区二区| 老熟妇高潮一区二区高清视频| 日韩av手机在线| 国产精品jk白丝蜜臀av小说 | 久久精品国产久精国产爱| 插我舔内射18免费视频| 91搞黄在线观看| 污片在线免费观看| 日韩av影视| www.激情成人| 日韩男人的天堂| 日韩色av导航| 妖精一区二区三区精品视频| 亚洲欧美aaa| 中文字幕一区视频| 天堂在线观看视频| 91色p视频在线|