java開發(fā)中常遇到的各種難點以及解決思路方案
作為一個開發(fā)人員 總會遇到各種難題 本文列舉博主 遇見/想到 的例子 ,也希望同學們可以共同進步~
邏輯刪除如何建立唯一索引
場景描述:
比如我們有project項目表
字段project_name 是唯一的,且有邏輯刪除字段is_delete 0表示未刪除 1表示已刪除
很顯然 不能直接將project_name設置為唯一索引,例如A用戶建立的project_name為 java工程,又把這個工程(邏輯)刪除了, 這時B用戶是允許建立 java工程的。
那將is_delete project_name 共同設置為唯一索引是否可行呢? 答案也是否定的,在B用戶刪除時,就會出現問題了。
解決方案:
is_delete 不用0和1表示,可改為數字遞增,或者時間戳(盡量小 例如納秒級別), 這時將is_delete project_name 共同設置為唯一索引 可以解決該問題。
唯一索引失效問題
場景描述:
人員姓名和電話 組成唯一索引 。
出現問題:
有兩個小孩 名字都叫小朋友 且他們都沒有手機號 此時數據重復 唯一索引失效。 我們換個場景,在高并發(fā)的電商活動中,用戶姓名和vip標識碼 組成唯一索引,此時有兩位用戶 都不是vip用戶,vip標識碼都為空,那可能出現的問題就比較嚴峻了
解決方案: 唯一索引的字段設置為非空,因為空是允許重復的
( 不管單獨將某一個字段設置為唯一索引 還是多個字段組合成唯一索引 都一樣的)
加密字段模糊查詢問題
場景描述: 用戶敏感信息,例如手機號 身份證 戶籍所在地 入庫時,我們通常會加密, 這時需要模糊查詢
解決方案:
數據量少時,例如只是一個公司內部系統(tǒng)的人員表,可以全表查詢 并解密,在java代碼中過濾 (如果遇到要分頁,那得好好考慮怎么處理分頁問題了)
與業(yè)務/產品溝通,看搜索的字數是否相對固定的,例如某用戶的戶籍所在地是廣東省廣州市 那么我們可以將廣東省、廣州市拆分加密。
假設廣東省加密后字符串為 pwd_gds 廣州市加密后字符串為pwd_gzs,此時我們前端傳入廣州市,后端加密后再進行模糊查詢 sql語句變成 like %pwd_gzs%
當然 前面兩種方式只是取巧,通常在中型規(guī)模的項目就已經不適用了,既然提到拆分,那我們可以聯(lián)想到分詞,所以我們可以使用es,將各詞都拆分加密 存入es中 (題外話 es也好 其它存儲也罷 一定要設置密碼 )
maven依賴沖突問題(jar包版本沖突問題)
場景描述: classNotFound , 這是在項目中,引入版本不正確最經常遇到的問題了。 我們跟進報錯類,找到頂部import導包處,假設我們紅色涂抹部分報紅,我們可以找到前一級目錄(紅色劃線處) ,按住ctrl 鍵 再鼠標左鍵點擊,找到所在jar包
解決方案: 將jar包升級(或降級)。
但很多時候,該jar包并不是我們直接通過maven依賴引入的,可能是通過其它組件內部引用的,這個時候我們就可以通過mvn dependency:tree 命令,將控制臺打印信息復制到文本編輯器,在文本編輯器搜索 即可知道是哪個父包引入的
sql in條件查詢時 將結果按照傳入順序排序
場景描述: 例如我們調用外部接口獲取id, 再通過id去數據庫查詢,如果獲取一條id 查一次庫,是可以保證結果順序和id傳入順序一致的;那此時我們希望優(yōu)化一下下,等獲取一批id時,再通過in條件查詢的形式 :
select xx,xxx,xxxx from t where id in(5,1,4,2,3)
此時如何保證返回結果順序與id傳入順序一致呢? 如上偽代碼 id=5 時,希望返回記錄在第一條
解決方案:
sql層面處理
orcale : order by decode
mysql : order by field
2. 如果條件允許 不是直接sql開發(fā),那么推薦是在java代碼中去二次處理數據的,循環(huán)idList 根據id對比去重新組裝結果即可。
數據庫主從復制 主從不同步問題
場景描述: 由于網絡延遲、負載、、自增主鍵不一致等等各種原因 導致主從數據不一致
解決方法: 線上真出現了問題,都到了需要集群數據庫級別的項目 博主覺得吧 大部分還是手動修復數據吧 出現問題 誰都擔不起…
言歸正傳:
- 鎖主庫 鎖為只讀狀態(tài)
- 數據導出
- 停止從庫
- 數據導入
- 重新開始同步
但是鎖主庫 停從庫 這時候如果有數據來源 非常難處理,這時候最好的方式就是 業(yè)務對外公布維護了。
數據庫讀寫分離 讀寫不一致
場景描述: 讀寫分離時,讀從庫時 數據和主庫不一致
解決方法: 還是數據同步問題,看業(yè)務是否能容忍錯誤,能就不處理 不能容忍就手動修數據/重新同步。
臨時解決方案為:強制路由(強制讀取主庫) 但博主還是認為,只要不是大面積出現問題,手動修數據都是比較穩(wěn)妥的方案。
雙寫不一致問題 并發(fā)下數據庫和緩存不一致
場景描述 : 在博主的 《從高并發(fā)場景下超賣問題到redis分布式鎖》博客中 有提到過具體案例
解決方法:
1.延遲雙刪
優(yōu)點: 博主個人認為優(yōu)點不明顯
缺點:博主認為在寫多讀少的場景下 沒有一點用
寫多讀少場景下,在寫入時刪除緩存,讀時更新緩存,此時延遲雙刪 不能解決任何問題 反而降低性能
2.使用隊列 串行化
優(yōu)點:避免不一致問題
缺點:效率低
3.分布式鎖 串行化 如redislock 提供了讀寫鎖
優(yōu)缺點與第2點一致
4.使用canal中間件
博主未接觸過 只是知道該中間件可以解決
java服務如何作為websocket客戶端
場景描述: 有的時候 我們對接供應商/甲方接口,可能會遇到對方給的websocket接口,我們避免在前后端傳輸之間出現數據丟失問題 可能想在后端自己搭建websocket客戶端。 注意是客戶端,網上搜java websocket客戶端,千篇一律都是搜出作為服務端的教程。
解決方法: 可以使用netty實現,博主目前在寫自動重連和發(fā)送心跳時 遇到了問題 找了大佬寫的比較好的代碼 并經過測試 是可用的 具體的代碼會單獨發(fā)博客教程
spring事務失效問題
場景描述: 事務失效 出現異常不回滾 ,首先 @Transactional需要加上(rollbackFor = Exception.class),博主之前有單獨文章介紹過為什么阿里規(guī)范要求加上
解決方法: 博主私認為 所有失效問題都是因為對spring代理對象機制理解不深導致的,失效只是自己沒用對,歡迎在博主博客搜索事務 查看相應文章
數據庫死鎖問題
場景描述: 數據庫死鎖 導致系統(tǒng)卡爆
解決方法: 博主曾切身體會過,在老舊項目中,使用的是oracle 存儲過程開發(fā),由于大量的sql代碼,且使用for update悲觀鎖,各處sql實在太多了,且未及時commit,引發(fā)了死鎖,出現死鎖我們需要在 v$session 中找到死鎖進程 并殺死進程,并及時優(yōu)化sql,簡化或拆分邏輯。
在mysql中,使用replace into語句 也會引發(fā)死鎖,建議使用select + insert方式替代,(據說mysql8.0已修復該bug 博主未親測)
跨庫分頁問題
場景描述:
數據源來自不同的庫,甚至不同類型的數據庫(例如一部分來自mysql,部分來自于時序數據庫)
大多數時候,只需要單獨查不同的庫就能滿足業(yè)務,各司其職;但有一個頁面 需要查看這兩個庫的數據 并實現分頁功能。
解決方法:
首先能不跨庫分頁就不跨庫分頁,看業(yè)務是否真的不能妥協(xié),數據源是否真的不能合并。
如果都不能,那只能考慮分頁方案,下面是博主想到的方法:
將兩個庫的數據,同步至同一張大表中,記錄好每次同步的最新那條數據的時間戳,下次同步時,同步這個時間戳以后的數據即可,大表只負責分頁查詢。
這時大表數據量雖然大些,但有分頁在,效率不會過低。
(如果數據量過大 根據實際情況,考慮同步至es 、clickhouse等)
博主看到有人提過 canal可以同步mysql數據到es,還是要提醒:生產環(huán)境中不是我們demo寫著玩,使用這種中間件 必須熟悉原理 否則重要數據丟失或出現問題 得不償失!
分布式事務問題
場景描述:在分布式中 需要事務回滾
解決方法:可以引入seata中間件,seata中間件本身就是個事務調度器,基于mysql的undo日志;
如果不引入seata,也可以手動回滾,但這得嚴格要求代碼及時調用,且不適用高并發(fā)場景,
僅適用于中小型項目, 偽代碼如下:
// service A public GoodsDO delete(Long id){ GoodsDO gs = database.getOne(id); database.deleteById(id); return gs; } public void insert(GoodsDO gs){ database.insert(gs); } // service B @Autowired private ServiceA serviceA; public void handle(Long id){ try{ GoodsDO gs = serviceA.deleteById(id); // do other things serviceB.xx(); } catch(E e){ // 這里可以換成aop方式,也可以通過mq實現異步 serviceA.insert(gs); } }
如何避免多人同時修改問題
場景描述:例如管理系統(tǒng)中,管理人員可以修改員工的基本信息,員工自己也可以修改。員工在修改過程中,如果管理員已經修改并提交,員工隨后提交,這就會將管理人員修改的內容覆蓋。
解決方法:詳情接口 加上樂觀鎖版本號,在點擊編輯按鈕時,調用一次詳情接口,獲取到當前的樂觀鎖版本號,例如員工點編輯時 version = 1,接下來管理員也點擊了編輯,管理員得到的版本號也為1 (此時員工還沒保存),接著管理員點擊保存,前端將版本號傳回后端,保存接口中去判斷前端傳入的版本號和當前數據庫版本號是否一致(這個時候是一致的 都是1),管理員保存成功 修改樂觀鎖版本號。員工點擊保存時,傳入的版本號也為1,但此時數據庫獲取的版本號,已經變成2了,提示前端信息已被他人修改 刷新頁面再進入。
netty中 發(fā)送多條指令 如何與回復內容進行對應
場景描述:netty中,向服務端發(fā)送多條指令,接收到回復時,如何確定哪條內容對應是哪條指令發(fā)送的
解決方法:可以在發(fā)送時,在數據頭部添加一個請求ID字段,或者在尾部添加一個ack應答機制, 但這前提都是需要服務端進行配合。
參考代碼如下:
// 客戶端代碼 public class ClientHandler extends ChannelInboundHandlerAdapter { // 記錄每個請求的請求ID private final Map<Integer, String> requestMap = new ConcurrentHashMap<>(); // 記錄每個請求對應的響應結果 private final Map<String, String> responseMap = new ConcurrentHashMap<>(); // 請求ID生成器 private final AtomicInteger requestIdGenerator = new AtomicInteger(0); public void sendRequest(byte[] data) { int requestId = requestIdGenerator.incrementAndGet(); ByteBuf buf = Unpooled.buffer(data.length + 4); buf.writeInt(requestId); buf.writeBytes(data); channel.writeAndFlush(buf); // 將請求ID和請求數據保存到請求映射表 requestMap.put(requestId, Arrays.toString(data)); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { if (msg instanceof ByteBuf) { ByteBuf buf = (ByteBuf) msg; int requestId = buf.readInt(); byte[] data = new byte[buf.readableBytes()]; buf.getBytes(buf.readerIndex(), data); String request = requestMap.get(requestId); if (request != null) { // 將請求ID和響應數據保存到響應映射表 String response = Arrays.toString(data); responseMap.put(request, response); // 從請求映射表中刪除請求ID requestMap.remove(requestId); } } } }
ack:
public class MyClientHandler extends ChannelInboundHandlerAdapter { // 記錄上一次請求的ACK字段的值 private int lastAck = 1; public void sendRequest(byte[] data) { // 在請求數據末尾添加一個預留的ACK字段 byte[] requestData = Arrays.copyOf(data, data.length + 1); requestData[data.length] = (byte) lastAck; channel.writeAndFlush(requestData); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { if (msg instanceof ByteBuf) { ByteBuf buf = (ByteBuf) msg; byte[] data = new byte[buf.readableBytes()]; buf.readBytes(data); int ack = data[data.length - 1]; // 修改ACK字段的值為1 data[data.length - 1] = 1; lastAck = 1; // 處理服務端的響應 handleResponse(data); } } public void handleResponse(byte[] data) { // 處理服務端的響應 // ... } }
那如果服務端拒絕配合呢? 那我們只能在等接收到響應后,再發(fā)送下一條指令,思路如下
(但是注意 并發(fā)下會出現問題 如果有并發(fā)場景,必須得服務端配合做應答機制):
1.定義一個 指令下標 (我們以要發(fā)送10條指令為例) :
public static AtomicInteger index = new AtomicInteger(0);
2.提供一個修改下標的方法
public static void setOtherIndex() { // 如果下標到了10 則清0 進行下一次的輪詢 if (Objects.equals(cabinIndex.get(), 10)) { cabinIndex.set(0); } else { cabinIndex.getAndAdd(1); } }
3.發(fā)送指令
if(index.get() == 0){ new byte[]{0x01} }else if (index.get() == 1){ new byte[]{0x02} } // .....
4.channelRead 方法中處理數據
// dosomething // 處理完畢后 下標偏移 setOtherIndex();
總結
到此這篇關于java開發(fā)中常遇到的各種難點以及解決思路方案的文章就介紹到這了,更多相關java開發(fā)難點內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
IntelliJ IDEA引入第三方jar包或查看Java源碼的時候報decompiled.class file byt
今天小編就為大家分享一篇關于IntelliJ IDEA引入第三方jar包或查看Java源碼的時候報decompiled.class file bytecode version:52.0(java 8)錯誤的解決辦法,小編覺得內容挺不錯的,現在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2018-10-10