博客項目 Spring + Redis + Mysql

基礎模塊

? ? ? ? 1. 郵箱發送功能

? ? ? ?最初設計的接口 (雛形)

public interface EmailService {/*** 發送驗證碼郵件** @param email 目標郵箱* @return 發送的code* @throws RuntimeException 如果發送郵件失敗,將拋出異常*/String sendVerificationCode(String email);/*** 校驗驗證碼是否正確** @param email 郵箱地址* @param code 用戶輸入的驗證碼* @return true 表示校驗通過,false 為不通過*/boolean checkVerificationCode(String email, String code);/*** 判斷郵箱當前是否處于驗證碼限流狀態** @param email 郵箱地址* @return true 表示當前已限流,不可發送,false 表示未限流,可以發送*/boolean isVerificationCodeRateLimited(String email);
}

EmailServiceImpl 實現類? ?( 具體實現)

@Service
public class EmailServiceImpl implements EmailService {@Autowiredprivate ObjectMapper objectMapper;@Autowiredprivate RedisTemplate<String, String> redisTemplate;@Value("${mail.verify-code.limit-expire-seconds}")private int limitExpireSeconds;@Overridepublic String sendVerificationCode(String email) {// ...}@Overridepublic boolean checkVerificationCode(String email, String code) {// ...}@Overridepublic boolean isVerificationCodeRateLimited(String email) {// ...}
}

分析

先來分析實現類中的三個注入的對象的用途:

  • redisTemplate:操作redis的接口,可以類比JDBC接口

  • limitExpireSeconds:application.yaml中的配置,表示一個email在發送一條郵件后,會被限流多長時間,才能發送第二條郵件,配置文件中是60秒,可以按照自己的需求修改

  • objectMapper:Jackson的接口對象,用于JSON和對象的相互轉換

sendVerificationCode方法和"EmailTaskConsumer"方法

到這里,我們可以開始實現一個"基于Redis消息隊列"的輕量級異步任務機制了。

消息隊列有兩個最基礎的部分:

1.生產者

2.消費者

從代碼層面來說,sendVerificationCode就是所謂的生產者。

生產者的工作步驟:

1.接收email參數,生成一個email對應的code

2.將email、code、以及當前的時間戳封裝為一個對象(EmailTask對象)

3.將EmailTask對象序列化為JSON字符串(Redis只支持存儲字符串)

4.將JSON字符串寫入到消息隊列中

5.防止重復請求,給郵箱設置一個限流鍵(這步可以不包含在sendVerificationCode中)

消費者的工作步驟:

1.每隔一段時間輪詢Redis,查看消息隊列中是否存在任務

2.將任務的JSON字符串從消息隊列中取出,將其反序列化為EmailTask對象

3.根據EmailTask對象中的字段,填充SimpleEmailMessage對象

4.JavaMailSender發送SimpleEmailMessage對象(調用第三方服務,發郵件給用戶)

到這里,生產者和消費者需要完成的步驟已經規劃完畢,接下來我們看具體代碼實現。

生產者部分:

首先是EmailTask對象,擁有三個字段

1.email字段:用戶的email

2.code:驗證碼

3.timestamp:時間戳

@Data
public class EmailTask {private String email;private String code;private long timestamp;
}

接下來是?sendVerificationCode 的實現

@Override
public String sendVerificationCode(String email) {// 檢查發送頻率if (isVerificationCodeRateLimited(email)) {throw new RuntimeException("驗證碼發送太頻繁,請 60 秒后重試");}// 生成6位隨機驗證碼String verificationCode = RandomCodeUtil.generateNumberCode(6);// 實現異步發送郵件的邏輯try {// 創建郵件任務EmailTask emailTask = new EmailTask();// 初始化郵件任務內容// 1. 郵件目的郵箱// 2. 驗證碼// 3. 時間戳emailTask.setEmail(email);emailTask.setCode(verificationCode);emailTask.setTimestamp(System.currentTimeMillis());// 將郵件任務存入消息隊列// 1. 將任務對象轉成 JSON 字符串// 2. 將 JSON 字符串保存到 Redis 模擬的消息隊列中String emailTaskJson = objectMapper.writeValueAsString(emailTask);String queueKey = RedisKey.emailTaskQueue();redisTemplate.opsForList().leftPush(queueKey, emailTaskJson);// 設置 email 發送注冊驗證碼的限制String emailLimitKey = RedisKey.registerVerificationLimitCode(email);redisTemplate.opsForValue().set(emailLimitKey, "1", limitExpireSeconds, TimeUnit.SECONDS);return verificationCode;} catch (Exception e) {log.error("發送驗證碼郵件失敗", e);throw new RuntimeException("發送驗證碼失敗,請稍后重試");}
}

消費者部分

EmailTaskConsumer? 的實現

@Component
public class EmailTaskConsumer {@Autowiredprivate JavaMailSender mailSender;@Autowiredprivate ObjectMapper objectMapper;@Autowiredprivate RedisTemplate<String, String> redisTemplate;@Value("${spring.mail.username}")private String from;// 每 3 秒輪詢一次 redis,查看是否有待發的郵件任務@Scheduled(fixedDelay = 3000)public void resume() throws JsonProcessingException {String emailQueueKey = RedisKey.emailTaskQueue();// 從隊列中取任務對象while (true) {// 獲取任務對象String emailTaskJson = redisTemplate.opsForList().rightPop(emailQueueKey);if (emailTaskJson == null) {  // 隊列中沒有任務對象,退出本次執行break;}// 將 redis 中的 JSON 字符串轉成 emailTask 對象EmailTask emailTask = objectMapper.readValue(emailTaskJson, EmailTask.class);String email = emailTask.getEmail();String verificationCode = emailTask.getCode();// 根據 emailTask 對象中的信息// 填充 SimpleMailMessage 對象,然后使用 JavaMailSender 發送郵件SimpleMailMessage mailMessage = new SimpleMailMessage();mailMessage.setFrom(from);mailMessage.setTo(email);mailMessage.setSubject("卡碼筆記- 驗證碼");mailMessage.setText("您的驗證碼是:" + verificationCode + ",有效期" + 5 + "分鐘,請勿泄露給他人。");mailSender.send(mailMessage);// 保存驗證碼到 Redis// 有效時間為 5 分鐘redisTemplate.opsForValue().set(RedisKey.registerVerificationCode(email), verificationCode, 5, TimeUnit.MINUTES);}}
}

checkVerificationCode? 的實現

@Override
public boolean checkVerificationCode(String email, String code) {String redisKey = RedisKey.registerVerificationCode(email);String verificationCode = redisTemplate.opsForValue().get(redisKey);if (verificationCode != null && verificationCode.equals(code)) {redisTemplate.delete(redisKey);return true;}return false;
}

checkVerificationCode的實現邏輯很簡單,根據用戶的email查詢出redis種存儲的code,和用戶輸入的code,比較是否相等。在驗證成功后需要將Redis中的記錄刪除。

isVerificationCodeRateLimited? ?實現

@Override
public boolean isVerificationCodeRateLimited(String email) {String redisKey = RedisKey.registerVerificationLimitCode(email);return redisTemplate.opsForValue().get(redisKey) != null;
}

檢查Redis中是否存在email對應的限流鍵,存在則返回true表示已被限流

2. 排行榜

Spring + Mysql + Redis? 異步更新

  • 1.? 用戶提交筆記時,同步寫業務數據后異步發送一條消息到 Redis 列表(當作 MQ),消息是 JSON(包含 userId、score?增量、messageId、timestamp、noteId 等)。
  • 2.? 后臺消費進程用 BRPOP/阻塞彈出消息,解析后對 Redis 的 ZSET?做增量更新(ZINCRBY),ZSET 用于排行榜查詢。
  • 3.? 定時任務每小時把 Redis 中的?ZSET 批量持久化到 MySQL(通過 MyBatis?批量 upsert),以確保最終一致性和歷史存檔。

?

controller.java?

接收用戶提交并返回消息

@RestController
@RequestMapping("/notes")
public class NoteController {private final ProducerService producer;@PostMappingpublic ResponseEntity<?> submitNote(@RequestBody NoteSubmitDto dto){// 1. 處理業務寫入筆記表(略)// 2. 發送異步消息NoteMessage msg = new NoteMessage(...);producer.sendMessage(msg);return ResponseEntity.ok().build();}
}
Producer

(將消息 push 到 Redis 列表)

@Service
public class ProducerService {private static final String QUEUE = "mq:note:queue";private final RedisTemplate<String, String> redis;private final ObjectMapper mapper = new ObjectMapper();public ProducerService(RedisTemplate<String, String> redis){ this.redis = redis; }public void sendMessage(NoteMessage msg){try {String json = mapper.writeValueAsString(msg);redis.opsForList().leftPush(QUEUE, json);} catch (Exception e) {// 記錄失敗/監控}}
}

Consumer

(后臺阻塞消費,更新 ZSET)

@Service
public class RedisConsumerService {private static final String QUEUE = "mq:note:queue";private static final String DLQ = "mq:note:dlq";private final RedisTemplate<String, String> redis;private final LeaderboardService leaderboard;private final ObjectMapper mapper = new ObjectMapper();private volatile boolean running = true;public RedisConsumerService(RedisTemplate<String, String> redis, LeaderboardService leaderboard) {this.redis = redis; this.leaderboard = leaderboard;}@PostConstructpublic void start() {Thread t = new Thread(this::loop, "redis-mq-consumer");t.setDaemon(true);t.start();}private void loop() {while (running) {try {// 阻塞彈出(0 表示一直阻塞)String json = redis.opsForList().rightPop(QUEUE, 0, TimeUnit.SECONDS);if (json == null) continue;NoteMessage msg = mapper.readValue(json, NoteMessage.class);// 可做冪等校驗(基于 messageId)leaderboard.incrementScore(msg.getUserId(), msg.getDelta());} catch (Exception e) {// 解析或處理失敗 -> 推到死信隊列并記錄redis.opsForList().leftPush(DLQ, /* 原始消息 */);}}}@PreDestroypublic void stop() { running = false; }
}
LeaderboardService

(封裝 ZSET 操作)

@Service
public class LeaderboardService {private static final String ZKEY = "leaderboard:zset";private final RedisTemplate<String, String> redis;public LeaderboardService(RedisTemplate<String, String> redis){ this.redis = redis; }public void incrementScore(Long userId, double delta){redis.opsForZSet().incrementScore(ZKEY, userId.toString(), delta);}public Set<ZSetOperations.TypedTuple<String>> topN(int n){return redis.opsForZSet().reverseRangeWithScores(ZKEY, 0, n - 1);}public Set<ZSetOperations.TypedTuple<String>> rangeAllWithScores(){return redis.opsForZSet().rangeWithScores(ZKEY, 0, -1);}
}

DB 定時持久化

(每小時)

@Component
public class DbPersistScheduler {private final LeaderboardService leaderboardService;private final LeaderboardMapper mapper;public DbPersistScheduler(LeaderboardService leaderboardService, LeaderboardMapper mapper){this.leaderboardService = leaderboardService; this.mapper = mapper;}// 每小時執行一次(整點)@Scheduled(cron = "0 0 * * * ?")public void persistHourly() {// 讀取全部或 top K(注意數據量)Set<ZSetOperations.TypedTuple<String>> entries = leaderboardService.rangeAllWithScores();if (entries.isEmpty()) return;List<Leaderboard> list = entries.stream().map(t -> new Leaderboard(Long.valueOf(t.getValue()), t.getScore().longValue(), new Date())).collect(Collectors.toList());// 批量 upsert(MyBatis 實現)mapper.batchUpsert(list);}
}

?MyBatis Mapper?+ SQL
Mapper 接口:
Apply to HallScene.ts
public interface LeaderboardMapper {void batchUpsert(@Param("list") List<Leaderboard> list);// 可加查詢方法
}mapper XML(LeaderboardMapper.xml)<insert id="batchUpsert" parameterType="map">INSERT INTO leaderboard (user_id, score, updated_at)VALUES<foreach collection="list" item="item" separator=",">(#{item.userId}, #{item.score}, #{item.updatedAt})</foreach>ON DUPLICATE KEY UPDATEscore = VALUES(score),updated_at = VALUES(updated_at)
</insert>

  • 冪等與重復消息:消息包含?messageId,消費端在?Redis/DB 中做冪等檢查(如?SETNX messageId:processed 或 Redis?Hash?記錄已處理?id)。
  • 死信與重試:消費失敗推到?mq:note:dlq,人工或單獨重試流程處理。
  • 批量與限流:ZSET?的全量持久化當數據量大時會耗時,建議:
  • 只持久化 Top N(按需求),或分批(分頁?ZRANGE)。
  • 按用戶分區并并行持久化。
  • 持久化策略:可以每小時寫入 DB?覆蓋(ON DUPLICATE KEY),也可寫入增量表(記錄歷史)。
  • 數據一致性:在關機或部署前優雅停止消費線程并將?Redis?中的變更 flush 到 DB。
  • 性能:消費端更新 ZSET?為單條 Redis 請求,若高并發可用 pipeline 或本地批隊列合并多條消息后一起?ZINCRBY(減少網絡往返)。
  • 監控:記錄隊列長度(LLEN)、consumer 錯誤率、持久化耗時,設置報警。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/95967.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/95967.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/95967.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

前端處理導出PDF。Vue導出pdf

前言&#xff1a;該篇主要是解決一些簡單的頁面內容導出為PDF1.安裝依賴使用到兩個依賴&#xff0c;項目目錄下運行這兩個//頁面轉換成圖片 npm install --save html2canvas //圖片轉換成pdf npm install jspdf --save 2.創建通用工具類exportPdf.js文件可以保存在工具類目錄下…

【GM3568JHF】FPGA+ARM異構開發板燒錄指南

1. Windows燒錄說明 SDK 提供 Windows 燒寫工具(工具版本需要 V3.31或以上)&#xff0c;工具位于工程根目錄&#xff1a; tools/ ├── windows/RKDevTool 如下圖&#xff0c;編譯生成相應的固件后&#xff0c;設備燒寫需要進入 MASKROM 或 LOADER 燒寫模式&#xff0c;準備…

C++ 多進程編程深度解析【C++進階每日一學】

文章目錄一、引言二、核心概念&#xff1a;進程 (Process)功能與作用三、C 多進程的實現方式四、核心函數詳解1. fork() - 創建子進程函數原型功能說明返回值完整使用格式2. wait() 和 waitpid() - 等待子進程結束函數原型參數與返回值詳解3. exec 系列函數 - 執行新程序函數族…

一周學會Matplotlib3 Python 數據可視化-繪制面積圖(Area)

鋒哥原創的Matplotlib3 Python數據可視化視頻教程&#xff1a; 2026版 Matplotlib3 Python 數據可視化 視頻教程(無廢話版) 玩命更新中~_嗶哩嗶哩_bilibili 課程介紹 本課程講解利用python進行數據可視化 科研繪圖-Matplotlib&#xff0c;學習Matplotlib圖形參數基本設置&…

北京JAVA基礎面試30天打卡11

1.索引創建注意事項 適合的場景 1.頻繁使用where語句查詢的字段 2.關聯字段需要建立索 3.如果不創建索引&#xff0c;那么在連接的過程中&#xff0c;每個值都會進行一次全表掃描 4.分組和排序字段可以建立索引因為索引天生就是有序的&#xff0c;在分組和排序時優勢不言而喻 5…

vscode無法檢測到typescript環境解決辦法

有一個vitereacttypescript項目&#xff0c;在工作電腦上一切正常。但是&#xff0c;在我家里的電腦運行&#xff0c;始終無法檢測到typescript環境。即使出現錯誤的ts語法&#xff0c;也不會有報錯提示&#xff0c;效果如下&#xff1a;我故意將一個string類型&#xff0c;傳入…

【MCP開發】Nodejs+Typescript+pnpm+Studio搭建Mcp服務

MCP服務支持兩種協議&#xff0c;Studio和SSE/HTTP&#xff0c;目前官方提供的SDK有各種語言。 開發方式有以下幾種&#xff1a; 編程語言MCP命令協議發布方式PythonuvxSTUDIOpypiPython遠程調用SSE服務器部署NodejspnpmSTUDIOpnpmNodejs遠程調用SSE服務器部署… 一、初始化項…

vscode使用keil5出現變量跳轉不了和搜索全局不了

vscode使用keil5出現變量跳轉不了&#xff0c;或者未包含文件&#xff0c;或者未全局檢索&#xff1b; 參考如下文章后還會出現&#xff1b; 為什么vscode搜索欄只搜索已經打開的文件_vscode全局搜索只能搜當前文件-CSDN博客 在機緣巧合之下發現如下解決方式&#xff1a; 下載…

命名空間——網絡(net)

命名空間——網絡&#xff08;net&#xff09; 一、網絡命名空間&#xff1a;每個都是獨立的“網絡房間” 想象你的電腦是一棟大樓&#xff0c;每個網絡命名空間就是大樓里的一個“獨立房間”&#xff1a; 每個房間里有自己的“網線接口”&#xff08;網卡&#xff09;、“門牌…

一文讀懂16英寸筆記本的實際尺寸與最佳應用場景

當您搜索"16寸筆記本電腦長寬"時&#xff0c;內心真正在問的是什么&#xff1f;是背包能否容納&#xff1f;桌面空間是否足夠&#xff1f;還是期待屏幕尺寸與便攜性的完美平衡&#xff1f;這個看似簡單的尺寸數字背后&#xff0c;凝結著計算機制造商對用戶體驗的深刻…

Android Studio中創建Git分支

做一些Android項目時&#xff0c;有時候想要做一些實驗性的修改&#xff0c;這個實驗可能需要很多步驟&#xff0c;所以不是一時半會能完成的&#xff0c;這就需要在實驗的過程中不斷修改代碼&#xff0c;且要提交代碼&#xff0c;方便回滾或比較差異&#xff0c;但是既然是實驗…

內存可見性和偽共享問題

文章目錄什么是內存可見性問題為什么會出現可見性問題解決可見性問題的方法1. 使用volatile關鍵字2. 使用synchronized3. 使用java.util.concurrent包下的原子類什么是偽共享問題CPU緩存行偽共享的危害解決偽共享的方法1. 緩存行填充2. 使用Contended注解&#xff08;JDK 8&…

Spring MVC 九大組件源碼深度剖析(三):ThemeResolver - 動態換膚的奧秘

文章目錄一、主題機制的核心價值二、核心接口設計三、四大實現類源碼解析1. FixedThemeResolver&#xff08;固定主題策略&#xff09;2. CookieThemeResolver&#xff08;Cookie存儲策略&#xff09;3. SessionThemeResolver&#xff08;Session存儲策略&#xff09;4. Abstra…

一、Docker本地安裝

((這里引用知乎上大佬的說法&#xff1a;https://www.zhihu.com/question/48174633 服務器虛擬化解決的核心問題是資源調配&#xff0c;而容器解決的核心問題是應用開發、測試和部署。 一、參考帖子 Ubuntu 的 |Docker 文檔 【docker】ubuntu完全卸載docker及再次安裝_ubuntu…

LeetCode 分類刷題:2962. 統計最大元素出現至少 K 次的子數組

題目給你一個整數數組 nums 和一個 正整數 k 。請你統計有多少滿足 「 nums 中的 最大 元素」至少出現 k 次的子數組&#xff0c;并返回滿足這一條件的子數組的數目。子數組是數組中的一個連續元素序列。示例 1&#xff1a;輸入&#xff1a;nums [1,3,2,3,3], k 2 輸出&#…

10分鐘掌握swift

整理一個 10分鐘掌握 Swift 的精華指南&#xff0c;用一個 Demo 串聯 Swift 的核心語法、數據結構、函數、類/結構體和閉包&#xff0c;讓你快速入門。1?? 基礎語法與變量import Foundation // 引入基礎庫// 變量和常量 var name: String "Alice" // 可變 let…

【完整源碼+數據集+部署教程】食品分類與實例分割系統源碼和數據集:改進yolo11-AggregatedAttention

背景意義 研究背景與意義 隨著全球食品產業的快速發展&#xff0c;食品安全和質量控制日益成為社會關注的焦點。食品分類與實例分割技術的應用&#xff0c;能夠有效提升食品識別的準確性和效率&#xff0c;為食品監管、營養分析以及智能餐飲等領域提供重要支持。傳統的食品識別…

C# 中的N+1問題

目錄 含義 影響 避免方法 1. 立即加載&#xff08;Eager Loading&#xff09; 2. 顯式加載&#xff08;Explicit Loading&#xff09; 3. 投影&#xff08;Projection&#xff09; 4. 批處理查詢 5. 禁用延遲加載 含義 N1 問題 是 ORM&#xff08;對象關系映射&#x…

國內多光譜相機做得好的廠家有哪些?-多光譜相機品牌廠家

多光譜相機是一種能夠同時捕捉多個特定波段的光譜信息&#xff0c;這些波段覆蓋可見光、近紅外以及短波紅外等區域。廣泛應用于遙感、農業、環境監測、工業檢測、安防等領域。近年來&#xff0c;我國在多光譜技術領域取得了顯著進步&#xff0c;涌現出一批技術實力強、產品性能…

如何用外部電腦訪問本地網頁?

之前本來說用內網穿透工具來查看完成這個工具&#xff0c;結果感覺各種不符合心意&#xff0c;突然發現有更簡單的方法。如果想讓兩臺電腦在 同一局域網 內都能訪問運行在 http://localhost:5174/ 上的項目&#xff0c;而不需要使用內網穿透工具&#xff0c;可以通過以下方法實…