开始准备:拉取快递员前端代码
git clone http://git.sl-express.com/sl/project-wl-kuaidiyuan-uniapp-vue3.git
1.统一下单
支付流程
1.1业务流程
1.2交易幂等性
幂等性就是针对同一个请求,不管该请求被提交了多少次,该请求都将被视为同一个请求,服务端不应该将同一个请求进行多次处理,以确认处理逻辑的正确性,针对交易性系统幂等性的设计尤为重要,否则由于网络或服务器处理超时等问题,就会造成交易混乱,最严重的后果就是乱扣用户的钱,造成投诉満天飞。
-
如果根据订单号查询交易单数据,如果不存在说明新交易单,生成交易单号后直接返回,这里的交易单号也是使用雪花id。
-
如果支付状态是已经【支付成功】或是【免单 - 不需要支付】,直接抛出异常。
-
如果支付状态是【付款中】,此时有两种情况。
-
如果支付渠道相同(此前使用支付宝付款,本次也是使用支付宝付款),这种情况抛出异常。
-
如果支付渠道不同,我们是允许在生成二维码后更换支付渠道,此时需要重新生成交易单号,此时交易单号与id将不同。
-
如果支付状态是【取消订单】或【挂账】,将id设置为原交易号,交易号重新生成,这样做的目的是既保留了原订单的交易号,又可以生成新的交易号(不重新生成的话,没有办法在支付平台进行支付申请),与之前不会有影响。
1.3HandlerFactory
这里是采用了工厂+反射模式来实现的 由于每个平台的支付的参数和返回值都不一样,所以没办法对其进行共用,只要是每个平台都需要去编写一个实现类。
集成关系
1.4分布式锁
由于下单需要保证数据不重复,要求在同一时刻,同一任务只在一个节点上运行,即保证某一方法同一时刻只能被一个线程执行。在单机环境中,应用是在同一进程下的,只需要保证单进程多线程环境中的线程安全性,通过 JAVA 提供的 volatile、ReentrantLock、synchronized 以及 concurrent 并发包下一些线程安全的类等就可以做到。而在多机部署环境中,不同机器不同进程,就需要在多进程下保证线程的安全性了。因此,分布式锁应运而生。
分布式锁需满足四个条件
首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了,即不能误解锁。
- 具有容错性。只要大多数Redis节点正常运行,客户端就能够获取和释放锁。
Redis分布式锁的缺点
Redis分布式锁会有个缺陷,就是在Redis哨兵模式下:
客户端1
对某个 master节点
写入了redisson锁,此时会异步复制给对应的 slave节点。但是这个过程中一旦发生master节点宕机,主备切换,slave节点从变为了 master节点。
这时 客户端2
来尝试加锁的时候,在新的master节点上也能加锁,此时就会导致多个客户端对同一个分布式锁完成了加锁。
这时系统在业务语义上一定会出现问题, 导致各种脏数据的产生 。
缺陷
在哨兵模式或者主从模式下,如果 master实例宕机的时候,可能导致多个客户端同时完成加锁。
1.5锁的区别
Redisson的分布式锁不仅仅提供了常规锁的功能,还包括以下特性:
- 可重入锁:同一线程可以多次获取同一个锁。
- 公平锁:基于Redis的有序集合实现,保证等待最久的线程最先获取锁。
- 联锁:支持同时获取多个锁,防止死锁。
- 红锁:在多个Redis节点上获取锁,确保高可用性。
- 读写锁:支持读锁和写锁,允许多个读操作同时进行。
1.5.1可重入锁
什么是 “可重入”,可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
Redisson的分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口,同时还支持自动过期解锁
/**
* @create by: 陈海彬
* @description: 测试可重入锁
* 按名称返回锁实例。实现非公平锁定,因此不能保证线程的获取顺序。为了提高故障转移期间的可靠性,
* 所有操作都等待传播到所有Redis从属设备。
* @create time: 2024/2/4 21:55
*/
@Test
public void testReentrantLock() {
RLock lock = redissonClient.getLock("anyLock");
try {
// 1.最常见的使用方法。
lock.lock();
// 2.支持过期解锁功能,10秒钟以后自动解锁,无需调用unlock方法手动释放锁
lock.lock(10, TimeUnit.SECONDS);
// 3.尝试加锁,最多等待3秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(3, 10, TimeUnit.SECONDS);
if (res) {
// 获取锁成功
System.out.println("获取锁成功");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
1.5.2公平锁
Redisson分布式可重入公平锁也是实现了java.util.concurrent.locks.Lock接口的一种RLock对象。在提供了自动过期解锁功能的同时,保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程
@Test
public void testFairLock() {
// 获取公平锁————实现了公平的锁定,因此它保证了线程的获取顺序
RLock fairLock = redissonClient.getFairLock("anyLock");
try {
// 最常见的方法
fairLock.lock();
// 支持过期解锁功能,10秒后真的解锁,无需调用unlock方法解锁
fairLock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁10秒自动解锁
boolean res = fairLock.tryLock(100, 10, TimeUnit.SECONDS);
} catch (Exception e) {
e.printStackTrace();
} finally {
fairLock.unlock();
}
}
1.5.3联锁
可以将多个RLock对象关联为一个联锁,每个RLock对象实例可以来自于不同的Redisson实例
@Test
public void test() {
// 1.获取不同的锁
RLock lock1 = redissonClient.getLock("lock1");
RLock lock2 = redissonClient.getLock("lock2");
RLock lock3 = redissonClient.getLock("lock3");
RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
try {
// 同时加锁:lock1、lock2、lock3,所有的锁都上锁成功才算成功
lock.lock();
// 尝试加锁,最多等待100秒,上锁10秒后自动解锁
lock.tryLock(100, 10, TimeUnit.SECONDS);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
1.5.4红锁
与联锁类似,但是红锁可以从不同的redissonClient获取到锁,并且红锁在大部分节点上锁成功就算成功。
@Test
public void testRedLock() {
// 1.获取不同的锁
RLock lock1 = redissonClient.getLock("lock1");
RLock lock2 = redissonClient.getLock("lock2");
RLock lock3 = redissonClient.getLock("lock3");
RedissonMultiLock lock = new RedissonRedLock(lock1, lock2, lock3);
try {
// 同时加锁:lock1 lock2 lock3, 红锁在大部分节点上加锁成功就算成功。
lock.lock();
// 尝试加锁,最多等待100秒,上锁10秒后自动解锁
lock.tryLock(100, 10, TimeUnit.SECONDS);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
1.5.5读写锁
该对象允许同时有多个读取锁,但是最多只能有一个写入锁
@Test
public void testReadWriteLock() {
RReadWriteLock lock = redissonClient.getReadWriteLock("anyLock");
// 常用的方法
lock.readLock().lock();
lock.writeLock().lock();
// 支持过期解锁功能
lock.readLock().lock(10, TimeUnit.SECONDS);
lock.writeLock().lock(10, TimeUnit.SECONDS);
}
2.查询订单
用户创建交易后,到底有没有支付成功,还是取消支付,这个可以通过查询交易单接口查询的,支付宝和微信也都提供了这样的接口服务。
对于其提供的两种方式,个人觉得提供微信的方式更为妥当,因为其增加了支付超时处理。
2.1流程图
2.2 支付宝交易查询
2.3微信交易查询
3.申请退款
当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,
将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。
支持一个交易单分多次退款,退款金额总额不能大于总支付的总金额,并且最多20次退款
3.1流程图
3.2微信申请退款
由于支付宝相对逻辑较简单,此处只画微信的逻辑图
微信v3 版本最大区别是参数为JSON
4.查询退款
4.1流程图
4.2微信退款查询
5.下载交易账单
发起相应请求然后将结果通过EasyExcel生成即可。
6.同步支付状态
- 在用户支付成功后,【步骤4】支付平台会通知【支付微服务】,这个就是异步通知,需要在【支付微服务】中对外暴露接口
- 由于网络的不确定性,异步通知可能出现故障【步骤6】
- 支付微服务中需要有定时任务,查询正在支付中的订单的状态
- 可以看出【异步通知】与【主动定时查询】这两种方式是互不的,缺一不可。
6.1异步通知
首先设置好回调地址
6.1.1更新交易单
6.1.2微信异步通知
6.2定时任务
定时任务采用xxl-job分布式任务来使用
xxl-job支持的路由策略非常丰富:
- FIRST(第一个):固定选择第一个机器;
- LAST(最后一个):固定选择最后一个机器;
- ROUND(轮询):在线的机器按照顺序一次执行一个
- RANDOM(随机):随机选择在线的机器;
- CONSISTENT_HASH(一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。
- LEAST_FREQUENTLY_USED(最不经常使用):使用频率最低的机器优先被选举;
- LEAST_RECENTLY_USED(最近最久未使用):最久未使用的机器优先被选举;
- FAILOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度;
- BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度;
- SHARDING_BROADCAST(分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务;
6.2.1分片广播方式查询支付状态
每次最多查询{tradingCount}个未完成的交易单,交易单id与shardTotal取模,值等于shardIndex进行处理
:最久未使用的机器优先被选举;
- FAILOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度;
- BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度;
- SHARDING_BROADCAST(分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务;
6.2.1分片广播方式查询支付状态
每次最多查询{tradingCount}个未完成的交易单,交易单id与shardTotal取模,值等于shardIndex进行处理