我是3y,一年CRUD
經驗用十年的markdown
程序員👨🏻💻常年被譽爲優質八股文選手
今天繼續更新austin項目,如果還沒看過該系列的同學可以點開我的歷史文章回顧下,在看的過程中不要忘記了點贊喲!建議不要漏了或者跳着看,不然這篇就看不懂了,之前寫過的知識點和業務我就不再贅述啦。
今天要實現的是handler
消費消息後,實現平臺性去重的功能。
01、什麼是去重和冪等
這個話題我之前在《對線面試官》系列就已經分享過了,這塊面試也會經常問到,可以再跟大家一起復習下
「冪等」和「去重」的本質:「唯一Key」+「存儲」
唯一Key如何構建以及選擇用什麼存儲,都是業務決定的。「本地緩存」如果業務合適,可以作爲「前置」篩選出一部分,把其他存儲作爲「後置」,用這種模式來提高性能。
今日要聊的Redis,它擁有着高性能讀寫,前置篩選和後置判斷均可,austin項目的去重功能就是依賴着Redis而實現的。
02、安裝Redis
先快速過一遍Redis的使用姿勢吧(如果對此不感興趣的可以直接跳到05講解相關的業務和代碼設計)
安裝Redis的環境跟上次Kafka是一樣的,爲了方便我就繼續用docker-compose
的方式來進行啦。
環境:
CentOS 7.6 64bit
首先,我們新建一個文件夾redis
,然後在該目錄下創建出data
文件夾、redis.conf
文件和docker-compose.yaml
文件
redis.conf
文件的內容如下(後面的配置可在這更改,比如requirepass 我指定的密碼爲austin
)
protected-mode no
port 6379
timeout 0
save 900 1
save 300 10
save 60 10000
rdbcompression yes
dbfilename dump.rdb
dir /data
appendonly yes
appendfsync everysec
requirepass austin
docker-compose.yaml
的文件內容如下:
version: '3'
services:
redis:
image: redis:latest
container_name: redis
restart: always
ports:
- 6379:6379
volumes:
- ./redis.conf:/usr/local/etc/redis/redis.conf:rw
- ./data:/data:rw
command:
/bin/bash -c "redis-server /usr/local/etc/redis/redis.conf "
配置的工作就完了,如果是雲服務器,記得開redis端口6379
03、啓動Redis
啓動Redis跟之前安裝Kafka的時候就差不多啦
docker-compose up -d
docker ps
docker exec -it redis redis-cli
進入redis客戶端了之後,我們想看驗證下是否正常。(在正式輸入命令之前,我們需要通過密碼校驗,在配置文件下配置的密碼是austin
)
然後隨意看看命令是不是正常就OK啦
04、Java中使用Redis
在SpringBoot環境下,使用Redis就非常簡單了(再次體現出使用SpringBoot的好處)。我們只需要在pom文件下引入對應的依賴,並且在配置文件下配置host
/port
和password
就搞掂了。
對於客戶端,我們就直接使用RedisTemplate就好了,它是對客戶端的高度封裝,已經挺好使的了。
05、去重功能業務
任何的功能代碼實現都離不開業務場景,在聊代碼實現之前,先聊業務!平時在做需求的時候,我也一直信奉着:先搞懂業務要做什麼,再實現功能。
去重該功能在austin項目裏我是把它定位是:平臺性功能。要理解這點很重要!不要想着把業務的各種的去重邏輯都在平臺上做,這是不合理的。
這裏只能是把共性的去重功能給做掉,跟業務強掛鉤應由業務方自行實現。所以,我目前在這裏實現的是:
- 5分鐘內相同用戶如果收到相同的內容,則應該被過濾掉。實現理由:很有可能由於MQ重複消費又或是業務方不謹慎調用,導致相同的消息在短時間內被austin消費,進而發送給用戶。有了該去重,我們可以在一定程度下減少事故的發生。
- 一天內相同的用戶如果已經收到某渠道內容5次,則應該被過濾掉。實現理由:在運營或者業務推送下,有可能某些用戶在一天內會多次收到推送消息。避免對用戶帶來過多的打擾,從總體定下規則一天內用戶只能收到N條消息。
不排除隨着業務的發展,還有些需要我們去做的去重功能,但還是要記住,我們這裏不跟業務強掛鉤。
當我們的核心功能依賴其他中間件的時候,我們儘可能避免由於中間件的異常導致我們核心的功能無法正常使用。比如,redis如果掛了,也不應該影響我們正常消息的下發,它只能影響到去重的功能。
06、去重功能代碼總覽
在之前,我們已經從Kafka拉取消息後,然後把消息放到各自的線程池進行處理了,去重的功能我們只需要在發送之前就好了。
我將去重的邏輯統一抽象爲:在X時間段內達到了Y閾值。去重實現的步驟可以簡單分爲:
- 從Redis獲取記錄
- 判斷Redis存在的記錄是否符合條件
- 符合條件的則去重,不符合條件的則重新塞進Redis
這裏我使用的是模板方法模式,deduplication
方法已經定義好了定位,當有新的去重邏輯需要接入的時候,只需要繼承AbstractDeduplicationService
來實現deduplicationSingleKey
方法即可。
比如,我以相同內容發送給同一個用戶的去重邏輯爲例:
07、去重代碼具體實現
在這場景下,我使用Redis都是用批量操作來減少請求Redis的次數的,這對於我們這種業務場景(在消費的時候需要大量請求Redis,使用批量操作提升還是很大的)
由於我覺得使用的場景還是蠻多的,所以我封裝了個RedisUtils工具類,並且可以發現的是:我對操作Redis的地方都用try catch
來包住。即便是Redis出了故障,我的核心業務也不會受到影響。
08、你的代碼有Bug!
不知道看完上面的代碼你們有沒有看出問題,有喜歡點讚的帥逼就很直接看出兩個問題:
- 你的去重功能爲什麼是在發送消息之前就做了?萬一你發送消息失敗了怎麼辦?
- 你的去重功能存在併發的問題吧?假設我有兩條一樣的消息,消費的線程有多個,然後該兩條線程同時查詢Redis,發現都不在Redis內,那這不就有併發的問題嗎
沒錯,上面這兩個問題都是存在的。但是,我這邊都不會去解決。
先來看第一個問題:
對於這個問題,我能扯出的理由有兩個:
-
假設我發送消息失敗了,在該系統也不會通過回溯MQ的方式去重新發送消息(回溯MQ重新消費影響太大了)。我們完全可以把發送失敗的
userId
給記錄下來(後面會把相關的日誌系統給完善),有了userId
以後,我們手動批量重新發就好了。這裏手動也不需要業務方調用接口,直接通過類似excel
的方式導入就好了。 -
在業務上,很多發送消息的場景即便真的丟了幾條數據,都不會被發現。有的消息很重要,但有更多的消息並沒那麼重要,並且我們即便在調用接口才把數據寫入Redis,但很多渠道的消息其實在調用接口後,也不知道是否真正發送到用戶上了。
再來看第二個問題:
如果我們要僅靠Redis來實現去重的功能,想要完全沒有併發的問題,那得上lua
腳本,但上lua
腳本是需要成本的。去重的實現需要依賴兩個操作:查詢和插入。查詢後如果沒有,則需要添加。那查詢和插入需要保持原子性才能避免併發的問題
再把視角拉回到我們爲什麼要實現去重功能:
當存在事故的時候,我們去重能一定保障到絕大多數的消息不會重複下發。對於整體性的規則,併發消息發送而導致規則被破壞的概率是非常的低。
09、總結
這篇文章簡要講述了Redis的安裝以及在SpringBoot中如何使用Redis,主要說明了爲什麼要實現去重的功能以及代碼的設計和功能的具體實現。
技術是離不開業務的,有可能我們設計或實現的代碼對於強一致性是有疏漏的,但如果系統的整體是更簡單和高效,且業務可接受的時候,這不是不可以的。
這是一種trade-off
權衡,要保證數據不丟失和不重複一般情況是需要編寫更多的代碼和損耗系統性能等才能換來的。我可以在消費消息的時候實現at least once
語義,保證數據不丟失。我可以在消費消息的時候,實現真正的冪等,下游調用的時候不會重複。
但這些都是有條件的,要實現at least once
語義,需要手動ack
。要實現冪等,需要用redis lua
或者把記錄寫入MySQL
構建唯一key並把該key設置唯一索引。在訂單類的場景是必須的,但在一個核心發消息的系統裏,可能並沒那麼重要。
No Bug,All Feature!
歡迎關注我的微信公衆號【Java3y】來聊聊Java面試,對線面試官系列持續更新中!
【對線面試官+從零編寫Java項目】 持續高強度更新中!求star!!原創不易!!求三連!!
Gitee鏈接:https://gitee.com/austin
GitHub鏈接:https://github.com/austin