在一個項目中,通常都會支付相關的業務,而涉及到支付一定會有轉帳的操做,轉帳這一步想起來算是比較關鍵的部分,這個接口的設計能力,也大體體現出一我的的水平。java
昨天碰到了一個題目:spring
嘗試用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("轉帳金額爲空");
}
}
}
複製代碼
歡迎一塊兒討論,上面只是個人一點思路,集思廣益才能你們一塊兒進步。