我是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
更多的文章可往:文章的目錄導航轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/430980.html
標籤:Java
上一篇:裸辭全職接單一個月的感受 !
