在一個專案中,一般都會支付相關的業務,而涉及到支付必定會有轉賬的操作,轉賬這一步想起來算是比較關鍵的部分,這個介面的設計能力,也大致體現出一個人的水平。
昨天碰到了一個題目:
嘗試用java編寫一個轉賬介面,傳入主要業務引數包括轉出賬號,轉入賬號,轉賬金額,完成轉出和轉入賬號的資金處理,該服務要確保在資金處理時轉出賬戶的餘額不會透支,金額計算準確。
設計
-
首先一般在系統中的引數不會有這麼少,一般情況下請求引數還會有一些公共的資訊,比如請求來源(請求ip與系統)、請求流水號,請求時間,等資訊。閘道器上一般會攔截一些不合法的請求
-
如果有返回結果,一般包含處理結果,響應時間,處理後的狀態,原始的請求資訊一般也會返回去
-
看要求是否有強一致性的需求,如果沒有強一致性的需求,是否要及時返回結果。根據需求做出來,如果不用實時返回結果,可以在後端不斷的重試,知道有最終結果,有強一致性的要求,則需要做一些特殊處理,如果沒有保證最終一致性即可。
-
冪等性設計,一個唯一的請求流水號只能對應一筆支付,防止重複扣款
-
可能會涉及到一些其它的遠端服務,做一些操作,這裡就需要根據與其它系統協商來處理,當然這個介面的入參也要與會呼叫這個系統的人說下
-
題目說的,要對餘額做判斷,內部要判斷使用者的資金是否足夠。可以從資料庫層面上,讓使用者的餘額不能小於0。金額計算準確,一般用BigDecimal。
-
寫程式碼的時候注意一些規範事項
-
內部注意一些限制條件,賬戶是否合法
程式碼
我的程式碼裡面沒有做冪等性的處理。可能程式碼還有一些其它的問題,如果有沒考慮到的點,歡迎指出
package me.aihe.demo;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.util.HashMap;
/**
* 嘗試用java編寫一個轉賬介面,傳入主要業務引數包括轉出賬號,
* 轉入賬號,轉賬金額,完成轉出和轉入賬號的資金處理,
* 該服務要確保在資金處理時轉出賬戶的餘額不會透支,金額計算準確。
*/
public class FirstProblem {
/**
* 假設這個東西是一個遠端服務名稱
*/
private String checkAmoutisEnoughRemoteService = "一個可以校驗使用者餘額是否足夠的遠端服務";
/*
* 題目分析:
* 定義介面:
* 入參: 轉出賬號 轉入賬號 轉賬金額
* 要求:
* 完成轉出轉入賬號的資金處理
* 處理時轉出賬戶的餘額不會透支 fromPerson 要判斷餘額是否足夠
* 金額準確 使用BigDeceimal
*
* 疑問:
* 是否需要返回值?還是隻是一次處理即可
*
*
* 關鍵點:
* 關鍵操作記得打日誌
* 如果存在併發情況記得加分散式鎖
* 其餘的根據需求,是否做一些額外控制,比如限流,回滾,重試
* 如果遠端呼叫可能存在等待狀態,可以進行重試,儘可能的同步,
* 如果可以非同步,後臺加定時任務進行非同步資料查詢並更新
*
*/
// 我在這裡直接寫介面,就不定義類的名稱了,如果需要也可以定義一下類的名字
// 因為不是介面,我先把方法留空
/**
* 轉賬介面,嘗試用java編寫一個轉賬介面,傳入主要業務引數包括轉出賬號,
* 轉入賬號,轉賬金額,完成轉出和轉入賬號的資金處理,
* 該服務要確保在資金處理時轉出賬戶的餘額不會透支,金額計算準確。
*
* @author he.ai 2019-04-18 20:16
*
* @param sourceAccount 題目中的轉出賬號,也就是從誰哪裡把錢拿出來
* @param destAccout 題目中的轉入賬號,也就是錢轉給誰
* @param amout 定義為字串,是想將字串轉為BigDecimal,傳入BigDeceimal也是可以的,可以再做商量
*
* 假設這裡需要返回結果的話,一般會用公用的Result類,封裝遠端呼叫的code,結果,已經資料之類的
*/
void transfer(
String sourceAccount,
String destAccout,
String amout
){
// 假設這裡我們可以獲取到轉出賬號(sourceAccout)的餘額
// 來源可以為:遠端呼叫服務,直接從資料庫拿,總之要能獲取到當前賬戶的餘額
// 1. 首先對引數進行校驗,是否合法
// 如果有異常的話,需定義異常在什麼位置進行處理
checkParam(sourceAccount, destAccout, amout);
// 也可以對賬戶是否存在做一次檢驗
// 2. 校驗轉出賬號的餘額是否足夠,這一步看我們是否有許可權,
// 如果我們沒有許可權獲取使用者的餘額資訊,需呼叫有許可權的部門進行判斷
// 我這裡假設的是我們沒有許可權知道使用者的餘額,需要判斷
// 呼叫遠端的引數,這個入參需要根據與其他系統進行協商
HashMap<String, String> map = new HashMap<>();
map.put("account",sourceAccount);
map.put("amount",amout);
Result result = callRemoteService(checkAmoutisEnoughRemoteService, map);
// 對result進行處理
// 進行判斷使用者餘額是否足夠,我就不寫判斷邏輯了
// 3. 進行轉賬操作,程式碼能執行到這裡,代表使用者賬戶是ok的,餘額也是ok的
// 至於什麼風險控制,使用者是否有安全隱患,看需求,以及其它的系統
// 這裡看要求是否要有一個全域性事務進行控制,如果對資料的一致性要求很高,那麼可以做全域性的事務控制
// 如果這裡對資料的一致性要求不高,那麼我們可以先進行出來,再補寫定時任務,或者採用非同步通知的方式
// 甚至可以加鎖
doTransfer(sourceAccount,destAccout,amout);
// 到這一步,假設錢已經轉好了,看要求是否要通知其他的業務系統
sendNotifytoOthers();
}
private void sendNotifytoOthers() {
}
/**
* 假設這裡有個全域性事務,本地事務也行
*/
@Transactional(rollbackFor = Exception.class)
public void doTransfer(String sourceAccount, String destAccout, String amout) {
// 其實既然讓我們做了,我們應該是有許可權獲取餘額的
// 假設我們這裡獲取到了使用者的餘額,當然呼叫其他的系統,真正做也有可能
// 但是我們還是要有一份備份資料
// 使用者的餘額,這一步要根據系統要求,看看從哪裡獲取
BigDecimal sourceLeftMoney = new BigDecimal("1000");
BigDecimal destLeftMoney = new BigDecimal("200");
// 再做一個假設,如果我們有許可權,我就直接更新了,上面的校驗也不用呼叫遠端服務了
// 這裡一般的ORM框架,可以幫我們進行轉換
// update userDatabase set rest_money = (sourceLeftMoney - amout) where rest_money = sourceLeftMoney and accout = sourceAccount
// 記得打日誌。
updatesourceAccount(sourceAccount,amout);
// 這裡的剩餘金額是轉入賬戶的剩餘金額
// update userDatabase set rest_money = (destLeftMoney + amout) where rest_money = destLeftMoney and accout = destAccout
updatedestAccout(destAccout,amout);
}
private void updatedestAccout(String destAccout, String amout) {
}
private void updatesourceAccount(String sourceAccount, String amout) {
}
/**
* 工具方法,可以直接呼叫遠端服務
* @param checkAmoutisEnoughRemoteService
* @param map
*/
private Result callRemoteService(String checkAmoutisEnoughRemoteService, HashMap<String, String> map) {
// 遠端服務的處理邏輯
return null;
}
static class Result{
}
/**
* 其實這裡的多個引數可以封裝為一個物件的,就不用專遞這麼多引數
* @param sourceAccount
* @param destAccout
* @param amout
*/
private void checkParam(String sourceAccount, String destAccout, String amout) {
if (StringUtils.isEmpty(sourceAccount)){
// 這個業務異常,專案內一般都有自己專案的業務異常,這裡為了方便就丟擲了執行時異常
// 至於異常的補貨,根據專案選擇是在當前進行捕獲,或者丟給全域性異常進行捕獲
// 這裡為了方便,我就不捕獲異常了
// 如果是關鍵業務,可以嘗試發出報警
throw new RuntimeException("業務異常" + "轉出賬號為空");
}
// 可以根據不同的引數,丟擲不同的異常
if (StringUtils.isEmpty(destAccout)){
throw new RuntimeException("業務異常:" + "轉入賬號為空");
}
if (StringUtils.isEmpty(amout)){
throw new RuntimeException("轉賬金額為空");
}
}
}
複製程式碼
最後
歡迎一起討論,上面只是我的一點思路,集思廣益才能大家一起進步。