高可用电商支付架构设计方案
高可用电商支付架构设计
在现代电商业务中,支付过程是其中至关重要的一环,一个高可用、安全稳定的支付架构不仅可以提高整个系统的可靠性和扩展性,降低维护成本,还可以优化用户体验,增加用户黏性。
本文将提出一种高可用的电商支付架构设计方案,并全程记录落地方法。
- 高可用电商支付架构设计
- 一、前言
- 1、电商支付一般流程
- 2、本项目业务背景
- 二、订单服务
- 1、数据库设计
- 2、如何确保订单的幂等性
- 3、未支付订单如何处理
- 三、支付服务
- 1、支付接口的选择
- 1)微信支付
- 2)支付宝
- 2、支付流程
- 1)二维码生成
- 2)数据库设计
- 3、支付结果接收失败怎么处理
- 四、通知服务
- 1、消息队列的选择
- 2、消息队列使用方法
- 3、如何确保消息的可靠性
- 4、如何避免消息的重复消费
- 五、总结
一、前言
1、电商支付一般流程
传统意义上的支付过程是A向B下单商品C,通过渠道D支付成功后,B将商品交给A。
在电商背景下依旧大致沿用这一套流程,只是会进行责任细化,例如上述中的B,会根据业务功能拆分成多个微服务。
每个微服务具有单一的功能,不同微服务之间相互独立运行,呈现一种高内聚、低耦合的状态。
电商支付大致可以划分成如下几个部分。
- 前端:用户通过电脑或手机下订单。
- 订单服务:处理订单并进行库存锁定。
- 支付服务:处理支付请求,和第三方支付平台进行交互。
- 通知服务:支付完成后通知用户和相关系统。
- 外部支付提供商:支付宝等第三方支付平台。
2、本项目业务背景
本项目依托于在线教育平台,平台有在线支付的需求,包括购买课程、购买学习资料、购买教师一对一服务等等,这些购买的对象很明显涉及多个微服务,所以我们需要根据业务功能对电商支付过程进行封装,封装成多个微服务,以便于代码复用。
封装成三个微服务:
- 订单服务:用户点击购买后,请求发送到订单服务,请求中携带参数,包括商品类型、商品id、商品价格、支付渠道等等。
- 支付服务:订单服务需要根据用户选择的支付方式将请求发送到支付服务中,支付服务与第三方的支付平台进行调用。
- 通知服务:在支付完成后,需要将支付结果通知给用户,支付结果的表现形式可以是文字提醒、跳转页面等等。
本项目基于SpringBoot进行开发,默认已经完成了基础服务,例如前端,网关,认证授权,购买对象所属服务(例如课程、资料、教师微服务)。
二、订单服务
用户在前端进行下单,选择商品规则、支付方式等等。

点击支付,请求从前端转发到网关,网关转发到订单服务,经过认证授权后执行业务逻辑。
1、数据库设计
首先,需要对提交的下单请求进行封装处理,保存到数据库。
订单表中的字段包括订单号,订单类型,订单价格,下单用户id,订单支付状态,创建时间,订单描述,第三方微服务唯一id等等。
第三方微服务唯一id是指下单业务涉及到的第三方微服务的唯一id,例如本次下单的是课程服务的商品,那在选课业务中就应当插入一张选课记录表,表中包含字段唯一id,这个id用于避免用户重复下单某件商品。

其中订单号是后台系统中,该订单的唯一标识,在并发分布式系统中,如何生成唯一标识有多种方法。
- 数据库自增长序列或字段生成id,优点:代码简单,成本小,缺点:强依赖DB,数据库迁移时需要考虑DB版本支持问题,只有一个主库可以生成时有单点故障的风险,主从节点切换时可能导致重复发号,难于扩展
- UUID,优点:简单性能高,全球唯一,方便数据迁移合并,缺点:16字节存储成本高,信息不安全,字符串查询效率低,无序,主键插入效率低
- Redis生成Id,Redis生成Id效率比较高,用5台Redis,每个自增步长为5,日期+自增Id作为Id即可,优点:不依赖数据库,Id有序,缺点:Redis单点故障影响可用性,且代码和配置工作量大
- zookeeper生成Id,用zookeeper的znode生成序列号,缺点:在高并发分布式性能差
- 雪花算法snowflake,Twitter开源的分布式Id生成算法,由41位时间戳+10位机器Id+12位毫秒内流水号+1位符号位0组成,优点:稳定灵活递增,缺点:强依赖机器时钟,机器时钟回拨会导致重复发号,分布式不同机器时钟不可能完全同步,导致不是全局递增
这里我们采用雪花算法,对雪花算法的生成实现如下所示
public final class IdWorkerUtils {private static final Random RANDOM = new Random();private static final long WORKER_ID_BITS = 5L;private static final long DATACENTERIDBITS = 5L;private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTERIDBITS);private static final long SEQUENCE_BITS = 12L;private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTERIDBITS;private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);private static final IdWorkerUtils ID_WORKER_UTILS = new IdWorkerUtils();private long workerId;private long datacenterId;private long idepoch;private long sequence = '0';private long lastTimestamp = -1L;/*** @description 构造方法* @param* @return* @author * @date 2024/6/22 21:03*/private IdWorkerUtils() {this(RANDOM.nextInt((int) MAX_WORKER_ID), RANDOM.nextInt((int) MAX_DATACENTER_ID), 1288834974657L);}private IdWorkerUtils(final long workerId, final long datacenterId, final long idepoch) {if (workerId > MAX_WORKER_ID || workerId < 0) {throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", MAX_WORKER_ID));}if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", MAX_DATACENTER_ID));}this.workerId = workerId;this.datacenterId = datacenterId;this.idepoch = idepoch;}/*** @description 返回雪花算法生成器* @param* @return* @author * @date 2024/6/22 21:03*/public static IdWorkerUtils getInstance() {return ID_WORKER_UTILS;}/*** @description 生成id* @param* @return* @author * @date 2024/6/22 21:03*/public synchronized long nextId() {long timestamp = timeGen();if (timestamp < lastTimestamp) {throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));}if (lastTimestamp == timestamp) {sequence = (sequence + 1) & SEQUENCE_MASK;if (sequence == 0) {timestamp = tilNextMillis(lastTimestamp);}} else {sequence = 0L;}lastTimestamp = timestamp;return ((timestamp - idepoch) << TIMESTAMP_LEFT_SHIFT)| (datacenterId << DATACENTER_ID_SHIFT)| (workerId << WORKER_ID_SHIFT) | sequence;}/*** @description 如果机器时钟回调,自旋直到时钟板正* @param* @return* @author * @date 2024/6/22 21:13*/private long tilNextMillis(final long lastTimestamp) {long timestamp = timeGen();while (timestamp <= lastTimestamp) {timestamp = timeGen();}return timestamp;}/*** @description 得到机器时钟* @param* @return* @author * @date 2024/6/22 21:13*/private long timeGen() {return System.currentTimeMillis();}
}
在订单表中,我们对商品详情的处理是序列化的方式,一个订单可以包括多个商品,如果后续要对商品进行处理,例如查找所有购买某类商品的用户信息,那就需要更细化的订单商品表。
创建订单商品表,表中包括字段:商品id,订单id,除此以外,还需要包括商品类型、商品名称、商品价格等等。

订单业务还涉及到支付问题,在高并发分布式场景下,应当在用户下订单后及时的将支付信息保存到数据库中,否则会面临重复支付、遗失支付信息的问题,所以在这一节一同设计支付表。
支付记录表是订单业务与支付业务相关联的表,需要包括字段:订单id,支付id,支付交易id,第三方支付渠道以及支付id,订单总金额,下单用户id,支付状态,支付时间,创建时间。
支付id是这条记录的id,而支付交易id是与第三方支付平台交互的id。

2、如何确保订单的幂等性
在保存订单信息到数据库之前,根据订单表中的用户id,和第三方微服务唯一id进行查询,如果可以查到对应记录,就说明该用户已经下单过,直接返回。
需要注意的是,支付记录表无需进行幂等性判定,因为如果支付失败或者用户退出支付页面,则支付交易号失效,这里的失效是指无法重新使用该交易号与第三方支付平台进行交互,需要重新生成一个交易号,但是不能直接修改这条记录,应该作为一条支付失败的记录进行保存。
3、未支付订单如何处理
一个订单在下单后迟迟没有支付,那我们就需要考虑取消该订单,这个时间一般设置为30min,或者1h,可以采用延时队列实现该功能。
在创建队列,或者发送消息时添加超时时间,如果超过这个时间,就将消息投递到队列绑定的死信交换机中,由监听死信队列的消费者执行处理业务。
//创建消息Message message = MessageBuiider.withBody(msg).setExpiration("1800000").build();//消息idCorrelationData correlationData = new correlationData(id);//发送消息rabbitTemplate,convertAndsend("ttl.direct","ttl",message,correlationData);
三、支付服务
业务背景是pc端,从用户的角度考虑,在用户点击下单后,需要展示一个二维码,用户手机扫码跳转到支付页面进行支付,支付完成后pc端进行相应的提示。
一般市面常用的第三方支付平台是微信支付和支付宝,需要根据用户选择支付类型执行对应支付流程,这个很容易实现,Spring提供了根据String类型的Name依赖注入
@Resource(name="weixin_pay")
private Pay WeixinPay;//Object pay = applicationContext.getBean("weixin_pay");
1、支付接口的选择
注意:下面代码中涉及的appid等信息均需要替换成自己申请的信息,仅做演示使用。
1)微信支付
微信支付提供了若干种支付方式

其中:
JSAPI支付是指商户通过调用微信支付提供的JSAPI接口,在支付场景中调起微信支付模块完成收款。
Native支付是指商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式。
这两个方式比较适合pc端支付场景,两个支付流程大同小异,我们选择JSAPI进行详细解释。
微信支付-JSAPI开发文档
在使用之前需要再微信支付平台注册,获得AppID、商户号,并设置回调域名(需要在公网备案)

步骤1-6,用户在前端点击提交订单,后台发送请求到微信支付,生成预付订单,得到预付订单标识
步骤7-11,后台在微信浏览器内通过JSAPI调起支付API调起微信支付,发起支付请求。
步骤12-18,用户支付成功后,商户可接收到微信支付支付结果通知支付结果通知API。
步骤19-22,后台在没有接收到微信支付结果通知的情况下需要主动调用查询订单API查询支付结果。
从上述步骤分析中可以看到,在支付服务中,需要提供JSAPI下单的接口,在传参中需要传入一个异步通知的notify_url,这个url对应后台的一个接口,如果未收到支付结果通知,需要向微信支付查询支付结果。
所以一共需要提供三个接口,分别是JSAPI下单的接口,支付结果异步通知的接口,查询支付结果的接口
JSAPI下单:
//请求URLHttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");// 请求body参数String reqdata = "{"+ "\"amount\": {"+ "\"total\": 100,"+ "\"currency\": \"CNY\""+ "},"+ "\"mchid\": \"1900006891\","+ "\"description\": \"Image形象店-深圳腾大-QQ公仔\","+ "\"notify_url\": \"https://www.weixin.qq.com/wxpay/pay.php\","+ "\"payer\": {"+ "\"openid\": \"o4GgauE1lgaPsLabrYvqhVg7O8yA\"" + "},"+ "\"out_trade_no\": \"1217752501201407033233388881\","+ "\"goods_tag\": \"WXG\","+ "\"appid\": \"wxdace645e0bc2c424\"" + "}"; StringEntity entity = new StringEntity(reqdata,"utf-8");entity.setContentType("application/json");httpPost.setEntity(entity);httpPost.setHeader("Accept", "application/json");//完成签名并执行请求CloseableHttpResponse response = httpClient.execute(httpPost);try {int statusCode = response.getStatusLine().getStatusCode();if (statusCode == 200) {System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));} else if (statusCode == 204) {System.out.println("success");} else {System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));throw new IOException("request failed");}} finally {response.close();httpClient.close();}
支付结果异步通知的接口:
通过先对post的请求体进行解密,然后反序列化。
微信支付在回调通知中对关键信息进行了AES-256-GCM加密,密钥需要再微信支付控制台进行设置。
/** 商户号 */public static String merchantId = "";/** 商户API私钥路径 */public static String privateKeyPath = "";/** 商户证书序列号 */public static String merchantSerialNumber = "";/** 商户APIV3密钥 */public static String apiV3Key = "";public static void main(String[] args) {// 使用自动更新平台证书的RSA配置// 一个商户号只能初始化一个配置,否则会因为重复的下载任务报错Config config =new RSAAutoCertificateConfig.Builder().merchantId(merchantId).privateKeyFromPath(privateKeyPath).merchantSerialNumber(merchantSerialNumber).apiV3Key(apiV3Key).build();JsapiService service = new JsapiService.Builder().config(config).build();// request.setXxx(val)设置所需参数,具体参数可见Request定义PrepayRequest request = new PrepayRequest();Amount amount = new Amount();amount.setTotal(100);request.setAmount(amount);request.setAppid("wxa9d9651ae******");request.setMchid("190000****");request.setDescription("测试商品标题");request.setNotifyUrl("https://notify_url");request.setOutTradeNo("out_trade_no_001");Payer payer = new Payer();payer.setOpenid("oLTPCuN5a-nBD4rAL_fa********");request.setPayer(payer);PrepayResponse response = service.prepay(request);System.out.println(response.getPrepayId());}
查询支付结果:
//请求URLURIBuilder uriBuilder = new URIBuilder("https://api.mch.weixin.qq.com/v3/pay/transactions/id/4200000745202011093730578574");uriBuilder.setParameter("mchid", mchId);//完成签名并执行请求HttpGet httpGet = new HttpGet(uriBuilder.build());httpGet.addHeader("Accept", "application/json");CloseableHttpResponse response = httpClient.execute(httpGet);try {int statusCode = response.getStatusLine().getStatusCode();if (statusCode == 200) {System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));} else if (statusCode == 204) {System.out.println("success");} else {System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));throw new IOException("request failed");}} finally {response.close();}
2)支付宝
支付宝提供了多种支付方式

电脑网站支付提供了两种支付场景,分别是手机支付宝app扫二维码支付,在pc端登录支付宝支付。根据我们的实际需求,可以选择电脑网站支付。
其实手机网站支付和电脑网站支付的后台接口设计方式类似,我们任选其一即可。

步骤1-6,用户进行下单,如果是在pc端登录支付宝,需要输入用户名密码先登录,如果是在pc端用手机app扫码,那直接输入支付密码,确认支付,后台会得到一个同步返回的参数。
步骤7,支付结果通过notify_url异步发送到后台接口。
步骤8,支付宝提供了查询api,通过这个api可以查询支付结果。
从上述步骤分析我们可以发现,后台一共需要实现三个接口,向支付宝发送请求的接口,支付宝异步发送支付结果的接口,向支付宝查询支付结果的接口。
支付宝-电脑网站支付开发文档
支付宝开发提供了在线的api调试,支持在线以表格填写参数的方式生成请求,以及沙箱app辅助开发。
向支付宝发送请求
public class AlipayTradePagePay {public static void main(String[] args) throws AlipayApiException {// 初始化SDKAlipayClient alipayClient = new DefaultAlipayClient(getAlipayConfig());// 构造请求参数以调用接口AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();AlipayTradePagePayModel model = new AlipayTradePagePayModel();// 设置商户门店编号model.setStoreId("NJ_001");// 设置订单绝对超时时间model.setTimeExpire("2016-12-31 10:05:01");// 设置业务扩展参数ExtendParams extendParams = new ExtendParams();extendParams.setSysServiceProviderId("2088511833207846");extendParams.setHbFqSellerPercent("100");extendParams.setHbFqNum("3");extendParams.setIndustryRefluxInfo("{\"scene_code\":\"metro_tradeorder\",\"channel\":\"xxxx\",\"scene_data\":{\"asset_name\":\"ALIPAY\"}}");extendParams.setSpecifiedSellerName("XXX的跨境小铺");extendParams.setRoyaltyFreeze("true");extendParams.setCardType("S0JP0000");model.setExtendParams(extendParams);// 设置订单标题model.setSubject("Iphone6 16G");// 设置请求来源地址model.setRequestFromUrl("https://");// 设置产品码model.setProductCode("FAST_INSTANT_TRADE_PAY");// 设置PC扫码支付的方式model.setQrPayMode("1");// 设置商户自定义二维码宽度model.setQrcodeWidth(100L);// 设置请求后页面的集成方式model.setIntegrationType("PCWEB");// 设置订单包含的商品列表信息List<GoodsDetail> goodsDetail = new ArrayList<GoodsDetail>();GoodsDetail goodsDetail0 = new GoodsDetail();goodsDetail0.setGoodsName("ipad");goodsDetail0.setAlipayGoodsId("20010001");goodsDetail0.setQuantity(1L);goodsDetail0.setPrice("2000");goodsDetail0.setGoodsId("apple-01");goodsDetail0.setGoodsCategory("34543238");goodsDetail0.setCategoriesTree("124868003|126232002|126252004");goodsDetail0.setShowUrl("http://www.alipay.com/xxx.jpg");goodsDetail.add(goodsDetail0);model.setGoodsDetail(goodsDetail);// 设置商户的原始订单号model.setMerchantOrderNo("20161008001");// 设置商户订单号model.setOutTradeNo("20150320010101001");// 设置订单总金额model.setTotalAmount("88.88");// 设置商户传入业务信息model.setBusinessParams("{\"mc_create_trade_ip\":\"127.0.0.1\"}");// 设置优惠参数 model.setPromoParams("{\"storeIdType\":\"1\"}");request.setBizModel(model);// 第三方代调用模式下请设置app_auth_token// request.putOtherTextParam("app_auth_token", "<-- 请填写应用授权令牌 -->");AlipayTradePagePayResponse response = alipayClient.pageExecute(request, "POST");// 如果需要返回GET请求,请使用// AlipayTradePagePayResponse response = alipayClient.pageExecute(request, "GET");String pageRedirectionData = response.getBody();System.out.println(pageRedirectionData);if (response.isSuccess()) {System.out.println("调用成功");} else {System.out.println("调用失败");// sdk版本是"4.38.0.ALL"及以上,可以参考下面的示例获取诊断链接// String diagnosisUrl = DiagnosisUtils.getDiagnosisUrl(response);// System.out.println(diagnosisUrl);}}private static AlipayConfig getAlipayConfig() {String privateKey = "<-- 请填写您的应用私钥,例如:MIIEvQIBADANB ... ... -->";String alipayPublicKey = "<-- 请填写您的支付宝公钥,例如:MIIBIjANBg... -->";AlipayConfig alipayConfig = new AlipayConfig();alipayConfig.setServerUrl("https://openapi.alipay.com/gateway.do");alipayConfig.setAppId("<-- 请填写您的AppId,例如:2019091767145019 -->");alipayConfig.setPrivateKey(privateKey);alipayConfig.setFormat("json");alipayConfig.setAlipayPublicKey(alipayPublicKey);alipayConfig.setCharset("UTF-8");alipayConfig.setSignType("RSA2");return alipayConfig;}
}
支付宝异步发送支付结果,以Post的方式发送请求,在请求中以Map的方式携带参数,需要解码后进行验签处理。
将签名参数(sign)使用 base64 解码为字节码串。
使用 RSA 的验签方法,通过签名字符串、签名参数(经过 base64 解码)及支付宝公钥验证签名。
验签和解码部分的问题可以查看开发文档
支付宝支付开发常见问题汇总
Map< String , String > params = new HashMap < String , String > ();Map requestParams = request.getParameterMap();for(Iterator 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);}//切记alipaypublickey是支付宝的公钥,请去open.alipay.com对应应用下查看。 //boolean AlipaySignature.rsaCheckV1(Map<String, String> params, String publicKey, String charset, String sign_type) boolean = AlipaySignature.rsaCheckV1(paramsMap, ALIPAY_PUBLIC_KEY, CHARSET, SIGN_TYPE) //调用SDK验证签名
if(signVerified){// TODO 验签成功后,按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验,校验成功后在response中返回success并继续商户自身业务处理,校验失败返回failure
}else{// TODO 验签失败则记录异常日志,并在response中返回failure.
}
向支付宝查询支付结果
public static void main(String[] args) throws AlipayApiException {// 初始化SDKAlipayClient alipayClient = new DefaultAlipayClient(getAlipayConfig());// 构造请求参数以调用接口AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();AlipayTradeQueryModel model = new AlipayTradeQueryModel();// 设置订单支付时传入的商户订单号model.setOutTradeNo("20150320010101001");// 设置查询选项List<String> queryOptions = new ArrayList<String>();queryOptions.add("trade_settle_info");model.setQueryOptions(queryOptions);// 设置支付宝交易号model.setTradeNo("2014112611001004680 073956707");request.setBizModel(model);AlipayTradeQueryResponse response = alipayClient.execute(request);System.out.println(response.getBody());if (response.isSuccess()) {System.out.println("调用成功");} else {System.out.println("调用失败");// sdk版本是"4.38.0.ALL"及以上,可以参考下面的示例获取诊断链接// String diagnosisUrl = DiagnosisUtils.getDiagnosisUrl(response);// System.out.println(diagnosisUrl);}}
2、支付流程
按照业务背景,用户在前端点击下单后,应该在前端展示二维码,用户用手机扫码后进行支付,展示的二维码应当与用户选择的支付渠道有关。
无论是微信支付还是支付宝支付,都有相似的逻辑,就是由后台向第三方支付平台发送请求,由第三方平台拉起客户端,并同步响应给后台。用户输入密码支付后将支付结果异步发送给后台,同时后台也可以向第三方支付平台查询支付结果。
支付流程分析到这里,还有两个问题,一个是用户扫描的二维码如何生成,其中应该包含哪些信息?一个是支付结果如何保存,在上一章的数据库设计中如何交互?
1)二维码生成
用户选择商品与支付渠道后点击下单,前端应当展示一个二维码,并提示用户用第三方app扫码支付,这个二维码应该包含一个接口,扫码之后将请求发送给第三方平台,第三方平台接收到支付请求后拉起客户端执行支付业务。
在支付服务中创建一个接口,在这个接口中执行向第三方平台发送请求的业务逻辑
@ApiOperation("扫码下单接口")@GetMapping("/requestpay")public void requestpay(String payNo, HttpServletResponse httpResponse) throws IOException {//根据订单id——payNo,查询是否有该订单//第三方支付平台}
而二维码中应该包含这个接口的url,生成一张包含对应url信息的二维码,这个实现技术有很多,例如ZXing。
String qrCode = new QRCodeUtil().createQRCode("http://www.xxxx.cn/api/orders/requestpay?payNo=" + payNo, 200, 200);
得到的是一个base64的String的url,返回前端展示,用户扫码后请求发送到后台,后台将支付请求发送到第三方支付平台,在前端拉起第三方客户端进行支付业务逻辑。
2)数据库设计
在接收到支付结果后,需要更新数据库,支付服务的数据库设计已经在上一章中进行了分析。

更新支付记录表,更新字段:支付状态,第三方平台支付交易号,支付结果通知时间
PayRecord payRecord = new PayRecord();
payRecord.setStatus("000002");//支付成功
payRecord.setOutPayNo(payStatus.getTrade_no());//支付交易号
payRecord.setPaySuccessTime(LocalDateTime.now());//通知时间
int update = payRecordMapper.update(payRecord, new LambdaQueryWrapper<PayRecord>().eq(PayRecord::getPayNo, payNo));
更新订单表,更新字段:支付状态,支付完成时间
Orders orders = ordersMapper.selectById(orderId);
orders.setStatus("100002");
orders.setPaySuccessTime(LocalDateTime.now());
int update = ordersMapper.update(orders, new LambdaQueryWrapper<Orders>().eq(Orders::getId, orderId));
之后需要还需要进行消息通知,这个放在下一章:通知服务中讲。
3、支付结果接收失败怎么处理
如果由于网络通信或notify_url设置错误等原因,异步通知支付结果失败,那我们需要手动查询支付结果,第三方支付平台都提供了查询api,问题是我们该以哪种方式查询接口呢?
从用户的角度出发,自然希望支付完成后能及时的得到支付结果的通知,所以在后台向第三方支付平台发起请求后,向消息队列发送一条定时消息,定时结束投递到死信交换机,由监听私信交换机的服务查询数据库,根据支付状态判断是否支付完成,如果是待支付,就根据支付id向第三方渠道查询支付结果,根据查询结果,成功或失败,更新到数据库,如果查询不到结果,或者支付未完成,就将定时消息重新投递到消息队列。
消息队列的设置在下一章讲。
四、通知服务
无论支付成功或失败,都需要对用户进行通知,在支付结果异步通知,或者手动查询支付结果之后,需要进行消息通知。
1、消息队列的选择
常用的消息队列组件一般有三个,RocketMQ、Kafka、RabbitMQ,这三个消息队列各有优缺点,有自己的使用场景,接下来分析三个组件各自使用场景,选择适合我们业务场景的组件。
1、RocketMQ是阿里自研的国产消息队列,它接受来自生产者的消息,将消息分类,每一类是一个 topic,一个topic存放在不同broker下,对应queue和tag,一个broker有一个commitlog,消费者根据需要订阅 topic,获取里面的消息。
2、Kafka由Linkedin公司开发,是一个分布式、支持分区的(partition)、多副本的(replica),基于 zookeeper 协调的分布式消息系统。
3、RabbitMQ是一个开源的消息代理软件,它实现了高级消息队列协议(AMQP),用于在分布式系统之间进行可靠的异步通信。它可以在不同的应用程序、服务和系统之间传递消息,并确保消息的可靠性和顺序性。
RocketMQ和 Kafka 相比,RocketMQ 在架构上做了减法,在功能上做了加法
RabbitMQ 是一个消息代理中间件,支持推送拉取两种模式,Kafka 是一个分布式流处理平台,只支持拉取。

- 对于秒杀活动等吞吐量要求高的,优先选Kafka和RabbitMQ
- 对于要求强对外提供能力,有很多主题介入的,可以考虑千级的RocketMQ,百万级的RabbitMQ
- 金融业务要求稳定安全的,分布式部署Kafka和RocketMQ
我们需要将支付结果通知给用户,这要求了高时效、高可用、支持多主题(提高扩展性,便于其他业务使用),所以我们选择RabbitMQ。
2、消息队列使用方法
首先在docker中启动RabbitMQ容器。
在父工程、或需要使用消息队列的微服务中添加RabbitMQ的依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
在配置文件中添加RabbitMQ的初始化配置
spring:rabbitmq:host: xxxxxxxxxport: xxxxusername: xxxxpassword: xxxxvirtual-host: /publisher-confirm-type: correlated #correlated 异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallbackpublisher-returns: false #开启publish-return功能,同样是基于callback机制,需要定义ReturnCallbacktemplate:mandatory: false #定义消息路由失败时的策略。true,则调用ReturnCallback;false:则直接丢弃消息listener:simple:acknowledge-mode: none #出现异常时返回unack,消息回滚到mq;没有异常,返回ack ,manual:手动控制,none:丢弃消息,不回滚到mqretry:enabled: true #开启消费者失败重试initial-interval: 1000ms #初识的失败等待时长为1秒multiplier: 1 #失败的等待时长倍数,下次等待时长 = multiplier * last-intervalmax-attempts: 3 #最大重试次数stateless: true #true无状态;false有状态。如果业务中包含事务,这里改为false
我们需要在支付服务中发送消息,让课程服务接收消息并进行处理,交换机Fanout广播模式。
在支付服务中创建交换机
/*** @author zkp15* @version 1.0* @description 消息队列配置* @date 2023/10/4 22:25*/
@Configuration
public class PayNotifyConfig {//交换机public static final String PAYNOTIFY_EXCHANGE_FANOUT = "paynotify_exchange_fanout";//支付结果通知消息类型public static final String MESSAGE_TYPE = "payresult_notify";//声明交换机@Bean(PAYNOTIFY_EXCHANGE_FANOUT)public FanoutExchange paynotify_exchange_fanout() {// 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除return new FanoutExchange(PAYNOTIFY_EXCHANGE_FANOUT, true, false);}}
在课程服务中创建交换机和消息队列,并声明绑定关系
/*** @author zkp15* @version 1.0* @description 消息队列配置* @date 2023/10/4 22:25*/
@Configuration
public class PayNotifyConfig {//交换机public static final String PAYNOTIFY_EXCHANGE_FANOUT = "paynotify_exchange_fanout";//通知队列名称public static final String CHOOSECOURSE_PAYNOTIFY_QUEUE = "choosecourse_paynotify_queue";//支付结果通知消息类型public static final String MESSAGE_TYPE = "payresult_notify";//声明交换机@Bean(PAYNOTIFY_EXCHANGE_FANOUT)public FanoutExchange paynotify_exchange_direct() {// 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除return new FanoutExchange(PAYNOTIFY_EXCHANGE_FANOUT, true, false);}//声明队列@Bean(CHOOSECOURSE_PAYNOTIFY_QUEUE)public Queue course_publish_queue() {return QueueBuilder.durable(CHOOSECOURSE_PAYNOTIFY_QUEUE).build();}//声明交换机和队列绑定@Beanpublic Binding binding_course_publish_queue(@Qualifier(CHOOSECOURSE_PAYNOTIFY_QUEUE) Queue queue, @Qualifier(PAYNOTIFY_EXCHANGE_FANOUT) FanoutExchange exchange) {return BindingBuilder.bind(queue).to(exchange);}}
接下来由支付服务发送消息到交换机,课程服务监听消息队列,得到消息,进行相应处理。
支付服务发送消息
//根据支付结果更新数据库
......//向消息队列插入消息
rabbitTemplate.convertAndSend(PayNotifyConfig.PAYNOTIFY_EXCHANGE_FANOUT, "", msg);
课程服务监听消息
@RabbitListener(queues = PayNotifyConfig.CHOOSECOURSE_PAYNOTIFY_QUEUE)public void receive(String message) {//获取消息MqMessage mqMessage = JSON.parseObject(message, MqMessage.class);//消息类型String messageType = mqMessage.getMessageType();//这里只处理支付结果通知if (PayNotifyConfig.MESSAGE_TYPE.equals(messageType)) {//获取选课记录idString choosecourseId = mqMessage.getBusinessKey1();//添加选课boolean b = myCourseTablesService.saveChooseCourseStauts(choosecourseId);if (b) {//向用户发送付费课程支付成功、选课成功的通知......}}}
3、如何确保消息的可靠性
如果消息没有通知到用户,用户可能会担心自己是否支付成功,自己的钱去哪里了。
所以要确保消息的可靠性,消息丢失的原因有很多,例如生产者发送消息到消息队列时消息丢失,消息队列宕机了,导致消息全部丢失,消费者在消费消息时由于网络等原因没有消费成功,但是消息队列没有收到通知,就会把消息销毁等等。
RabbitMQ提供了生产者确认机制、消息持久化、消费者确认机制。
生产者确认机制,生产者发送消息到消息队列后,消息队列会响应一个结果给生产者confirm
try {Channel channel = rabbitTemplate.getConnectionFactory().createConnection().createChannel(false);channel.confirmSelect();channel.basicPublish(PayNotifyConfig.PAYNOTIFY_EXCHANGE_FANOUT, "", null, msg);channel.addConfirmListener(new ConfirmListener() {//消息失败处理@Overridepublic void handleNack(long deliveryTag, boolean multiple) throws IOException {log.info("sendQueue-ack-confirm-fail");try {Thread.sleep(3000l);} catch (InterruptedException e) {throw new RuntimeException(e);}//重发channel.basicPublish(PayNotifyConfig.PAYNOTIFY_EXCHANGE_FANOUT, "", null, msg);}//消息成功处理@Overridepublic void handleAck(long deliveryTag, boolean multiple) throws IOException {log.info("sendQueue-ack-confirm-successs");}});} catch (Exception e) {log.error("发送消息失败——"+e.printStackTrace());}
消息持久化,消息队列将消息存储在硬盘中,但是这同样会增大开销,导致响应缓慢
我们在声明交换机和消息队列时可以选择持久化
//声明交换机@Bean(PAYNOTIFY_EXCHANGE_FANOUT)public FanoutExchange paynotify_exchange_direct() {// 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除return new FanoutExchange(PAYNOTIFY_EXCHANGE_FANOUT, true, false);}//声明队列@Bean(CHOOSECOURSE_PAYNOTIFY_QUEUE)public Queue course_publish_queue() {return QueueBuilder.durable(CHOOSECOURSE_PAYNOTIFY_QUEUE).build();}// 消息持久化Message msg = MessageBuilder.withBody(message.getBytes(Standardcharsets.UTF_8)) //消息内容.setDeliveryMode(MessageDeliveryMode.PERSISTENT) //持久代.build();
消费者确认机制,消费者消费消息后可以选择向MQ发送ack消息,MQ接收到ack后删除消息。
一般情况下MQ会自动发送ack消息,为了确保消息不丢失,我们可以在执行业务逻辑后手动发送ack消息。
try {// 业务逻辑......// deliveryTag消息的index,true表示批量处理所有小于deliveryTag的消息channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);} catch (Exception e) {log.error("消费消息确认失败"+e.printStackTrace());try {//手动确认回滚 拒绝deliveryTag对应的消息,第二个参数是否requeue,true则重新入队列,否则丢弃或者进入死信队列。channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);} catch (IOException ex) {throw new RuntimeException(ex);}}
4、如何避免消息的重复消费
从用户的角度来看,如果通知了两条一模一样的消息,会感觉莫名其妙,可能会怀疑自己充了两次钱,所以有必要研究分析如何避免消息的重复消费。
在Kafka中,同一个partition会维护一个offset,offset表示消息被消费的标记,可以选择手动更新offset来避免消息的重复消费。
而RabbitMQ中,我们采用分布式幂等性解决方案,使用唯一id,或者redis分布式锁。
在业务中,我们采用唯一id,在消息通知时,根据课程id查询是否已经在课程表中,如果存在,就说明用户购买付费课程成功,消息已经通知过了,就不需要重复通知了。
如果用redis分布式锁该如何实现消息不重复消费呢?我们可以在消息通知之前在redis中插入一个分布式锁,锁的内容可以是消息唯一id,这个可以在消息中添加一个id字段来实现,在接收到消息时从redis中查询是否存在这个锁,如果没有,就拒绝消费消息,如果有,就消费消息,并最终删除锁。
五、总结
本文提出了一种高可用的电商支付架构设计方案,依托实际业务场景进行落地开发,架构分为三个模块,订单模块,支付模块和通知模块。
-
在订单模块设计了订单数据库,包括订单表、订单记录表,考虑到了订单的幂等性问题,和订单未能及时处理的问题。
-
在支付模块对常见支付接口进行了选择分析,包括微信支付和支付宝,分析了适合pc端支付的支付流程,并设计了支付记录表,使用二维码生成器生成二维码,成功请求第三方支付平台实现支付,并对支付结果接收失败的问题进行了预备处理。
-
在通知模块使用消息队列RabbitMQ将订单支付结果及时通知给其他微服务,并对消息的可靠性和重复消费问题进行了讨论。
每个模块履行单一职责,模块间耦合度较低,代码复用率高,极大地提升了系统的稳定性,从而实现高可用。
从用户角度出发优化了业务逻辑,提升了用户体验,增强用户黏性,符合电商平台视顾客如上帝的理念。
相关文章:
高可用电商支付架构设计方案
高可用电商支付架构设计 在现代电商业务中,支付过程是其中至关重要的一环,一个高可用、安全稳定的支付架构不仅可以提高整个系统的可靠性和扩展性,降低维护成本,还可以优化用户体验,增加用户黏性。 本文将提出一种高…...
PriorityQueue详解(含动画演示)
目录 PriorityQueue详解1、PriorityQueue简介2、PriorityQueue继承体系3、PriorityQueue数据结构PriorityQueue类属性注释完全二叉树、大顶堆、小顶堆的概念☆PriorityQueue是如何利用数组存储小顶堆的?☆利用数组存储完全二叉树的好处? 4、PriorityQueu…...
python 字符串驻留机制
偶然发现一个python字符串的现象: >>> a 123_abc >>> b 123_abc >>> a is b True >>> c abc#123 >>> d abc#123 >>> c is d False 这是为什么呢,原来它们的id不一样。 >>> id(a)…...
express+vue 在线五子棋(一)
示例 在线体验地址五子棋,记得一定要再拉个人才能对战 本期难点 1、完成了五子棋的布局,判断游戏结束 2、基本的在线对战 3、游戏配套im(这个im的实现,请移步在线im) 下期安排 1、每步的倒计时设置 2、黑白棋分配由玩家自定义 3、新增旁观…...
AI 大模型企业应用实战(06)-初识LangChain
LLM大模型与AI应用的粘合剂。 1 langchain是什么以及发展过程 LangChain是一个开源框架,旨在简化使用大型语言模型构建端到端应用程序的过程,也是ReAct(reasonact)论文的落地实现。 2022年10月25日开源 54K star 种子轮一周1000万美金,A轮2…...
JavaScript的学习之旅之初始JS
目录 一、认识三个常见的js代码 二、js写入的第二种方式 三、js里内外部文件 一、认识三个常见的js代码 <script>//写入js位置的第一个地方// 控制浏览器弹出一个警告框alert("这是一个警告");// 在计算机页面输入一个内容(写入body中ÿ…...
DataStructure.时间和空间复杂度
时间和空间复杂度 【本节目标】1. 如何衡量一个算法的好坏2. 算法效率3. 时间复杂度3.1 时间复杂度的概念3.2 大O的渐进表示法3.3 推导大O阶方法3.4 常见时间复杂度计算举例3.4.1 示例13.4.2 示例23.4.3 示例33.4.4 示例43.4.5 示例53.4.6 示例63.4.7 示例7 4.空间复杂度4.1 示…...
[Spring Boot]Netty-UDP客户端
文章目录 简述Netty-UDP集成pom引入ClientHandler调用 消息发送与接收在线UDP服务系统调用 简述 最近在一些场景中需要使用UDP客户端进行,所以开始集成新的东西。本文集成了一个基于netty的SpringBoot的简单的应用场景。 Netty-UDP集成 pom引入 <!-- netty --…...
基础C语言知识串串香11☞宏定义与预处理、函数和函数库
六、C语言宏定义与预处理、函数和函数库 6.1 编译工具链 源码.c ——> (预处理)——>预处理过的.i文件——>(编译)——>汇编文件.S——>(汇编)——>目标文件.o->(链接)——>elf可执行程序 预处理用预处理器,编译用编译器,…...
Python 3 函数
Python 3 函数 引言 Python 是一种高级编程语言,以其简洁明了的语法和强大的功能而闻名。在 Python 中,函数是一等公民,扮演着至关重要的角色。它们是组织代码、提高代码复用性和模块化编程的关键。本文将深入探讨 Python 3 中的函数,包括其定义、特性、类型以及最佳实践…...
【Linux详解】冯诺依曼架构 | 操作系统设计 | 斯坦福经典项目Pintos
目录 一. 冯诺依曼体系结构 (Von Neumann Architecture) 注意事项 存储器的意义:缓冲 数据流动示例 二. 操作系统 (Operating System) 操作系统的概念 操作系统的定位与目的 操作系统的管理 系统调用和库函数 操作系统的管理: sum 三. 系统调…...
html做一个画热图的软件
完整示例 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><title>热图生成器</title><script src"https://cdn.plot.ly/plotly-latest.min.js"></script><style>body …...
软考初级网络管理员__软件单选题
1.在Excel 中,设单元格F1的值为56.323,若在单元格F2中输入公式"TEXT(F1,"¥0.00”)”,则单元格F2的值为()。 ¥56 ¥56.323 ¥56.32 ¥56.00 2.要使Word 能自动提醒英文单…...
数据库新技术【分布式数据库】
文章目录 第一章 概述1.1 基本概念1.1.1 分布式数据库1.1.2 数据管理的透明性1.1.3 可靠性1.1.4 分布式数据库与集中式数据库的区别 1.2 体系结构1.3 全局目录1.4 关系代数1.4.1 基操1.4.2 关系表达式1.4.3 查询树 第二章 分布式数据库的设计2.1 设计策略2.2 分布设计的目标2.3…...
关于运用人工智能帮助自己实现英语能力的有效提升?
# 实验报告 ## 实验目的 - 描述实验的目标:自己可以知道,自己的ai学习方法是否可以有效帮助自己实现自己的学习提升。 预期结果:在自己利用科技对于自己进行学习的过程中,自己的成长速度应该是一个幂指数的增长 ## 文献回顾 根据…...
IPv6知识点整理
IPv6:是英文“Internet Protocol Version 6”(互联网协议第6版)的缩写,是互联网工程任务组(IETF)设计的用于替代IPv4的下一代IP协议,其地址数量号称可以为全世界的每一粒沙子编上一个地址 。 国…...
数据赋能(127)——体系:数据标准化——概述、关注焦点
概述 数据标准化是指将数据按照一定的规范和标准进行处理的过程。 数据标准化是属于数据整理过程。 数据标准化的目的在于提高数据的质量、促进数据的共享和交互、降低数据管理的成本,并增强数据的安全性。通过数据标准化,可以使得数据具有统一的格式…...
【 ARMv8/ARMv9 硬件加速系列 3.5.1 -- SVE 谓词寄存器有多少位?】
文章目录 SVE 谓词寄存器(predicate registers)简介SVE 谓词寄存器的位数SVE 谓词寄存器对向量寄存器的控制SVE 谓词寄存器位数计算SVE 谓词寄存器小结 SVE 谓词寄存器(predicate registers)简介 ARMv9的Scalable Vector Extension (SVE) 引入了谓词寄存器(Predica…...
Python - 调用函数时检查参数的类型是否合规
前言 阅读本文大概需要3分钟 说明 在python中,即使加入了类型注解,使用注解之外的类型也是不报错的 def test(uid: int):print(uid)test("999")但是我就想要类型不对就直接报错确实可以另辟蹊径,实现报错,似乎有强…...
Python基础面试题解答
Python基础面试题解答 基础语法 1. Python中的变量是如何管理内存的? Python中的变量通过引用计数来管理内存。当一个变量被创建时,会分配一个内存地址,并记录引用次数。当引用次数变为0时,垃圾回收机制会自动释放该内存。 2.…...
后进先出(LIFO)详解
LIFO 是 Last In, First Out 的缩写,中文译为后进先出。这是一种数据结构的工作原则,类似于一摞盘子或一叠书本: 最后放进去的元素最先出来 -想象往筒状容器里放盘子: (1)你放进的最后一个盘子(…...
大话软工笔记—需求分析概述
需求分析,就是要对需求调研收集到的资料信息逐个地进行拆分、研究,从大量的不确定“需求”中确定出哪些需求最终要转换为确定的“功能需求”。 需求分析的作用非常重要,后续设计的依据主要来自于需求分析的成果,包括: 项目的目的…...
云启出海,智联未来|阿里云网络「企业出海」系列客户沙龙上海站圆满落地
借阿里云中企出海大会的东风,以**「云启出海,智联未来|打造安全可靠的出海云网络引擎」为主题的阿里云企业出海客户沙龙云网络&安全专场于5.28日下午在上海顺利举办,现场吸引了来自携程、小红书、米哈游、哔哩哔哩、波克城市、…...
Linux相关概念和易错知识点(42)(TCP的连接管理、可靠性、面临复杂网络的处理)
目录 1.TCP的连接管理机制(1)三次握手①握手过程②对握手过程的理解 (2)四次挥手(3)握手和挥手的触发(4)状态切换①挥手过程中状态的切换②握手过程中状态的切换 2.TCP的可靠性&…...
最新SpringBoot+SpringCloud+Nacos微服务框架分享
文章目录 前言一、服务规划二、架构核心1.cloud的pom2.gateway的异常handler3.gateway的filter4、admin的pom5、admin的登录核心 三、code-helper分享总结 前言 最近有个活蛮赶的,根据Excel列的需求预估的工时直接打骨折,不要问我为什么,主要…...
HBuilderX安装(uni-app和小程序开发)
下载HBuilderX 访问官方网站:https://www.dcloud.io/hbuilderx.html 根据您的操作系统选择合适版本: Windows版(推荐下载标准版) Windows系统安装步骤 运行安装程序: 双击下载的.exe安装文件 如果出现安全提示&…...
【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分
一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计,提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合:各模块职责清晰,便于独立开发…...
Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement
Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement 1. LAB环境2. L2公告策略2.1 部署Death Star2.2 访问服务2.3 部署L2公告策略2.4 服务宣告 3. 可视化 ARP 流量3.1 部署新服务3.2 准备可视化3.3 再次请求 4. 自动IPAM4.1 IPAM Pool4.2 …...
Modbus RTU与Modbus TCP详解指南
目录 1. Modbus协议基础 1.1 什么是Modbus? 1.2 Modbus协议历史 1.3 Modbus协议族 1.4 Modbus通信模型 🎭 主从架构 🔄 请求响应模式 2. Modbus RTU详解 2.1 RTU是什么? 2.2 RTU物理层 🔌 连接方式 ⚡ 通信参数 2.3 RTU数据帧格式 📦 帧结构详解 🔍…...
C# winform教程(二)----checkbox
一、作用 提供一个用户选择或者不选的状态,这是一个可以多选的控件。 二、属性 其实功能大差不差,除了特殊的几个外,与button基本相同,所有说几个独有的 checkbox属性 名称内容含义appearance控件外观可以变成按钮形状checkali…...
