java支付宝开发流程

不管是支付宝支付,还是微信支付,还是银联支付等,大部分的支付流程都是相似的,学会了其中的思想,那么其他支付方式也就很简单了。

支付宝支付流程:
1、A网站以POST请求方式提交参数给支付宝接口,在支付宝端进行支付处理。

POST请求方式一定程度下保证了安全性,即在url上看不到参数,但可以在浏览器开发者工具中可以看到参数,为防止篡改,则可以采用一些加密协议,如:https、加签名、加密手段(MD5加盐、base64、DES、sha1)等。

在加密中又可以分为对称加密(base64、des等)与非对称加密(RSA公钥与私钥的互换)。

那么在支付宝中主要使用什么方式进行加密呢? 加签名和RSA非对称加密。

2、在支付宝接口中,把支付的结果通知给A网站(成功/失败),以便更新订单的状态信息。

那么支付宝怎么把支付结果返回给A网站呢?有两种通知/回调方式:1、同步通知(同步回调) 2、异步通知(异步回调)

同步通知:当A网站以post请求方式将参数提交给支付宝接口,支付宝会返回同步通知给A网站,意味着A网站需要提供一个接口给支付宝,而同步通知实际上是:本地浏览器的重定向操作,告知A网站支付成功还是失败,不做订单状态的更改。

异步通知:为了安全性考虑,一般需要进行订单状态的更改时,使用异步通知,即支付宝服务器使用httpclient技术调用A网站的接口进行通知,A网站解析报文,判断到底是支付成功还是支付失败。异步通知包含补偿机制,即:支付宝把结果异步通知给A网站,若A网站未及时响应给支付宝,则支付宝会进行补偿重发,类似与MQ。所以在网络存在延迟的情况下,需要解决支付回调的幂等性问题,解决方式跟MQ很相似——使用全局ID。

简而言之:

回调方式:同步回调、异步回调

回调场景: 告诉商户支付通知结果

同步回调: 整个支付流程完毕,使用同步方式将参数重定向给商户平台,一般场景用于展示结果。

异步回调: 第三方支付接口发一个后台通知给商户平台,一般场景用户修改订单信息。

在支付环境可能产生的问题:

安全性问题、支付回调的幂等性问题(如充值1毛钱,可以购买500元的商品漏洞——html篡改数据),分布式事务问题(解决数据的双方一致性问题,因为A网站和支付宝并不使用同一个数据库),若A网站不能及时收到支付宝的异步通知,则支付宝会重试补偿,则应该在A网站内做幂等性判断即可。

支付宝开发环境:
使用支付宝沙箱:https://openhome.alipay.com/platform/appDaily.htm

初次访问需要进行认证,选择自研开发者:

认证成功后进入沙箱环境,下载JAVA版本的SDK&Demo https://docs.open.alipay.com/270/106291/

下载后导入Demo到Eclipse(plus:貌似Demo并不是一个Maven工程)

导入后修改AlipayConfig.java文件(app_id、RSA2、公钥,测试环境下修改网关等信息)

加密方式
在支付领域,数据安全肯定是首要的任务,加密种类可分为:单向加密、对称加密、非对称加密。

最安全的肯定是RSA:公钥与私钥的互换,效率不如单向加密和对称加密高,但安全性很好,要想破解,必须知道公钥和私钥两把密钥,属于非对称加密。

单向加密:如MD5、SHA等不可逆【不能解密,只能加密】,主要用来验证数据传输的过程中,是否被篡改过。

对称加密:一方通过密钥将信息加密后,把密文传给另一方,另一方通过这个相同的密钥将密文解密,转换成可以理解的明文。

明文 <-> 密钥 <-> 密文 【可以加密,又可以解密】

常用对称加密方案 DES、AES、Base64

非对称加密:在支付领域一般都使用RSA非对称加密。在通信双方,如果使用非对称加密,一般遵从这样的原则:公钥加密,私钥解密。同时,一般一个密钥加密,另一个密钥就可以解密。

因为公钥是公开的,如果用来解密,那么就很容易被人解密消息。因此,私钥也可以认为是个人身份的证明。

如果通信双方需要互发消息,那么应该建立两套非对称加密的机制(即两对公私钥密钥对),发消息的一方使用对方的公钥进行加密,接收消息的一方使用自己的私钥解密。

每个人生成一个“私钥-公钥”对,这个私钥需要每个人自行进行保护!公钥可以随便分享,后面详细说,同时,生成的这个“私钥-公钥”对还有个强大的功能就是,使用私钥加密的信息,只能由该私钥对应的公钥才能解密,使用公钥加密的信息,只能由该公钥对应的私钥才能解密!

初次使用支付宝沙箱环境,默认RSA2(SHA256)密钥是未启用状态,需要手动配置应用公钥!

支付步骤
使用tomcat运行项目,进入web页面,如下显示:

点击付款,使用账号密码登录。注意:不要使用真实环境登录,而是用沙箱账号密码登录

沙箱账号密码可以在这查看:

登录密码和支付密码默认都为111111

支付成功后,跳转到你所填写的同步通知地址,实际为浏览器重定向。

若重定向后显示:

trade_no:20180916xxxxxxxxx

out_trade_no:20180916xxx

total_amount:100000

则表示测试成功

DEBUG看底层执行原理
演示完成,下面我们使用断点方式,debug运行看看底层是如何运行的。

首先我打开google浏览器,打开开发者工具,当我们点击付款时候,访问的是alipay.trade.page.pay.jsp,在项目中找到该jsp

在13行打一个断点,点击页面按钮

return_url代表同步通知本地浏览器重定向的url,而notify_url代表异步通知url,精彩在后头。

为什么他要这么做呢?实际上他在29行-33行把参数封装为json格式,并把result动态生成为一个表单,我拷贝表单在本地生成.html文件运行试试看。

此处有一个scirpt标签,当页面加载时候提交表单,表单的action为alipaydev.com,提交方式为POST请求,且内部封装了两个隐藏域,value为刚才的debug所示的封装后的json,并提交给支付宝服务器,双击1.html,运行结果为:

接下来,登录账户付款。

分别在notify_url.jsp和return_url.jsp打一个断点

发现断点先进入notify_url.jsp(异步通知/异步回调),①接收支付宝传递过来的参数,②验证签名,防止被篡改,如果验证签名失败,则有重试机制,直到A系统返回"success"给支付宝,支付宝才不会重试。实际上同步通知和异步通知代码基本一样,最后返回结果。需要考虑网络延迟的情况下,A系统与支付宝系统双方数据一致性问题

所以需要A系统还需要做一个幂等性问题的判断,在网络延迟的情况下,需要使用全局id处理幂等性问题,全局id可以参考订单ID

项目中如何接入支付宝开发
1、引入依赖,该依赖包含了支付宝所需的sdk

<dependency>
		<groupId>com.github.1991wangliang</groupId>
		<artifactId>alipay-sdk</artifactId>
		<version>1.0.0</version>
</dependency>

2、支付服务需要提供两个接口
1、创建token接口
2、使用token进行支付

数据库支付表结构,非正式,仅供参考

CREATE TABLE payment_info (
id int(11) NOT NULL AUTO_INCREMENT,
userid int(11) DEFAULT NULL,
typeid int(2) DEFAULT NULL,
orderid varchar(50) DEFAULT NULL,
price decimal(10,0) DEFAULT NULL,
source varchar(10) DEFAULT NULL,
state int(2) DEFAULT NULL,
created datetime DEFAULT NULL,
updated datetime DEFAULT NULL,
platformorderid varchar(100) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

token接口如何创建?

详细的支付流程:
生成支付令牌,支付令牌,假设有效期15分钟
1、请求时,向支付表创建一条支付信息
2、生成支付token,存到redis中,key为支付token,value为支付表的id,并设置有效期为15分钟
3、返回支付token给客户端
4、使用支付token,向redis查找对应支付表的id
5、使用支付表的id,获取支付信息
6、封装支付宝form表单提交参数

具体代码实现:

public ResponseBase createToken(@RequestBody PaymentInfo paymentInfo) {
//1.创建支付请求信息
Integer insertResultCount = paymentInfoDao.savePaymentType(paymentInfo);
if(insertResultCount <= 0 ){
return setResultFail(“创建支付订单失败”);
}
//2.生成对应的token
String payToken = TokenUtils.getPayToken();
//3.存放在redis中,key为token,value为支付表的id
baseRedisService.setString(payToken, paymentInfo.getId()+"", ResponseConstants.PAY_TOKEN_MEMBER_TIME);
//4.返回token给客户端
JSONObject jsonObject = new JSONObject();
jsonObject.put(“payToken”, payToken);
return setResultSuccess(jsonObject);
}

如何使用token进行支付

思路:

1.对传递的token,并进行校验

2.从redis中根据token获取支付id,并进行校验

3.使用支付id查询数据库,并进行校验

4.从数据库中找到订单信息,封装为json,组装form表单(result),输出到页面

Service层

public ResponseBase findToken(@RequestParam(“payToken”) String payToken){
// 1.参数验证
if (StringUtils.isEmpty(payToken)) {
return setResultFail(“请传递payToken”);
}
// 2.判断token有效期
// 3.使用token 查找redis 找到对应的支付id
String payID = (String) baseRedisService.getString(payToken);
if (StringUtils.isEmpty(payID)) {
return setResultFail(“支付token不存在或已经过期”);
}
// 4.使用支付id进行下单
Long payIDInteger = Long.parseLong(payID);
// 5.使用支付id查询支付信息
PaymentInfo paymenInfo = paymentInfoDao.getPaymentInfo(payIDInteger);
if (paymenInfo == null) {
return setResultFail(“未找到该支付信息”);
}
// 6.对接支付宝代码,返回提交支付form表单元素给客户端,拷贝alipay.trade.page.pay.jsp中的代码
// 获得初始化的AlipayClient
AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.gatewayUrl, AlipayConfig.app_id,
AlipayConfig.merchant_private_key, “json”, AlipayConfig.charset, AlipayConfig.alipay_public_key,
AlipayConfig.sign_type);

	// 设置请求参数
	AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
	alipayRequest.setReturnUrl(AlipayConfig.return_url);
	alipayRequest.setNotifyUrl(AlipayConfig.notify_url);

	// 商户订单号,商户网站订单系统中唯一订单号,必填
	String out_trade_no = paymenInfo.getOrderId();
	// 付款金额,必填
	String total_amount = paymenInfo.getPrice()+"";
	// 订单名称,必填
	String subject = "itcats.cn充值中心";
	// 商品描述,可空
	//String body = new String(request.getParameter("WIDbody").getBytes("ISO-8859-1"), "UTF-8");

	alipayRequest.setBizContent("{\"out_trade_no\":\"" + out_trade_no + "\"," + "\"total_amount\":\"" + total_amount
			+ "\"," + "\"subject\":\"" + subject + "\"," 

// + ““body”:”" + body + “”,"
+ ““product_code”:“FAST_INSTANT_TRADE_PAY”}”);

	// 若想给BizContent增加其他可选请求参数,以增加自定义超时时间参数timeout_express来举例说明
	// alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no
	// +"\","
	// + "\"total_amount\":\""+ total_amount +"\","
	// + "\"subject\":\""+ subject +"\","
	// + "\"body\":\""+ body +"\","
	// + "\"timeout_express\":\"10m\","
	// + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");
	// 请求参数可查阅【电脑网站支付的API文档-alipay.trade.page.pay-请求参数】章节

	// 请求
	try{
		String result = alipayClient.pageExecute(alipayRequest).getBody();
		// 输出

// out.println(result);
JSONObject data = new JSONObject();
data.put(“payHtml”, result);
return setResultSuccess(data);
}catch(Exception e){
return setResultFail(“支付异常”);
}

}

Controller层

//使用token进行支付
@RequestMapping("/aliPay")
public void aliPay(String payToken,HttpServletResponse response) throws IOException{
response.setContentType(“text/html;charset=utf-8”);
PrintWriter writer = response.getWriter();
//1.参数验证
if(StringUtils.isEmpty(payToken)){
return ;
}
//2.调用支付服务接口,返回支付宝html元素
ResponseBase result = payServiceFeign.findToken(payToken);
if(!result.getCode().equals(ResponseConstants.HTTP_RES_CODE_200)){
writer.println(result.getMsg());
return ;
}
//3.将html元素返回给客户端,等于200,获取html
LinkedHashMap data = (LinkedHashMap) result.getData();
String html = (String) data.get(“payHtml”);
log.info("######输出的html结果为#####:{}",html);
//4.页面渲染html
writer.println(html);
writer.close();
}

支付宝回调
回调分为同步通知和异步通知

【支付宝的回调推荐打印日志】,支付宝这边会正常的将支付消息存到支付宝数据库,而若A系统某接口挂掉,支付宝无法获取A系统返回的"success",则支付宝有重试机制,重试次数跟周期参照支付宝官方文档,重试本质上是为了保证支付宝数据库和A系统数据库的双方数据一致性问题,但重试也带来了一些问题,如接口幂等性问题(接口重复消费),所以在A系统接口中需要做幂等性处理,常见例子:记录成功录入支付宝数据库,录入后支付宝会通知A系统支付结果,若A系统存在网络延迟,则可能重复发送,如支付宝充值100元,A系统送100积分,若A系统未做幂等性处理,则可能出现用户获得500积分、1000积分之类的情况。

同步回调处理思路,可以参考支付宝Demo中的return_url.jsp

1、日志处理

2、验证签名操作

3、从Map中取出参数【参照return_url.jsp,参数都封装在Map中】

4、返回json数据给客户端

具体代码:

Service层

// 同步通知
public ResponseBase synCallBack(@RequestParam Map<String, String> params) {
// 1.日志记录
log.info("###支付宝同步通知开始###params:{}", params);
// 2.验签操作,参考支付宝Demo的return_url.jsp
try {
boolean signVerified = AlipaySignature.rsaCheckV1(params, AlipayConfig.alipay_public_key, AlipayConfig.charset,
AlipayConfig.sign_type); // 调用SDK验证签名
log.info("###支付宝同步通知验证参数###signVerified:{}",signVerified);
// ——请在这里编写您的程序(以下代码仅作参考)——
if (!signVerified) {
return setResultFail(“验签失败”);
}
// 商户订单号
String outTradeNo = params.get(“out_trade_no”);
// 支付宝交易号
String tradeNo = params.get(“trade_no”);
// 付款金额
String totalAmount = params.get(“total_amount”);

		JSONObject data = new JSONObject();
		data.put("outTradeNo", outTradeNo);
		data.put("tradeNo", tradeNo);
		data.put("totalAmount", totalAmount);
		return setResultSuccess(data);
	} catch (AlipayApiException e) {
		log.error("支付宝同步通知出现异常,ERROR:{}",e);
		return setResultFail("同步通知出现异常");
	}finally {
		log.info("###支付宝同步通知结束###params:{}", params);
	}
}

同步通知Controller层,负责把参数放在request并转发到页面

@Controller
@Slf4j
//参照return_url.jsp
@RequestMapping("/alipay/callback")
public class CallbackController {
private static final String SUCCESS_PAY = “success_pay”;
@Autowired
private CallbackServiceFeign callbackServiceFeign;
/**
* 同步回调地址,成功以输出流生成的form表单页面,并把参数放在form表单hidden域,失败输出失败
* 需要修改本地项目拷贝过来的支付宝AlipayConfig.java中同步回调地址
* @param request
* @return
* @throws IOException
*/

@RequestMapping("returnUrl")
public void synCallBack(HttpServletRequest request,HttpServletResponse response) throws IOException{
	response.setContentType("text/html;charset=utf-8");
	PrintWriter writer = response.getWriter();
	Map<String,String> params = new HashMap<String,String>();
	Map<String,String[]> requestParams = request.getParameterMap();
	for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();) {
		String name = (String) iter.next();
		String[] values = (String[]) requestParams.get(name);
		String valueStr = "";
		for (int i = 0; i < values.length; i++) {
			valueStr = (i == values.length - 1) ? valueStr + values[i]
					: valueStr + values[i] + ",";
		}
		//乱码解决,这段代码在出现乱码时使用
		valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
		params.put(name, valueStr);
	}
	log.info("###支付宝同步通知CallbackController###synCallBack开始params:{}",params );
	ResponseBase res = callbackServiceFeign.synCallBack(params);
	//执行失败,返回状态码不是200
	if(!res.getCode().equals(ResponseConstants.HTTP_RES_CODE_200)){
		//报错页面error.ftl
		writer.print("跳转页面失败");;
	}
	//执行成功,返回状态码为200
	LinkedHashMap data = (LinkedHashMap) res.getData();
	//封装参数到form表达,浏览器模拟提交,为隐藏参数,使用POST+隐藏域表单包装
	String htmlFrom = "<form name='punchout_form'"
			+ " method='post' action='http://127.0.0.1/alipay/callback/synSuccessPage' >"
			+ "<input type='hidden' name='outTradeNo' value='" + data.get("out_trade_no") + "'>"
			+ "<input type='hidden' name='tradeNo' value='" + data.get("trade_no") + "'>"
			+ "<input type='hidden' name='totalAmount' value='" + data.get("total_amount") + "'>"
			+ "<input type='submit' value='立即支付' style='display:none'>"
			+ "</form><script>document.forms[0].submit();" + "</script>";
	log.info("###支付宝同步通知CallbackController###synCallBack结束params:{}",params );
	//输出表单页面
	writer.println(htmlFrom);
	writer.close();
	
}

//同步回调解决get请求url地址暴露参数问题,这里使用POST请求隐藏参数
	@RequestMapping(value = "/synSuccessPage", method = RequestMethod.POST)
	public String synSuccessPage(HttpServletRequest request, String outTradeNo, String tradeNo, String totalAmount) {
		request.setAttribute("outTradeNo", outTradeNo);
		request.setAttribute("tradeNo", tradeNo);
		request.setAttribute("totalAmount", totalAmount);
		return SUCCESS_PAY;
	}

}

异步通知:

考虑重试机制导致的幂等性问题,但支付宝的重试一般不会并行执行,所以一般只需要根据全局ID(订单ID)进行幂等性判断即可,如果支付失败(如钱不够了),支付宝也不会把消息回调过来。

以下代码涉及到分布式事务问题,plus订单数据库与支付数据库是不同的数据源

// 异步通知
public String asynCallBack(@RequestParam Map<String, String> params) {
// 1.日志记录
log.info("###支付宝同步通知开始###params:{}", params);
// 2.验签操作,参考支付宝Demo的return_url.jsp
try {
boolean signVerified = AlipaySignature.rsaCheckV1(params, AlipayConfig.alipay_public_key,
AlipayConfig.charset, AlipayConfig.sign_type); // 调用SDK验证签名
log.info("###支付宝同步通知验证参数###signVerified:{}", signVerified);
// ——请在这里编写您的程序(以下代码仅作参考)——
if (!signVerified) {
return ResponseConstants.PAY_FAIL;
}

		// 【重点代码】修改支付数据库,先判断,后设置,可以解决多次设置造成的幂等性问题
		// 根据订单id查询支付表,返回支付对象
		String outTradeNo = params.get("out_trade_no");
		//在支付宝中,解决全局幂等性问题使用订单号进行区分
		//如果担心重试并行执行,可以考虑在这加入zookeeperLock.lock()
		PaymentInfo paymenInfo = paymentInfoDao.getByOrderIdPayInfo(outTradeNo);
		//zookeeperLock.unlock()
		if(paymenInfo == null){
			return ResponseConstants.PAY_FAIL;
		}
		//获取支付状态0 待支付、1支付成功 、2支付失败
		//在支付宝的重试机制中,重试不会并行执行,重试都是有时间间隔的
		Integer state = paymenInfo.getState();
		if(state == 1){
			//已经支付过了,不要继续重试,返回success
			return ResponseConstants.PAY_SUCCESS;
		}
		// 商户订单号
		// 支付宝交易号
		String tradeNo = params.get("trade_no");




		// 付款金额,实际开发中,这个金额应该从数据库里查,防止别人知道接口篡改金额
		String totalAmount = params.get("total_amount");
                  //或从数据库中查询出商品金额和totalAmount是否一致,不一致标记为异常订单




		JSONObject data = new JSONObject();

		// 设置为已经支付状态
		paymenInfo.setState(1);
		// 设置支付宝id
		paymenInfo.setPlatformorderId(tradeNo);
		// 对支付参数进行记录
		paymenInfo.setPayMessage(params.toString());
		//手动begin事务
		Integer resultCount = paymentInfoDao.updatePayInfo(paymenInfo);
		if (resultCount <= 0) {
			// 更新支付表失败,返回fail,让支付宝重试
			return ResponseConstants.PAY_FAIL;
		}
		// 更新支付表成功
		// 调用订单接口通知 更新订单表
		ResponseBase orderResult = orderServiceFeign.updateOrder(1l, tradeNo , outTradeNo);
		if(!orderResult.getCode().equals(ResponseConstants.HTTP_RES_CODE_200)){
		//手动回滚事务 
			return ResponseConstants.PAY_FAIL;
		}
		//手动提交,如订单表更新失败,支付表也应该回滚

		return ResponseConstants.PAY_SUCCESS;
	} catch (AlipayApiException e) {
		log.error("支付宝同步通知出现异常,ERROR:{}", e);
		return ResponseConstants.PAY_FAIL;
	} finally {
		log.info("###支付宝同步通知结束###params:{}", params);
	}
}

涉及分布式事务问题的代码,我把上面的部分代码拷贝下来

                    //手动begin事务
                    //更新支付宝表更新数据库
		Integer resultCount = paymentInfoDao.updatePayInfo(paymenInfo);
		if (resultCount <= 0) {
			// 更新支付表失败,返回fail,让支付宝重试
			return ResponseConstants.PAY_FAIL;
		}
		// 更新支付表成功
		// 调用订单接口通知 更新订单表
		ResponseBase orderResult = orderServiceFeign.updateOrder(1l, tradeNo , outTradeNo);
		if(!orderResult.getCode().equals(ResponseConstants.HTTP_RES_CODE_200)){
		//手动回滚事务 
			return ResponseConstants.PAY_FAIL;
		}
		//手动提交,如订单表更新失败,支付表也应该回滚
		return ResponseConstants.PAY_SUCCESS;

常规的电商支付流程:先更改本地的支付宝数据库,更新成功后,再更新订单数据库更新订单状态,但订单状态并不受本地事务的影响,在执行完更新订单数据库后抛出异常,则订单更新 orderServiceFeign.updateOrder(1l, tradeNo , outTradeNo);不能回滚,而支付更新paymentInfoDao.updatePayInfo(paymenInfo);受本地事务影响可以回滚,这便产生了分布式事务问题。

分布式事务:
就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。

常见的分布式事务解决方案:

两段提交协议(2pc)、三段提交协议(3pc)、TCC补偿机制、MQ(补偿机制)+幂等性处理、提供回滚接口、分布式数据库支付宝流程等。

分布式理论:CAP理论和BASE理论

CAP理论
所谓的CAP理论即为:数据的一致性、服务的可用性、分区容错

一致性

指“all nodes see the same data at the same time”,即更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致。
对于分布式事务一致性,可以分为从客户端和服务端两个不同的视角。

从客户端来看,一致性主要指的是多并发访问时更新过的数据如何获取的问题。

从服务端来看,则是更新如何复制分布到整个系统,以保证数据最终一致。

一致性是因为有并发读写才有的问题,因此在理解一致性的问题时,一定要注意结合考虑并发读写的场景。
从客户端角度,多进程并发访问时,更新过的数据在不同进程如何获取的不同策略,决定了不同的一致性。

对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性。
如果能容忍后续的部分或者全部访问不到,则是弱一致性。
如果经过一段时间后要求能访问到更新后的数据,则是最终一致性。

可用性

可用性指“Reads and writes always succeed”,即服务一直可用,而且是正常响应时间。
对于一个可用性的分布式系统,每一个非故障的节点必须对每一个请求作出响应。也就是,该系统使用的任何算法必须最终终止。这是一个很强的定义:即使是严重的网络错误,每个请求必须终止,如对服务降级等操作。
高的可用性主要是指系统能够很好的为用户服务,不出现用户操作失败或者访问超时等用户体验不好的情况。可用性通常情况下可用性和分布式数据冗余,负载均衡等有着很大的关联。

分区容错

分区容错性指“the system continues to operate despite arbitrary message loss or failure of part of the system”,即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。
分区容错性和扩展性紧密相关。在分布式应用中,可能因为一些分布式的原因导致系统无法正常运转。好的分区容错性要求能够使应用虽然是一个分布式系统,而看上去却好像是在一个可以运转正常的整体。比如
现在的分布式系统中有某一个或者几个机器宕掉了,其他剩下的机器还能够正常运转满足系统需求,或者是机器之间有网络异常,将分布式系统分隔未独立的几个部分,各个部分还能维持分布式系统的运作,这样就具有好的分区容错性。

BASE理论
BASE理论是指,Basically Available(基本可用)、Soft-state( 软状态/柔性事务)、Eventual Consistency(最终一致性)。是基于CAP定理演化而来,是对CAP中一致性和可用性权衡的结果。核心思想:即使无法做到强一致性,但每个业务根据自身的特点,采用适当的方式来使系统达到最终一致性。

1、基本可用:指分布式系统在出现故障的时候,允许损失部分可用性,保证核心可用。但不等价于不可用。比如:搜索引擎0.5秒返回查询结果,但由于故障,2秒响应查询结果;网页访问过大时,部分用户提供降级服务等。

2、软状态:软状态是指允许系统存在中间状态,并且该中间状态不会影响系统整体可用性。即允许系统在不同节点间副本同步的时候存在延时,如接口被使用的时候不能影响整体可用性。

3、最终一致性:

系统中的所有数据副本经过一定时间后,最终能够达到一致的状态,不需要实时保证系统数据的强一致性。最终一致性是弱一致性的一种特殊情况。BASE理论面向的是大型高可用可扩展的分布式系统,通过牺牲强一致性来获得可用性。ACID是传统数据库常用的概念设计,追求强一致性模型。

ACID,指数据库事务正确执行的四个基本要素的缩写。包含:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。

Base理论为柔性事务

柔性事务和刚性事务
柔性事务满足BASE理论(基本可用,最终一致)
刚性事务满足ACID理论

本文主要围绕分布式事务当中的柔性事务的处理方式进行讨论。

柔性事务分为

两阶段型
补偿型
异步确保型
最大努力通知型几种。 由于支付宝整个架构是SOA架构,因此传统单机环境下数据库的ACID事务满足了分布式环境下的业务需要,以上几种事务类似就是针对分布式环境下业务需要设定的。

什么是XA接口
XA–eXtended Architecture 在事务中意为分布式事务
XA由协调者(coordinator,一般为transaction manager)和参与者(participants,一般在各个资源上有各自的resource manager)共同完成。在MySQL中,XA事务有两种。

什么是JTA
作为java平台上事务规范JTA(Java Transaction API)也定义了对XA事务的支持,实际上,JTA是基于XA架构上建模的,在JTA 中,事务管理器抽象为javax.transaction.TransactionManager接口,并通过底层事务服务(即JTS)实现。像很多其他的java规范一样,JTA仅仅定义了接口,具体的实现则是由供应商(如J2EE厂商)负责提供,目前JTA的实现主要由以下几种:
1.J2EE容器所提供的JTA实现(JBoss)
2.独立的JTA实现:如JOTM,Atomikos.这些实现可以应用在那些不使用J2EE应用服务器的环境里用以提供分布事事务保证。如Tomcat,Jetty以及普通的java应用。

2PC两段提交
所谓的两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)。

XA一般由两阶段完成,称为two-phase commit(2PC)。
阶段一为准备阶段,即所有的参与者准备执行事务并锁住需要的资源。参与者ready时,向transaction manager汇报自己已经准备好。
阶段二为提交阶段。当transaction manager确认所有参与者都ready后,向所有参与者发送commit命令。
如下图所示:

XA的性能问题
XA的性能很低。一个数据库的事务和多个数据库间的XA事务性能对比可发现,性能差10倍左右。因此要尽量避免XA事务,例如可以将数据写入本地,用高性能的消息系统分发数据。或使用数据库复制等技术。
只有在这些都无法实现,且性能不是瓶颈时才应该使用XA。

3PC三段提交
三阶段提交(Three-phase commit),也叫三阶段提交协议(Three-phase commit protocol),是二阶段提交(2PC)的改进版本。

与两阶段提交不同的是,三阶段提交有两个改动点。

1、引入超时机制。同时在协调者和参与者中都引入超时机制。
2、在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。

也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

CanCommit阶段

3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

1.事务询问 协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。

2.响应反馈 参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No

PreCommit阶段

协调者根据参与者的反应情况来决定是否可以执行事务的PreCommit操作。根据响应情况,有以下两种可能。

假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。

1.发送预提交请求 协调者向参与者发送PreCommit请求,并进入Prepared阶段。

2.事务预提交 参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。

3.响应反馈 如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。

1.发送中断请求 协调者向所有参与者发送abort请求。

2.中断事务 参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

doCommit阶段

该阶段进行真正的事务提交,也可以分为以下两种情况。

执行提交

1.发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。

2.事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。

3.响应反馈 事务提交完之后,向协调者发送Ack响应。

4.完成事务 协调者接收到所有参与者的ack响应之后,完成事务。

中断事务 协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。

1.发送中断请求 协调者向所有参与者发送abort请求

2.事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。

3.反馈结果 参与者完成事务回滚之后,向协调者发送ACK消息

4.中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交。(其实这个应该是基于概率来决定的,当进入第三阶段时,说明参与者在第二阶段已经收到了PreCommit请求,那么协调者产生PreCommit请求的前提条件是他在第二阶段开始之前,收到所有参与者的CanCommit响应都是Yes。(一旦参与者收到了PreCommit,意味他知道大家其实都同意修改了)所以,一句话概括就是,当进入第三阶段时,由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是他有理由相信:成功提交的几率很大。 )

2PC与3PC的区别
相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。

TCC
TCC(Try-Confirm-Cancel),则是将业务逻辑分成try、confirm/cancel两个阶段执行,具体介绍见TCC事务机制简介。其事务处理方式为:
1、 在全局事务决定提交时,调用与try业务逻辑相对应的confirm业务逻辑;
2、 在全局事务决定回滚时,调用与try业务逻辑相对应的cancel业务逻辑。
可见,TCC在事务处理方式上,是很简单的:要么调用confirm业务逻辑,要么调用cancel逻辑

MQ分布式事物
采用时效性高的 MQ,由对方订阅消息并监听,有消息时自动触发事件
采用定时轮询扫描的方式,去检查消息表的数据。

其他补偿
做过支付宝交易接口的同学都知道,我们一般会在支付宝的回调页面和接口里,解密参数,然后调用系统中更新交易状态相关的服务,将订单更新为付款成功。同时,只有当我们回调页面中输出了 success 字样或者标识业务处理成功相应状态码时,支付宝才会停止回调请求。否则,支付宝会每间隔一段时间后,再向客户方发起回调请求,直到输出成功标识为止。
其实这就是一个很典型的补偿例子,跟一些 MQ 重试补偿机制很类似。

一般成熟的系统中,对于级别较高的服务和接口,整体的可用性通常都会很高。如果有些业务由于瞬时的网络故障或调用超时等问题,那么这种重试机制其实是非常有效的。

当然,考虑个比较极端的场景,假如系统自身有 bug 或者程序逻辑有问题,那么重试 1W 次那也是无济于事的。那岂不是就发生了“明明已经付款,却显示未付款不发货”类似的悲剧?

其实为了交易系统更可靠,我们一般会在类似交易这种高级别的服务代码中,加入详细日志记录的,一旦系统内部引发类似致命异常,会有邮件通知。同时,后台会有定时任务扫描和分析此类日志,检查出这种特殊的情况,会尝试通过程序来补偿并邮件通知相关人员。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值