個人部落格專案筆記_05

CherriesOvO發表於2024-04-10

1. ThreadLocal記憶體洩漏

ThreadLocal 記憶體洩漏是指由於沒有及時清理 ThreadLocal 例項所儲存的資料,導致這些資料線上程池或長時間執行的應用中累積過多,最終導致記憶體佔用過高的情況。

記憶體洩漏通常發生在以下情況下:

  1. 執行緒池場景下的 ThreadLocal 使用不當: 在使用執行緒池時,如果執行緒被重用而沒有正確清理 ThreadLocal 中的資料,那麼下次使用這個執行緒時,它可能會攜帶上一次執行任務所遺留的資料,從而導致資料累積並消耗記憶體。
  2. 長時間執行的應用中未清理 ThreadLocal 資料: 在一些長時間執行的應用中,比如 Web 應用,可能會建立很多 ThreadLocal 例項並儲存大量資料。如果這些資料在使用完後沒有及時清理,就會導致記憶體洩漏問題。
  3. 沒有使用 remove() 方法清理 ThreadLocal 資料: 在使用完 ThreadLocal 儲存的資料後,如果沒有呼叫 remove() 方法清理資料,就會導致資料長時間存在於 ThreadLocal 中,從而可能引發記憶體洩漏。

實線代表強引用,虛線代表弱引用

每一個Thread維護一個ThreadLocalMap, key為使用弱引用的ThreadLocal例項,value為執行緒變數的副本。

強引用,使用最普遍的引用,一個物件具有強引用,不會被垃圾回收器回收。當記憶體空間不足,Java虛擬機器寧願丟擲OutOfMemoryError錯誤,使程式異常終止,也不回收這種物件。

如果想取消強引用和某個物件之間的關聯,可以顯式地將引用賦值為null,這樣可以使JVM在合適的時間就會回收該物件。

弱引用,JVM進行垃圾回收時,無論記憶體是否充足,都會回收被弱引用關聯的物件。在java中,用java.lang.ref.WeakReference類來表示。

2. 文章詳情

2.1 介面說明

介面url:/articles/view/{id}

請求方式:POST

請求引數:

引數名稱 引數型別 說明
id long 文章id(路徑引數)

返回資料:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": "token"
}

2.2 涉及到的表

CREATE TABLE `blog`.`ms_article_body`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `content` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
  `content_html` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
  `article_id` bigint(0) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `article_id`(`article_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 38 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
package com.cherriesovo.blog.dao.pojo;

import lombok.Data;

@Data
public class ArticleBody {	//文章詳情表

    private Long id;
    private String content;
    private String contentHtml;
    private Long articleId;
}
#文章分類
CREATE TABLE `blog`.`ms_category`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  `category_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
package com.cherriesovo.blog.dao.pojo;

import lombok.Data;

@Data
public class Category {

    private Long id;

    private String avatar;

    private String categoryName;

    private String description;
}

2.3 Controller

//json資料進行互動
@RestController
@RequestMapping("articles")
public class ArticleController {
    /*
    * 透過id獲取文章
    * */
    @PostMapping("view/{id}")
    	//@PathVariable("id") 註解用於將 URL 中的 {id} 賦值給 articleId 引數。
    public Result findArticleById(@PathVariable("id") Long articleId) {	

        return articleService.findArticleById(articleId);
    }
}

2.4 Service

 ArticleVo findArticleById(Long id);
public interface ArticleService {
    
    //檢視文章詳情
    Result findArticleById(Long articleId);
}

package com.cherriesovo.blog.vo;


import lombok.Data;

import java.util.List;

@Data
public class ArticleVo {

    private Long id;

    private String title;

    private String summary;

    private int commentCounts;

    private int viewCounts;

    private int weight;
    /**
     * 建立時間
     */
    private String createDate;

    private String author;

    private ArticleBodyVo body;

    private List<TagVo> tags;

    private CategoryVo category;

}
@Service
public class ArticleServiceImpl implements ArticleService {
    @Autowired
    private ArticleMapper articleMapper;
    @Autowired
    private TagService tagService;
    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private CategoryService categoryService;

    public ArticleVo copy(Article article,boolean isAuthor,boolean isBody,boolean isTags,boolean isCategory){
        ArticleVo articleVo = new ArticleVo();
        BeanUtils.copyProperties(article, articleVo);

        articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
        //並不是所有的介面都需要標籤,作者資訊
        if(isTags){
            Long articleId = article.getId();
            articleVo.setTags(tagService.findTagsByArticleId(articleId));
        }
        if(isAuthor){
            Long authorId = article.getAuthorId();
            //getNickname()用於獲取某個物件或實體的暱稱或別名
            articleVo.setAuthor(sysUserService.findUserById(authorId).getNickname());
        }
        if (isBody){
            Long bodyId = article.getBodyId();
            articleVo.setBody(findArticleBodyById(bodyId));
        }
        if (isCategory){
            Long categoryId = article.getCategoryId();
            articleVo.setCategory(categoryService.findCategoryById(categoryId));
        }
        return articleVo;
    }

    private List<ArticleVo> copyList(List<Article> records,boolean isAuthor,boolean isBody,boolean isTags) {
        List<ArticleVo> articleVoList = new ArrayList<>();
        for (Article article : records) {
            ArticleVo articleVo = copy(article,isAuthor,false,isTags,false);
            articleVoList.add(articleVo);
        }
        return articleVoList;
    }

    private List<ArticleVo> copyList(List<Article> records,boolean isAuthor,boolean isBody,boolean isTags,boolean isCategory) {
        List<ArticleVo> articleVoList = new ArrayList<>();
        for (Article article : records) {
            ArticleVo articleVo = copy(article,isAuthor,isBody,isTags,isCategory);
            articleVoList.add(articleVo);
        }
        return articleVoList;
    }

    @Autowired
    private ArticleBodyMapper articleBodyMapper;
    
    private ArticleBodyVo findArticleBodyById(Long bodyId) {
        ArticleBody articleBody = articleBodyMapper.selectById(bodyId);
        ArticleBodyVo articleBodyVo = new ArticleBodyVo();
        articleBodyVo.setContent(articleBody.getContent());//setContent()是articleBodyVo的set方法
        return articleBodyVo;
    }

    @Override
    public List<ArticleVo> listArticlesPage(PageParams pageParams) {
    //  分頁查詢article資料庫表
        QueryWrapper<Article> queryWrapper = new QueryWrapper<>();
        Page<Article> page = new Page<>(pageParams.getPage(),pageParams.getPageSize());
        Page<Article> articlePage = articleMapper.selectPage(page, queryWrapper);
        List<ArticleVo> articleVoList = copyList(articlePage.getRecords(),true,false,true);
        return articleVoList;
    }

    @Override
    public Result hotArticle(int limit) {
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.orderByDesc(Article::getViewCounts);   //根據瀏覽量倒序
        queryWrapper.select(Article::getId,Article::getTitle);
        queryWrapper.last("limit " + limit);
        //select id,title from article order by view_counts desc limit 5
        List<Article> articles = articleMapper.selectList(queryWrapper);
        return Result.success(copyList(articles,false,false,false));
    }

    @Override
    public Result newArticles(int limit) {
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.orderByDesc(Article::getCreateDate);
        queryWrapper.select(Article::getId,Article::getTitle);
        queryWrapper.last("limit "+limit);
        //select id,title from article order by create_date desc limit 5
        List<Article> articles = articleMapper.selectList(queryWrapper);

        return Result.success(copyList(articles,false,false,false));
    }

    @Override
    public Result listArchives() {
        List<Archives> archivesList = articleMapper.listArchives();
        return Result.success(archivesList);
    }

    @Override
    public Result findArticleById(Long articleId) {
        /*
        * 1、根據id查詢文章資訊
        * 2、根據bodyId和categoryId去做關聯查詢
        * */
        Article article = this.articleMapper.selectById(articleId);
        ArticleVo articleVo = copy(article, true, true, true,true);
        return Result.success(articleVo);
    }
}
package com.cherriesovo.blog.vo;

import lombok.Data;

@Data
public class CategoryVo {

    private Long id;

    private String avatar;

    private String categoryName;
}
package com.cherriesovo.blog.vo;

import lombok.Data;

@Data
public class ArticleBodyVo {

    private String content;
}

package com.cherriesovo.blog.service;

import com.cherriesovo.blog.vo.CategoryVo;

import java.util.List;

public interface CategoryService {
    CategoryVo findCategoryById(Long categoryId);
}

@Service
public class CategoryServiceImpl implements CategoryService {
    @Autowired
    private CategoryMapper categoryMapper;
    @Override
    public CategoryVo findCategoryById(Long categoryId) {
        Category category = categoryMapper.selectById(categoryId);
        CategoryVo categoryVo = new CategoryVo();
        BeanUtils.copyProperties(category,categoryVo);
        return categoryVo;
    }
}
package com.cherriesovo.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cherriesovo.blog.dao.pojo.ArticleBody;

public interface ArticleBodyMapper extends BaseMapper<ArticleBody> {
}

package com.cherriesovo.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cherriesovo.blog.dao.pojo.Category;

public interface CategoryMapper extends BaseMapper<Category> {
}

2.5 測試

3. 使用執行緒池 更新閱讀次數

3.1 執行緒池配置

  1. taskExecutor 是一個執行緒池物件,在這段程式碼中透過 @Bean("taskExecutor") 註解定義並配置了一個執行緒池,並將其命名為 "taskExecutor"。
  2. asyncServiceExecutor() 方法是一個 Bean 方法,用於建立並配置一個執行緒池,並以 taskExecutor 作為 Bean 的名稱。
  3. ThreadPoolTaskExecutor 是 Spring 框架提供的一個實現了 Executor 介面的執行緒池
  4. 在方法中建立了一個 ThreadPoolTaskExecutor 例項 executor,並對其進行了一系列配置:
    • setCorePoolSize(5): 設定核心執行緒數為 5,即執行緒池在空閒時會保持 5 個核心執行緒。
    • setMaxPoolSize(20): 設定最大執行緒數為 20,即執行緒池中允許的最大執行緒數量。
    • setQueueCapacity(Integer.MAX_VALUE): 配置佇列大小為整數的最大值,即任務佇列的最大容量。
    • setKeepAliveSeconds(60): 設定執行緒活躍時間為 60 秒,即執行緒在空閒超過該時間後會被銷燬。
    • setThreadNamePrefix("CherriesOvO部落格專案"): 設定執行緒名稱的字首為 "CherriesOvO部落格專案"。
    • setWaitForTasksToCompleteOnShutdown(true): 設定在關閉執行緒池時等待所有任務結束。
    • initialize(): 執行執行緒池的初始化。
  5. 最後,將配置好的執行緒池返回為一個 Executor Bean,供其他元件使用。
package com.cherriesovo.blog.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
@EnableAsync    //開啟多執行緒
public class ThreadPoolConfig {

    @Bean("taskExecutor")
    public Executor asyncServiceExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 設定核心執行緒數
        executor.setCorePoolSize(5);
        // 設定最大執行緒數
        executor.setMaxPoolSize(20);
        //配置佇列大小
        executor.setQueueCapacity(Integer.MAX_VALUE);
        // 設定執行緒活躍時間(秒)
        executor.setKeepAliveSeconds(60);
        // 設定預設執行緒名稱
        executor.setThreadNamePrefix("CherriesOvO部落格專案");
        // 設定等待所有任務結束後再關閉執行緒池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        //執行初始化
        executor.initialize();
        return executor;
    }
}

3.1 使用

  1. 透過 @Async("taskExecutor") 註解,該方法標記為非同步執行,並指定了使用名為 "taskExecutor" 的執行緒池。

  2. articleMapper.update(articleUpdate, updateWrapper) 是一個 MyBatis-Plus 中的更新操作,用於更新資料庫中的文章記錄。

    update 方法接受兩個引數:

    1. articleUpdate:表示需要更新的文章物件,其中包含了新的閱讀量。
    2. updateWrapper:表示更新條件,即確定哪些文章需要被更新的條件。
  3. 這段程式碼透過 Thread.sleep(5000) 方法在當前執行緒中休眠了5秒鐘。這樣做的目的是為了模擬一個耗時操作,以展示在非同步執行緒中執行的任務不會影響到主執行緒的執行。

package com.cherriesovo.blog.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cherriesovo.blog.dao.mapper.ArticleMapper;
import com.cherriesovo.blog.dao.pojo.Article;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
public class ThreadService {

    //期望此操作線上程池中執行,不會影響原有的主執行緒
    @Async("taskExecutor")
    public void updateArticleViewCount(ArticleMapper articleMapper, Article article){

        int viewCounts = article.getViewCounts();
        Article articleUpdate = new Article();
        articleUpdate.setViewCounts(viewCounts + 1);
        LambdaQueryWrapper<Article> updateWrapper = new LambdaQueryWrapper<>();
        updateWrapper.eq(Article::getId,article.getId());
        //設定一個 為了在多執行緒環境下 執行緒安全
        updateWrapper.eq(Article::getViewCounts,article.getViewCounts());
		//update article set view_count=? where view_count=? and id=?
        articleMapper.update(articleUpdate,updateWrapper);
        try {
            //睡眠5秒 證明不會影響主執行緒的使用,5秒後資料才會出現
            Thread.sleep(5000);
//            System.out.println("更新完成了");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

@Service
public class ArticleServiceImpl implements ArticleService {

    @Autowired
    private ThreadService threadService;
    @Override
    public Result findArticleById(Long articleId) {
        /*
        * 1、根據id查詢文章資訊
        * 2、根據bodyId和categoryId去做關聯查詢
        * */
        Article article = this.articleMapper.selectById(articleId);
        ArticleVo articleVo = copy(article, true, true, true,true);
        //檢視完文章了,新增閱讀數,有沒有問題?
        //檢視完文章之後,本應該直接返回資料了,這時候做了一個更新操作,更新時加寫鎖,阻塞其他讀操作,效能比較低
        //更新增加此時介面的耗時,如果一旦更新出問題,不能影響檢視文章的操作
        //執行緒池  可以把更新操作扔到執行緒池中去執行,和主執行緒就不相關了
        threadService.updateArticleViewCount(articleMapper,article);
        return Result.success(articleVo);
    }
}

3.3 測試

睡眠 ThredService中的方法 5秒,不會影響主執行緒的使用,即文章詳情會很快的顯示出來,不受影響

相關文章