據(jù)質(zhì)量排查實(shí)戰(zhàn):全一字符串“111111111111”的成因與處置)
我拿到這個(gè)字段值的第一反應(yīng)是懷疑自己眼花了一長(zhǎng)串“111111111111”不長(zhǎng)不短正好12位既不像隨機(jī)生成的ID也不像任何業(yè)務(wù)編碼。找了一圈資料發(fā)現(xiàn)它在系統(tǒng)里出現(xiàn)的頻率比想象中高得多——測(cè)試環(huán)境的Mock數(shù)據(jù)里有它接口返回的示例報(bào)文里有它生產(chǎn)庫(kù)某個(gè)表的默認(rèn)值字段里也躺著它。十二個(gè)“1”連在一起看起來像是手滑打出來的亂碼但它其實(shí)是一個(gè)典型的占位符和測(cè)試樁串專門用來做無(wú)腦填數(shù)的。這篇文章不打算講什么高深理論就從一個(gè)讓我折騰了兩天的“全一字符串”項(xiàng)目說起聊聊它到底是從哪混進(jìn)來的怎么排查以及踩過的坑。1. 拿到“111111111111”后的第一次排查先把它當(dāng)字段不是亂碼1.1 為什么一串“1”比一串“0”更讓人警惕先說直覺上的差別。系統(tǒng)里如果出現(xiàn)一列“000000000000”大部分人第一反應(yīng)是“空值、未填、數(shù)據(jù)還沒生效”頂多當(dāng)成臟數(shù)據(jù)清理掉。但換成“111111111111”很多人反而會(huì)猶豫因?yàn)橛行I(yè)務(wù)系統(tǒng)確實(shí)會(huì)用“1”表示“是”“啟用”“正常”這類含義全一字符串很容易被誤讀成“某種有效狀態(tài)”。我那次接到任務(wù)時(shí)業(yè)務(wù)方反饋說某個(gè)報(bào)表里出現(xiàn)了一個(gè)非常奇怪的“客戶編號(hào)”點(diǎn)進(jìn)去關(guān)聯(lián)任何用戶都匹配不上。當(dāng)時(shí)我先做了最基礎(chǔ)的探查查詢發(fā)現(xiàn)這個(gè)字符串在近三個(gè)月的記錄里出現(xiàn)了幾百次分布在多個(gè)庫(kù)表里字段類型還不同有的是VARCHAR有的是BIGINT甚至還有JSON里的字符串值。真正讓我警覺的不是它出現(xiàn)得多而是它跨了那么多表說明它很可能不是某個(gè)人手動(dòng)填出來的而是某個(gè)公共邏輯統(tǒng)一寫進(jìn)去的。把“111111111111”當(dāng)成字段值去排查而不是當(dāng)成數(shù)字去理解是我踩了兩次坑后總結(jié)出來的第一個(gè)原則。因?yàn)橐坏┠阆热霝橹靼阉?dāng)成“一億一千一百一十一萬(wàn)一千一百一十一”就會(huì)老想著去查業(yè)務(wù)里有沒有這么大數(shù)字的編號(hào)結(jié)果繞了一圈發(fā)現(xiàn)它就是一個(gè)字符串常量。程序里它出現(xiàn)在哪里比它本身等于多少更重要。1.2 排查之前先問自己三個(gè)問題真正開始動(dòng)手之前我習(xí)慣先把三個(gè)問題寫在文檔里避免自己像無(wú)頭蒼蠅一樣到處查。第一個(gè)問題是它到底是字符串還是數(shù)字這個(gè)決定了后續(xù)所有查詢和替換語(yǔ)法。在MySQL里字段是VARCHAR(50)時(shí)查詢要寫成field_value 111111111111如果是BIGINT那么field_value 111111111111也能命中但一旦字段值變成帶前導(dǎo)零的形式數(shù)字類型會(huì)直接把它吃掉所以必須先確認(rèn)建表語(yǔ)句。第二個(gè)問題是它出現(xiàn)了多久是一直都有還是某次發(fā)布之后才開始出現(xiàn)的這個(gè)時(shí)間點(diǎn)非常關(guān)鍵。我那次通過備份庫(kù)對(duì)比發(fā)現(xiàn)全一字符串是在某次接口聯(lián)調(diào)上線后突然多起來的這就把懷疑范圍縮小到了那個(gè)新接入的第三方服務(wù)上。第三個(gè)問題是它所在字段的業(yè)務(wù)含義是什么是主鍵、外鍵、狀態(tài)碼、金額、備注還是預(yù)留字段不同的業(yè)務(wù)含義對(duì)應(yīng)不同的處理方式。如果它是主鍵出現(xiàn)重復(fù)值說明數(shù)據(jù)寫入邏輯有嚴(yán)重問題如果它是狀態(tài)碼可能只是定義了“未知”這種特殊情況如果它是備注那更多是測(cè)試數(shù)據(jù)殘留。這三個(gè)問題問完基本上就能判斷這是個(gè)小范圍數(shù)據(jù)問題還是一個(gè)需要改代碼的邏輯問題。接下來要做的就是搞清楚它的來源。2. 全一字符串的三大出身測(cè)試樁、業(yè)務(wù)占位值、異?;靥?.1 測(cè)試樁聯(lián)調(diào)環(huán)境里的“無(wú)腦數(shù)據(jù)”傳遍全鏈路“111111111111”最常見的一個(gè)來源就是開發(fā)同事在聯(lián)調(diào)時(shí)寫的測(cè)試樁。比如對(duì)接一個(gè)外部客戶系統(tǒng)對(duì)方還沒把真實(shí)接口準(zhǔn)備好本地先Mock一份返回結(jié)果圖省事就把所有字段都填了“1”。這種數(shù)據(jù)一般只存在于本地配置或測(cè)試環(huán)境但有一種情況它會(huì)流到生產(chǎn)環(huán)境測(cè)試環(huán)境和生產(chǎn)環(huán)境共用了一套配置中心或者某個(gè)配置項(xiàng)沒有做環(huán)境隔離。我遇到過不止一次上游服務(wù)在測(cè)試環(huán)境返回{customerId: 111111111111, name: test}然后因?yàn)榫W(wǎng)關(guān)的路由配置漏改聯(lián)調(diào)請(qǐng)求被轉(zhuǎn)發(fā)到了生產(chǎn)環(huán)境的部分節(jié)點(diǎn)于是一批真實(shí)業(yè)務(wù)記錄被寫進(jìn)了這個(gè)占位客戶ID。這種情況最坑的地方在于日志里看是正常的接口也返回成功但下游數(shù)據(jù)全是假的。排查時(shí)需要重點(diǎn)看公共的Mock配置、環(huán)境變量和配置中心的覆蓋關(guān)系不能只盯著代碼本身。2.2 業(yè)務(wù)占位值默認(rèn)值和示例值之間的邊界模糊第二種來源是業(yè)務(wù)系統(tǒng)在設(shè)計(jì)初期就把某些字段的默認(rèn)值設(shè)成了全一字符串。比如新用戶注冊(cè)后在用戶信息尚未完善之前系統(tǒng)給“證件號(hào)”字段先塞一個(gè)“111111111111”或者訂單系統(tǒng)在接入支付渠道之前先把“渠道流水號(hào)”置為這個(gè)占位值。這種做法的本意是“等我拿到真實(shí)數(shù)據(jù)再覆蓋”但它有兩個(gè)天然缺陷。第一如果后續(xù)邏輯里沒有強(qiáng)制校驗(yàn)“這個(gè)字段是否還是占位值”那么占位值就會(huì)一路順到下游報(bào)表、對(duì)賬文件、短信模板里。第二占位值一旦暴露給外部用戶很容易被當(dāng)作一個(gè)可用的“萬(wàn)能編號(hào)”引發(fā)批量重試或惡意嘗試。我當(dāng)時(shí)就見過一個(gè)渠道對(duì)賬文件里出現(xiàn)了三萬(wàn)多筆“111111111111”流水號(hào)導(dǎo)致財(cái)務(wù)系統(tǒng)把這三萬(wàn)筆全部匹配到同一筆訂單上。所以占位符最好帶明顯的前綴比如“TMP111111111111”或者直接在字段注釋里說明“該值僅為初始化占位上線前必須替換”。否則全一串在人工排查時(shí)幾乎沒有任何區(qū)分度。2.3 異?;靥頲atch塊里的默認(rèn)初始化最容易漏第三種來源容易被代碼評(píng)審漏掉就是異常分支里的默認(rèn)賦值。很多程序員在寫catch塊時(shí)習(xí)慣初始化一個(gè)返回值比如String customerId 111111111111;然后異常發(fā)生時(shí)直接返回這個(gè)初始值。這種代碼本身問題不大問題在于如果后續(xù)沒有打日志也沒有監(jiān)控調(diào)用方拿到的是一個(gè)看起來合法但實(shí)際無(wú)效的值而它可能被直接寫入數(shù)據(jù)庫(kù)或傳給外部系統(tǒng)。我排查過一個(gè)特別隱蔽的案例某個(gè)定時(shí)任務(wù)每晚拉取第三方名單正常情況下會(huì)更新名單狀態(tài)但某天第三方接口超時(shí)catch塊就把名單狀態(tài)字段置為全一字符串然后程序繼續(xù)往下執(zhí)行還把狀態(tài)寫到了更新表里。第二天對(duì)賬時(shí)幾百條記錄的狀態(tài)全是“111111111111”一眼看過去根本不知道發(fā)生了什么。后來我在catch塊里強(qiáng)制要求異常情況下必須拋錯(cuò)或者至少寫入一個(gè)明確帶錯(cuò)誤碼的標(biāo)識(shí)絕不允許使用正常業(yè)務(wù)字段的占位值作為異常返回值。這種方式判斷起來其實(shí)很簡(jiǎn)單搜一下項(xiàng)目里有沒有直接把一串“1”作為常量賦值的代碼看它出現(xiàn)在哪里。如果出現(xiàn)在try塊里還好出現(xiàn)在catch塊里那基本就是隱患。3. 從數(shù)據(jù)類型的角度重新認(rèn)識(shí)十二個(gè)13.1 二進(jìn)制里的12個(gè)14095、-1與“全置位”為什么偏偏是12個(gè)“1”而不是11個(gè)或13個(gè)我查過很多系統(tǒng)發(fā)現(xiàn)12位全1并不是隨手敲的而是有一定技術(shù)來源的。比如在一些硬件對(duì)接場(chǎng)景里寄存器位寬是12位全1表示“所有信號(hào)置位”對(duì)應(yīng)到十進(jìn)制就是4095。如果把“111111111111”看作二進(jìn)制數(shù)它的值就是4095如果按照補(bǔ)碼表示法12位全1在某些語(yǔ)境下代表“-1”。這個(gè)知識(shí)點(diǎn)聽著有點(diǎn)繞但它實(shí)際影響了好幾個(gè)判斷。比如我看到一個(gè)配置文件里寫了0b111111111111第一反應(yīng)不是“這是一個(gè)大數(shù)字”而是“有人想表達(dá)一個(gè)全置位的掩碼”。在Linux的權(quán)限管理里0777這類全置位表示所有用戶有全部權(quán)限在狀態(tài)碼設(shè)計(jì)里全1往往表示“未知”或“所有狀態(tài)的總和”。所以“111111111111”可以被看成是“狀態(tài)全部打開”的隱喻而不只是數(shù)字大。不過在業(yè)務(wù)數(shù)據(jù)里它更多只是一個(gè)普通的字符串。真正需要注意的是不要把二進(jìn)制語(yǔ)境下的“全1”理解成“正?!币膊灰?yàn)樗谀承┧惴ɡ锏扔?095就覺得它是個(gè)合法的枚舉值。不同場(chǎng)景下的含義必須分開看。3.2 數(shù)字類型與字符串類型之間的隱性轉(zhuǎn)換不同類型字段存同樣一串字符行為完全不一樣。VARCHAR字段老老實(shí)實(shí)存了“111111111111”BIGINT字段存的是數(shù)字111111111111兩者在數(shù)據(jù)庫(kù)里查詢時(shí)都能命中但一旦涉及到外部對(duì)接問題就來了。比如某個(gè)系統(tǒng)用JSON接口傳輸前端先把一個(gè)字符串類型的customerId塞進(jìn)對(duì)象后端的實(shí)體類卻把它定義成了Long序列化框架就會(huì)自動(dòng)把111111111111轉(zhuǎn)成數(shù)字111111111111。如果某個(gè)字段是12個(gè)1帶前導(dǎo)零比如011111111111轉(zhuǎn)成Long后前導(dǎo)零直接丟失再轉(zhuǎn)回字符串就變成了“11111111111”和原來的值對(duì)不上。這種隱式轉(zhuǎn)換是大量“看起來數(shù)據(jù)沒變但實(shí)際上變了”的根源。另一個(gè)真實(shí)場(chǎng)景是SQL查詢里的隱式轉(zhuǎn)換。如果一個(gè)表的關(guān)聯(lián)字段是VARCHAR另一個(gè)表的關(guān)聯(lián)字段是BIGINT直接用a.customer_id b.customer_id做關(guān)聯(lián)MySQL會(huì)用cast把字符串轉(zhuǎn)成數(shù)字來比較。這時(shí)候“111111111111”和“111111111112”之類的值可能被截?cái)嗷蜣D(zhuǎn)成浮點(diǎn)數(shù)導(dǎo)致關(guān)聯(lián)結(jié)果莫名其妙變多。解決方式就是把關(guān)聯(lián)字段統(tǒng)一改成同一種類型或者查詢時(shí)顯式加上類型轉(zhuǎn)換不要依賴數(shù)據(jù)庫(kù)的默認(rèn)行為。3.3 “全0”與“全1”在業(yè)務(wù)里的對(duì)稱語(yǔ)義很多系統(tǒng)里全0字符串表示“空”全1字符串表示“占位”。這兩者看起來是對(duì)稱的但在數(shù)據(jù)質(zhì)量上的風(fēng)險(xiǎn)完全不同。全0幾乎不會(huì)和真實(shí)業(yè)務(wù)數(shù)據(jù)混淆因?yàn)榇蠖鄶?shù)真實(shí)編碼不會(huì)執(zhí)著于全0但全1在某些規(guī)則下可能撞上合法值。比如手機(jī)號(hào)段里雖然不會(huì)出現(xiàn)12個(gè)1但某些內(nèi)部系統(tǒng)中“111111111111”可能恰好是某個(gè)測(cè)試賬號(hào)、某個(gè)特殊設(shè)備編號(hào)。我還遇到過一種對(duì)稱語(yǔ)義字段值為全0時(shí)前端頁(yè)面顯示為“未填寫”字段值為全1時(shí)前端頁(yè)面顯示為“全部”。這種設(shè)計(jì)在篩選項(xiàng)里特別常見比如用戶ID傳全1表示“不區(qū)分用戶”產(chǎn)品線ID傳全1表示“所有產(chǎn)品線”。如果后端把這種“語(yǔ)義上的全部”落庫(kù)報(bào)表統(tǒng)計(jì)時(shí)就會(huì)把所有數(shù)據(jù)歸到一類導(dǎo)致匯總結(jié)果完全失真。所以看到全1時(shí)不能只問“這是不是臟數(shù)據(jù)”還要問“它在某個(gè)配置里是否被定義為通配符”。4. 一套完整處置流程定位、溯源、修復(fù)、防復(fù)發(fā)4.1 定位SQL先看分布不急著刪拿到這類問題我建議先跑一遍分布統(tǒng)計(jì)而不是直接寫UPDATE把所有值改掉。分布統(tǒng)計(jì)的目的有兩個(gè)一是確認(rèn)影響范圍二是判斷它是持續(xù)寫入還是歷史遺留。以MySQL為例可以先查SELECT COUNT(*) AS total_cnt, COUNT(DISTINCT related_table) AS table_cnt, MIN(create_time) AS first_seen, MAX(create_time) AS last_seen FROM order_info WHERE customer_no 111111111111;這個(gè)結(jié)果能告訴我們這個(gè)值最早出現(xiàn)和最后出現(xiàn)的時(shí)間。如果最近兩天還在持續(xù)出現(xiàn)說明上游寫入邏輯沒有停如果只存在于某個(gè)歷史時(shí)間段可能只是某次發(fā)布或測(cè)試造成的殘留。我習(xí)慣把這個(gè)SQL復(fù)制到每個(gè)相關(guān)庫(kù)都跑一遍然后把表名、行數(shù)和時(shí)間范圍記在wiki里形成一個(gè)影響面清單。這個(gè)過程不要急著用UPDATE因?yàn)橐坏└腻e(cuò)了要回滾的成本很高。4.2 溯源日志、接口說明、配置項(xiàng)三條線索分布查完以后下一步是找源頭。我常用的排查路徑是先在代碼倉(cāng)庫(kù)里全局搜索這個(gè)字符串看有沒有定義grep -rn 111111111111 --include*.java --include*.xml --include*.yml --include*.properties .搜索結(jié)果會(huì)出現(xiàn)幾類文件常量類、Mock工具類、配置文件和測(cè)試用例。先從配置文件開始看因?yàn)榕渲庙?xiàng)往往會(huì)被多環(huán)境復(fù)用。比如某個(gè)third-party.yml里配置了默認(rèn)的appId或userId一旦這個(gè)配置沒有按環(huán)境拆分生產(chǎn)環(huán)境就會(huì)拉到測(cè)試值。接口說明也要看尤其是對(duì)接外部系統(tǒng)時(shí)對(duì)方返回的報(bào)文字段里如果帶了這個(gè)值大概率是對(duì)方測(cè)試環(huán)境沒切換干凈。日志則用來確認(rèn)具體的寫入鏈路在日志平臺(tái)搜索這個(gè)值關(guān)聯(lián)的requestId或traceId就能定位到是哪一次調(diào)用的哪個(gè)方法把它寫入數(shù)據(jù)庫(kù)的。三條線索不一定每一條都有結(jié)果但至少能回答“誰(shuí)寫的、走哪條鏈路、什么時(shí)候?qū)戇M(jìn)來”這三個(gè)問題。4.3 修復(fù)謹(jǐn)慎替換保留備份與審計(jì)定位到寫入邏輯之后真正的修復(fù)工作有兩條線一條是修代碼另一條是修數(shù)據(jù)。修代碼的時(shí)候我會(huì)把所以使用“111111111111”作為占位值的地方列出來逐個(gè)判斷它是否合法。比如有些測(cè)試代碼里的Mock數(shù)據(jù)需要保留有些生產(chǎn)代碼里的異常返回需要改成明確的錯(cuò)誤碼有些字段的默認(rèn)值需要改成業(yè)務(wù)上更安全的值。修數(shù)據(jù)時(shí)推薦先把涉及的表備份或者至少生成一份包含主鍵和原值的數(shù)據(jù)文件。然后按照業(yè)務(wù)規(guī)則決定替換成什么可能是NULL、空字符串也可能是某個(gè)真實(shí)編碼。要注意的是不同表的替換值可能不一樣不能一個(gè)UPDATE走天下。比如訂單表里的客戶編號(hào)替換成空字符串可以但報(bào)表表里的分組字段替換成空字符串會(huì)導(dǎo)致分組失敗這時(shí)替換成“UNKNOWN”更合適。每次替換都更新一條審計(jì)記錄事后如果發(fā)現(xiàn)有誤還能精準(zhǔn)回滾。4.4 防復(fù)發(fā)加約束比加監(jiān)控更前置修完數(shù)據(jù)只是解決了“存量”更重要的是解決“增量”。我現(xiàn)在的習(xí)慣是優(yōu)先在數(shù)據(jù)庫(kù)層面對(duì)這類字段加約束方案比加監(jiān)控更前置。比如在寫庫(kù)之前用一個(gè)統(tǒng)一的校驗(yàn)方法判斷字段是否命中保留值清單命中則直接報(bào)錯(cuò)。在數(shù)據(jù)庫(kù)層面如果字段本身是唯一標(biāo)識(shí)就加唯一索引如果不是唯一標(biāo)識(shí)但業(yè)務(wù)上不允許某些特殊值可以加CHECK約束。當(dāng)然CHECK約束在MySQL 8.0以前很多版本是不強(qiáng)制生效的所以還需要在應(yīng)用層兜底。更實(shí)用的方式是寫一個(gè)簡(jiǎn)單的掃描任務(wù)定期執(zhí)行下面的SQLSELECT table_name, column_name, COUNT(*) FROM information_schema.columns WHERE table_schema your_db AND column_name IN (customer_no, order_id, channel_flow_no);然后再對(duì)每個(gè)命中列跑一次分組統(tǒng)計(jì)看有沒有出現(xiàn)保留值。這種巡檢不用天天跑一周一次就夠了。真正把增量控制住之后數(shù)據(jù)庫(kù)里那種“滿屏都是1”的壓抑感才會(huì)慢慢消失。5. 實(shí)踐中容易踩的坑三個(gè)真實(shí)案例5.1 一把梭替換毀掉了合法測(cè)試賬號(hào)第一次處理全一字符串時(shí)我圖省事直接用一條UPDATE把所有表的“111111111111”全部替換成了NULL。結(jié)果第二天就有同事來找我說他們有一套聯(lián)調(diào)賬號(hào)客戶編號(hào)固定就是這個(gè)全一串他們靠這個(gè)值在測(cè)試環(huán)境里定位數(shù)據(jù)被我當(dāng)成臟數(shù)據(jù)清掉以后整套測(cè)試用例全亂了。這個(gè)教訓(xùn)讓我明白了兩件事一是不能只從“我這張表”的視角判斷一個(gè)值是否合法它可能在另一個(gè)系統(tǒng)里就是合法值二是在處理跨表數(shù)據(jù)時(shí)一定要做一個(gè)白名單確認(rèn)哪些環(huán)境、哪些表、哪些字段允許出現(xiàn)這個(gè)占位符哪些不允許?,F(xiàn)在我再處理這類值都會(huì)先在文檔里寫清楚“排除清單”比如測(cè)試庫(kù)的mock表、配置中心里的固定值、歷史歸檔表這些地方即使出現(xiàn)全一串也不能動(dòng)。5.2 弱類型語(yǔ)言把字符串當(dāng)數(shù)字導(dǎo)致條件判斷失效有段時(shí)間系統(tǒng)里有一段PHP代碼判斷客戶編號(hào)是否有效用的是if ($customerNo)這個(gè)寫法在正常字符串下沒問題但如果某個(gè)接口返回了“111111111111”這個(gè)條件永遠(yuǎn)為真邏輯就會(huì)走向“客戶有效”分支。后來數(shù)值型客戶編號(hào)被轉(zhuǎn)成了字符串本身沒有問題但另一個(gè)同事接手后把這個(gè)字符串又和數(shù)字做了比較PHP的弱類型轉(zhuǎn)換規(guī)則在比較時(shí)會(huì)嘗試把能轉(zhuǎn)成數(shù)字的字符串轉(zhuǎn)成數(shù)字結(jié)果全一串依然能通過校驗(yàn)。這個(gè)坑的本質(zhì)是寫代碼的人以為自己在判斷“這個(gè)編號(hào)是否存在”但實(shí)際上判斷的是“這個(gè)值是否非空”。要避免它只能改成顯式判斷比如$customerNo ! null $customerNo ! 并且把“111111111111”這類保留值放進(jìn)黑名單統(tǒng)一走無(wú)效分支。弱類型語(yǔ)言的寬松是一件好事但遇到這種字段語(yǔ)義判斷時(shí)寬松反而容易掩蓋臟數(shù)據(jù)。5.3 Excel導(dǎo)入的“科學(xué)計(jì)數(shù)法”事故還有一次不是數(shù)據(jù)庫(kù)問題而是數(shù)據(jù)交換時(shí)的格式問題。業(yè)務(wù)方從系統(tǒng)導(dǎo)出CSV在Excel里打開里面的“111111111111”被默認(rèn)識(shí)別為數(shù)字右對(duì)齊顯示成“111111111111”看起來還行。但有人習(xí)慣性地在Excel里改了一個(gè)無(wú)關(guān)字段另存為CSV之后這一個(gè)數(shù)字被寫成了科學(xué)計(jì)數(shù)法形式。重新導(dǎo)入數(shù)據(jù)庫(kù)時(shí)程序按照字符串解析讀出來的值變成了“1.11E11”和原值對(duì)不上。這個(gè)問題的根源是CSV并非強(qiáng)類型格式Excel打開和保存時(shí)會(huì)改變數(shù)值的表現(xiàn)形式。解決辦法只有一個(gè)在導(dǎo)出SQL時(shí)對(duì)這類字段加一層格式化比如用CONCAT(\t, customer_no)或強(qiáng)制轉(zhuǎn)成字符串并加上雙引號(hào)讓Excel把它當(dāng)文本處理。另外導(dǎo)入端的校驗(yàn)代碼也應(yīng)該容錯(cuò)識(shí)別出科學(xué)計(jì)數(shù)法格式后至少給出一個(gè)“疑似格式錯(cuò)誤”的警告而不是直接寫庫(kù)。5.4 速查日常巡檢用特殊值清單如果不想每次都從頭排查可以在系統(tǒng)里維護(hù)一份“特殊值清單”字段至少包括保留值、允許環(huán)境、所屬系統(tǒng)、處理策略。我常用的清單長(zhǎng)這樣保留值常見含義建議處理策略例外場(chǎng)景000000000000空值占位寫入前攔截?zé)o111111111111測(cè)試樁/占位告警并隔離配置中心Mock999999999999最大值占位告警并隔離報(bào)表默認(rèn)“所有”aaaaaa演示數(shù)據(jù)內(nèi)部測(cè)試可用測(cè)試環(huán)境有了這個(gè)清單掃描腳本就能自動(dòng)化運(yùn)行每次發(fā)現(xiàn)異常值直接發(fā)通知。不用每次都靠人肉看數(shù)據(jù)效率會(huì)高很多。6. 這波操作之后我保留了三個(gè)長(zhǎng)期習(xí)慣6.1 給項(xiàng)目里的占位初始值建立白名單登記表從那次全一字符串事件之后我要求所在項(xiàng)目組把代碼里定義的所有“魔法值”統(tǒng)一登記到一份文檔里包括全1、全0、999999、以及一些隨機(jī)生成的測(cè)試串。登記表的字段不算多只要寫下常量名、常量值、使用位置、設(shè)計(jì)原因和負(fù)責(zé)人就夠了。這樣后面任何一個(gè)新同事接手看到“111111111111”也不會(huì)到處亂猜直接查登記表就能知道它是不是合法值。這個(gè)習(xí)慣帶來的另一個(gè)好處是在做代碼評(píng)審時(shí)評(píng)審人一旦看到代碼里出現(xiàn)這類字符串常量會(huì)主動(dòng)提醒“這個(gè)值有沒有在白名單里登記”而不是等到數(shù)據(jù)污染了之后再排查。成本和收益相比這點(diǎn)登記工作是非常劃算的。6.2 每季度全庫(kù)掃一次保留值數(shù)據(jù)量不算特別大的項(xiàng)目我建議每季度跑一次保留值巡檢。不用把所有表都掃一遍可以先把字段名包含ID、NO、CODE、STATUS這類后綴的表挑出來再用程序批量生成查詢。掃描結(jié)果只要出現(xiàn)不在白名單里的保留值就生成一條工單。這樣可以保證即使有新的全一字符串悄悄混進(jìn)來最多一個(gè)季度內(nèi)就能被發(fā)現(xiàn)而不是等到大規(guī)模對(duì)賬失敗時(shí)才暴露。6.3 提交代碼前做一次“原始數(shù)據(jù)巡檢”最后一個(gè)習(xí)慣是個(gè)人層面的跟技術(shù)關(guān)系不大但很管用每次涉及讀寫外部接口的代碼提交前我會(huì)自己先拼一個(gè)簡(jiǎn)單的本地測(cè)試把返回字段里可能出現(xiàn)的特殊值和保留值都列出來逐個(gè)斷言它們按預(yù)期被處理。這個(gè)方法不需要等到測(cè)試環(huán)境直接在單測(cè)里就可以做。我現(xiàn)在每次看到一長(zhǎng)串同樣的數(shù)字第一反應(yīng)不是發(fā)笑而是打開表查一下它是不是又漏進(jìn)去了這個(gè)警惕性算是靠踩坑換來的。