我是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專案】 持續高強度更新中!求star!!原創不易!!求三連!!
原始碼Gitee連結:gitee.com/austin
原始碼GitHub連結:github.com/austin