目录
[编辑 实现远程接口的编写](#编辑 实现远程接口的编写)
[编辑 下单代码优化](#编辑 下单代码优化)
预约下单
用户点击预约下单的时候会预先生成一个订单号
填好相关信息,然后会进入支付页面
订单状态
订单数据中有一个属性关系到订单的整个流程,就是订单状态 ,订单状态有七种
待支付:订单的初始状态。
派单中:用户支付成功后订单的状态由待支付变为派单中。
待服务:服务人员或机构抢单成功订单的状态由派单中变为待服务。
服务中:服务人员开始服务,订单状态变为服务中。
订单完成:服务人员完成服务订单状态变为订单完成。
已取消:订单是待支付状态时用户取消订单,订单状态变为已取消。
已关闭:订单已支付状态下取消订单后订单状态变为已关闭。
订单表设计方案
在设计订单表时通常采用的结构是订单主表与订单明细表一对多关系结构,比如:在电商系统中,一个订单购买的多件不同的商品,设计订单表和订单明细表:
订单表:记录订单号、订单金额、下单人、订单状态、地址 等信息。
订单明细表:记录该订单购买商品的信息,包括:商品名称、商品价格、交易价格、购买商品数量等。
如果系统需求是一个订单只包括一种商品,此时无须记录订单明细,将购买商品的详细信息记录在订单表即可,设计字段包括:订单号、订单金额、下单人、订单状态、商品名称、购买商品数量等。
本项目订单表的表结构
sql
create table `jzo2o-orders`.orders
(
id bigint not null comment '订单id'
constraint `PRIMARY`
primary key,
user_id bigint not null comment '订单所属人',
serve_type_id bigint null comment '服务类型id',
serve_type_name varchar(50) null comment '服务类型名称',
serve_item_id bigint not null comment '服务项id',
serve_item_name varchar(50) null comment '服务项名称',
serve_item_img varchar(255) null comment '服务项图片',
unit int null comment '服务单位',
serve_id bigint not null comment '服务id',
orders_status int not null comment '订单状态,0:待支付,100:派单中,200:待服务,300:服务中,400:待评价,500:订单完成,600:已取消,700:已关闭',
pay_status int null comment '支付状态,2:待支付,4:支付成功',
refund_status int null comment '退款状态 1退款中 2退款成功 3退款失败',
price decimal(10, 2) not null comment '单价',
pur_num int default 1 not null comment '购买数量',
total_amount decimal(10, 2) not null comment '订单总金额',
real_pay_amount decimal(10, 2) not null comment '实际支付金额',
discount_amount decimal(10, 2) not null comment '优惠金额',
city_code varchar(20) not null comment '城市编码',
serve_address varchar(255) not null comment '服务详细地址',
contacts_phone varchar(20) not null comment '联系人手机号',
contacts_name varchar(255) not null comment '联系人姓名',
serve_start_time datetime not null comment '服务开始时间',
lon double(10, 5) null comment '经度',
lat double(10, 5) null comment '纬度',
pay_time datetime null comment '支付时间',
evaluation_time datetime null comment '评价时间',
trading_order_no bigint null comment '支付服务交易单号',
transaction_id varchar(50) null comment '第三方支付的交易号',
refund_no bigint null comment '支付服务退款单号',
refund_id varchar(50) null comment '第三方支付的退款单号',
trading_channel varchar(50) null comment '支付渠道',
display int default 1 null comment '用户端是否展示,1:展示,0:隐藏',
sort_by bigint null comment '排序字段,serve_start_time毫秒级时间戳+订单id后六位',
create_time datetime default CURRENT_TIMESTAMP not null,
update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP
)
comment '订单表' charset = utf8mb4;
数据来源分析
|------------------|-------------------|-------------------------------|
| 字段名称 | 中文含义 | 来源 |
| id | 订单id | 自动生成 19位:2位年+2位月+2位日+13位序号 |
| user_id | 订单所属人id | 从token中获取 |
| serve_id | 服务id | 前端请求 |
| serve_type_id | 服务类型id | 根据服务id远程调用运营基础服务查询 |
| serve_type_name | 服务类型名称 | 根据服务id远程调用运营基础服务查询 |
| serve_item_id | 服务项id | 根据服务id远程调用运营基础服务查询 |
| serve_item_name | 服务项名称 | 根据服务id远程调用运营基础服务查询 |
| serve_item_img | 服务项图片 | 根据服务id远程调用运营基础服务查询 |
| unit | 服务单位 | 根据服务id远程调用运营基础服务查询 |
| orders_status | 订单状态 | 设置为 未支付 |
| pay_status | 支付状态 | 设置为 未支付 |
| price | 单价 | 根据服务id远程调用运营基础服务查询 |
| pur_num | 购买数量 | 前端请求 |
| total_amount | 订单总金额 | 订单总金额 价格 * 购买数量 |
| real_pay_amount | 实际支付金额 | 实付金额 订单总金额 - 优惠金额 |
| discount_amount | 优惠金额 | 根据优惠券加订单总金额计算优惠金额,暂时为0 |
| city_code | 城市编码 | 根据服务id远程调用运营基础服务查询 |
| serve_address | 服务详细地址 | 远程调用客户中心服务查询我的地址获得 |
| contacts_phone | 联系人手机号 | 远程调用客户中心服务查询我的地址获得 |
| contacts_name | 联系人姓名 | 远程调用客户中心服务查询我的地址获得 |
| serve_start_time | 服务开始时间 | 前端请求 |
| lon | 经度 | 远程调用客户中心服务查询我的地址获得 |
| lat | 纬度 | 远程调用客户中心服务查询我的地址获得 |
| pay_time | 支付时间 | 对接支付服务获取 |
| evaluation_time | 评价时间 | 用户评价的时间,预留 |
| trading_order_no | 支付服务交易单号 | 对接支付服务,支付服务生成的交易单号,支付完成填充 |
| transaction_id | 第三方支付的交易号 | 微信支付的交易号,支付完成填充 |
| refund_no | 支付服务退款单号 | 对接支付服务,支付服务生成的退款单号,退款完成填充 |
| refund_id | 第三方支付的退款单号 | 微信支付的退款单号,退款完成填充 |
| trading_channel | 支付渠道 | 微信、支付等,支付完成填充 |
| display | 用户端是否展示,1:展示,0:隐藏 | 默认为1 |
| sort_by | 排序字段 | 根据服务开始时间转为 毫秒 时间戳+订单后5位 |
| create_time | 创建时间 | 数据库控制,默认当前时间 |
| update_time | 更新时间 | 数据库控制,更新时默认当前时间 |
订单是一个单独的微服务:
预下订单接口设计
前端传递信息,服务项id,地址id,购买数量等。然后后端使用远程调用获取到相关信息
实现远程接口的编写
使用@FeignClient注解实现Feign远程调用,此注解是openfeign下的注解,OpenFeign 是 Spring Cloud 对 Feign 进行了集成,并提供了对 Spring Cloud 注解的支持。
我们把所有的远程接口提到了一个 api中,在这里面编写,谁需要用直接依赖这个api就可以了
valus/name是微服务的名称,指定调用哪个微服务,value和name选一个就可以,path是请求的路径,通过给每个FeignClient指定唯一的contextId,可以确保它们在运行时能够正确地识别和区分彼此,避免出现冲突和混淆。
java
//contextId 指定FeignClient实例的上下文id,如果不设置默认为类名,value指定微服务的名称,path:指定接口地址
@FeignClient(contextId = "jzo2o-customer", value = "jzo2o-customer", path = "/customer/inner/address-book")
public interface AddressBookApi {
@GetMapping("/{id}")
AddressBookResDTO detail(@PathVariable("id") Long id);
}
定义完成后需要install 打包到本地,企业中需要deploy到私服
然后这个接口需要别人去实现它
如何编写一个项目内部调用的远程接口?
-
项目内部的远程接口统一放在jzo2o-api工程
-
首先进入jzo2o-api编写接口,注意使用@FeignClient注解
-
进入服务提供者微服务,编写接口实现类
微服务之间远程调用怎么实现的?
项目使用的Spring Cloud Alibaba框架,微服务之间远程调用使用OpenFeign,具体实现步骤如下:
在api工程定义Feign接口,使用@FeignClient注解进行定义。
服务提供方法定义Feign接口的实现类,实现具体的逻辑。
服务调用方(客户端)依赖api工程,使用@EnableFeignClients注解扫描Feign接口,生成代理对象并放在Spring容器中。使用spring.factories进行自动装配。其他的工程只要依赖了这个api 就会自动装配,然后扫描带有注解的这个类,也就是clientscanconfiguration,这个类上由@EnableFeignClients注解,然后扫描到对应的接口
java
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.jzo2o.config.ClientScanConfiguration
熔断降级
在微服务架构一定要去预防微服务雪崩问题,微服务雪崩问题是指在微服务架构中,当一个服务出现故障时,由于服务之间的依赖关系,故障可能会传播到其他服务,导致大规模的服务失败,系统无法正常运行。这种情况就像雪崩一样,最初一个小问题最终引发了整个系统的崩溃。简单理解微服务雪崩就是微服务之间相互调用,因为调用链中的一个服务故障,引起整个链路都无法访问的情况。
常用的预防微服务雪崩的的方法:
超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待。
熔断降级:当服务的异常数或异常比例超过了预设的阈值时,熔断器会进入开启状态,暂时中断对该服务的请求,此时走降级方法,能够快速响应,确保系统的基本功能能够继续运行。
限流:限制对服务的请求速率,避免短时间内大量的请求导致系统崩溃。
线程池 隔离:给要请求的资源分配一个线程池,线程池去控制请求数量
信号量 隔离:使用计数器模式,记录请求资源的并发线程数量,达到信号量上限时,禁止新的请求。
信号量隔离适合同步请求,控制并发数,比如:对文件的下载并发数进行控制。
大多数场景都适合使用线程池隔离,对于需要同步操作控制并发数的场景可以使用信号量隔离。
使用sentinel实现熔断降级
之前使用的是fallback,hystrix降级
为啥不直接在在fallback?
因为很多服务都使用远程接口,如果使用fallback那么所有的降级逻辑就都一样了,不能单独写自己的降级逻辑
本项目使用Sentinel实现限流、熔断等机制预防微服务雪崩。
熔断降级是微服务保护的一种方法,当使用Feign进行远程调用,在客户端通过熔断降级措施进行微服务保护。
如下图:
orders-manager订单服务请求customer查询地址簿,在进行feign远程调用过程出现异常将走降级方法,当异常比例或异常数达到一定的阈值将触发熔断,熔断期间将直接走降级逻辑快速响应。
当customer服务恢复后,熔断时间结束此时会再次尝试请求customer,如果成功请求将关闭熔断,恢复原来的链路。
安装sentinal
Docshttps://mx67xggunk5.feishu.cn/wiki/QxFgwuGm4ibjemkyAxbc4dbRnFd
引入sentinal依赖
新建一个类,在这个类中中转一下方法的调用
使用sentinalResource注解 value只是名字,可任意,这里相当于中转了一下。出现异常先走异常,达到熔断后走熔断的逻辑
java
@Component
@Slf4j
public class CustomerClient {
@Resource
private AddressBookApi addressBookApi;
@SentinelResource(value = "getAddressBookDetail", fallback = "detailFallback", blockHandler = "detailBlockHandler")
public AddressBookResDTO getDetail(Long id) {
log.error("根据id查询地址簿,id:{}", id);
// 调用其他微服务方法
AddressBookResDTO detail = addressBookApi.detail(id);
return detail;
}
//执行异常走 不知道返回什么异常
public AddressBookResDTO detailFallback(Long id, Throwable throwable) {
log.error("非限流、熔断等导致的异常执行的降级方法,id:{},throwable:", id, throwable);
return null;
}
//熔断后的降级逻辑,一定会返回这个异常
public AddressBookResDTO detailBlockHandler(Long id, BlockException blockException) {
log.error("触发限流、熔断时执行的降级方法,id:{},blockException:", id, blockException);
return null;
}
}
@SentinelResource注解的属性说明:
value: 用于定义资源的名称,即 Sentinel 会对该资源进行流量控制和熔断降级。
fallback :非限流、熔断等导致的异常执行的降级方法
blockHandler :触发限流、熔断时执行的降级方法
当出现异常时,在sentinal客户端能看到,然后点击熔断设置熔断规则。
订单号生成规则
常见的订单号生成规则
- 自增数字序列
使用数据库的自增主键或者其他递增的数字序列(比如redis的INCR命令)作为订单号的一部分。例如,订单号可以是"202310280001",其中"20231028"表示日期,"0001"是自增的订单序号。
- 时间戳+随机数
将年月日时分秒和一定范围内的随机数组合起来。例如,订单号可以是"20181028124523" + "1234",其中"20181028124523"表示日期和时间,"1234"是随机生成的数字。
使用时间戳+随机数作为主键有重复的风险。
- 订单类型+日期+序号
将订单类型(例如"01"表示普通订单,"02"表示VIP订单等)、日期和序号组合起来。例如,订单号可以是"0101028100001",其中"01"表示订单类型,"20181028"表示日期,"00001"是序号。
加上订单类型的好处是方便客户服务,根据订单号就可以知道订单的类型。
- 分布式 唯一ID生成器
使用分布式唯一ID生成器(例如Snowflake雪花算法)生成全局唯一的ID作为订单号。这种方法保证了在分布式系统中生成的订单号的唯一性和有序性。
Snowflake 算法根据机器ID、时间戳、序号等因素生成,保证全局唯一性,它的优势在于生成的 ID 具有趋势递增、唯一性、高效性等特点.
Snowflake 算法对系统时钟的依赖性较强,如果系统时钟发生回拨,可能会导致 ID 生成出现问题。因此,在使用 Snowflake 算法时,需要定时进行时钟同步,确保系统时钟的稳定性。
本项目订单号生成规则
19位:2位年+2位月+2位日+13位序号
例如:2311011000000000001
实现方案:
1、前6位通过当前时间获取。
2、后13位通过Redis的INCR 命令实现。
java
@Slf4j
@Service
public class OrdersCreateServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements IOrdersCreateService {
@Resource
private RedisTemplate<String, Long> redisTemplate;
/**
* 生成订单id 格式:{yyMMdd}{13位id}
*
* @return
*/
private Long generateOrderId() {
//通过Redis自增序列得到序号
Long id = redisTemplate.opsForValue().increment(ORDERS_SHARD_KEY_ID_GENERATOR, 1);
//生成订单号 2位年+2位月+2位日+13位序号
long orderId = DateUtils.getFormatDate(LocalDateTime.now(), "yyMMdd") * 10000000000000L + id;
return orderId;
}
下单代码优化
不是所有的方法都加上@tranctional
加上他就会开启事务,事务中最好不要有网络连接,因为这会导致连接时长过长导师资源占用。
不要在这个方法上面加@tranctional
把对数据库的操作单独提出来
事务失效:
事
首先要明白Spring进行事务控制是通过代理对象进行的,在调用add方法之前开启事务,方法执行结束提交事务。
如下图所示:
代理对象调用原始对象的placeOrder方法,在placeOrder方法中通过this.add()调用add方法,this就是原始对象本身并不是代理对象。
循环依赖问题
在 Spring 中,如果一个 bean 尝试将自身引用注入到自身中,通常会引发循环依赖。
首先搞清楚什么是循环依赖:
两个Bean,A依赖B,B依赖A就构成了循环依赖,如下图:
- Spring 如何解决循环依赖
以上图为例说明Spring是如何处理循环依赖问题的?
首先按照常规的流程是:
创建A实例--》初始化A--》注入B--》创建B实例--》初始化B--》注入A
在初始化A时需要注入B,要注入B就需要创建B实例再初始化B,而在初始B时需要注入A,此时A还没有创建完成就陷入死循环。
针对循环依赖的问题Spring会上边的过程调整为下边的流程:
创建A实例--》创建B实例--》在B中注入A--》B初始化---》在A中注入B--》A初始化。
Spring是如何做到呢?
Spring会延迟初始化,B需要注入A,此时Spring会先实例化A,把一个半成品A注入给B,延迟A的初始化。
具体的底层原理是Spring通过三级缓存实现:
1)singletonObjects缓存 :这是 Spring 容器用来缓存完全初始化好的 单例 bean 实例的缓存。当一个 bean 初始化完成后,它会被放入singletonObjects缓存中。这个缓存是单例 bean 的最终缓存,也是 BeanFactory 中保存 bean 的主要缓存。
2)earlySingletonObjects缓存 :这个缓存是用来保存被实例化但还未完全初始化的 bean 的引用。当一个 bean 已经被实例化(但还未初始化)时,它会被放入earlySingletonObjects缓存中。
3)singletonFactories缓存:这个缓存保存的是用于创建 bean 实例的 ObjectFactory,用于支持循环依赖的延迟初始化。当一个 bean 被实例化,但尚未完全初始化时,Spring 会在singletonFactories缓存中查找该 bean 的ObjectFactory。这个ObjectFactory会在需要时被调用来完成 bean 的初始化。
Spring 通过这三级缓存的组合,来确保在循环依赖情况下,能够正常初始化 bean。当两个或多个 bean 之间存在循环依赖时,Spring 使用 singletonFactories 缓存来存储 bean 的提供者(ObjectFactory)。当一个 bean 在初始化过程中需要依赖另一个还未初始化的 bean 时,Spring 会调用相应的 ObjectFactory 来获取对应的 bean 实例,这样就实现了循环依赖的延迟初始化。一旦 bean 初始化完成,它就会被移动到singletonObjects缓存中。
举例:
创建A实例--》创建B实例--》在B中注入A--》B初始化---》在A中注入B--》A初始化。
创建A实例(半成品),在earlySingletonObjects放入A半成品。
创建B实例(半成品),在earlySingletonObjects放入B半成品。
在B中注入A,通过singletonFactories拿到A的对象工厂,通过对象工厂拿到A的半成品注入到B中。
B初始化完成,将B从earlySingletonObjects移动到singletonObjects**。**
A初始化完成,将A从earlySingletonObjects移动到singletonObjects**。**
构造参数注入解决循环依赖问题
虽然Spring可以解决上边通过成员变量注入引发的循环依赖问题,但是通过构造参数注入引发的循环依赖问题是会报错。
为什么上图中的循环依赖会报错呢?
因为创建C需要调用构造方法,而构造方法需要依赖D,此时C是无法实例化的,上边分析Spring解决循环依赖是通过延迟初始化,当出现循环依赖问题可以注入一个半成品,而这里连半成品都无法创建成功。
如何解决这种通过构造参数注入导致的循环依赖问题呢?
可以在C或D的任意一方注入另一方的代理对象而不是注入原始对象,如下:
假设在C的构造方法中注入D的代理对象可以写为:
在构造参数前加@Lazy注解,表示注入D的代理对象。