云岚到家 第四天 预约下单

目录

预约下单 

订单状态

订单表设计方案

本项目订单表的表结构

预下订单接口设计

​编辑 实现远程接口的编写

如何编写一个项目内部调用的远程接口?

微服务之间远程调用怎么实现的?

熔断降级

使用sentinel实现熔断降级

为啥不直接在在fallback?

订单号生成规则

常见的订单号生成规则

本项目订单号生成规则

​编辑 下单代码优化

 事务失效:

循环依赖问题


预约下单 

用户点击预约下单的时候会预先生成一个订单号

填好相关信息,然后会进入支付页面

订单状态

订单数据中有一个属性关系到订单的整个流程,就是订单状态 ,订单状态有七种

待支付:订单的初始状态。

派单中:用户支付成功后订单的状态由待支付变为派单中。

待服务:服务人员或机构抢单成功订单的状态由派单中变为待服务。

服务中:服务人员开始服务,订单状态变为服务中。

订单完成:服务人员完成服务订单状态变为订单完成。

已取消:订单是待支付状态时用户取消订单,订单状态变为已取消。

已关闭:订单已支付状态下取消订单后订单状态变为已关闭。

订单表设计方案

在设计订单表时通常采用的结构是订单主表与订单明细表一对多关系结构,比如:在电商系统中,一个订单购买的多件不同的商品,设计订单表和订单明细表:

订单表:记录订单号、订单金额、下单人、订单状态、地址 等信息。

订单明细表:记录该订单购买商品的信息,包括:商品名称、商品价格、交易价格、购买商品数量等。

如果系统需求是一个订单只包括一种商品,此时无须记录订单明细,将购买商品的详细信息记录在订单表即可,设计字段包括:订单号、订单金额、下单人、订单状态、商品名称、购买商品数量等。

本项目订单表的表结构
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,可以确保它们在运行时能够正确地识别和区分彼此,避免出现冲突和混淆。

//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到私服 

 然后这个接口需要别人去实现它

如何编写一个项目内部调用的远程接口?

  1. 项目内部的远程接口统一放在jzo2o-api工程

  2. 首先进入jzo2o-api编写接口,注意使用@FeignClient注解

  3. 进入服务提供者微服务,编写接口实现类

微服务之间远程调用怎么实现的?

项目使用的Spring Cloud Alibaba框架,微服务之间远程调用使用OpenFeign,具体实现步骤如下:

在api工程定义Feign接口,使用@FeignClient注解进行定义。

服务提供方法定义Feign接口的实现类,实现具体的逻辑。

服务调用方(客户端)依赖api工程,使用@EnableFeignClients注解扫描Feign接口,生成代理对象并放在Spring容器中。使用spring.factories进行自动装配。其他的工程只要依赖了这个api 就会自动装配,然后扫描带有注解的这个类,也就是clientscanconfiguration,这个类上由@EnableFeignClients注解,然后扫描到对应的接口

 

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.jzo2o.config.ClientScanConfiguration

熔断降级

在微服务架构一定要去预防微服务雪崩问题,微服务雪崩问题是指在微服务架构中,当一个服务出现故障时,由于服务之间的依赖关系,故障可能会传播到其他服务,导致大规模的服务失败,系统无法正常运行。这种情况就像雪崩一样,最初一个小问题最终引发了整个系统的崩溃。简单理解微服务雪崩就是微服务之间相互调用,因为调用链中的一个服务故障,引起整个链路都无法访问的情况。

常用的预防微服务雪崩的的方法:

超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待。

熔断降级:当服务的异常数或异常比例超过了预设的阈值时,熔断器会进入开启状态,暂时中断对该服务的请求,此时走降级方法,能够快速响应,确保系统的基本功能能够继续运行。

限流:限制对服务的请求速率,避免短时间内大量的请求导致系统崩溃。

线程池隔离:给要请求的资源分配一个线程池,线程池去控制请求数量

信号量隔离:使用计数器模式,记录请求资源的并发线程数量,达到信号量上限时,禁止新的请求。

信号量隔离适合同步请求,控制并发数,比如:对文件的下载并发数进行控制。

大多数场景都适合使用线程池隔离,对于需要同步操作控制并发数的场景可以使用信号量隔离。

使用sentinel实现熔断降级

之前使用的是fallback,hystrix降级

远程调用(feign)服务降级处理,远程调用错误如何兜底_fegin服务降级类-CSDN博客文章浏览阅读253次。在wemedia的nacos配置中心里添加如下内容,开启服务降级,也可以指定服务响应的超时的时间。客户端开启降级heima-leadnews-wemedia。在自媒体微服务中添加类,扫描降级代码类的包。远程接口中指向降级代码。_fegin服务降级类https://blog.csdn.net/2201_75600005/article/details/140443632?fromshare=blogdetail&sharetype=blogdetail&sharerId=140443632&sharerefer=PC&sharesource=2201_75600005&sharefrom=from_link

为啥不直接在在fallback?

因为很多服务都使用远程接口,如果使用fallback那么所有的降级逻辑就都一样了,不能单独写自己的降级逻辑

本项目使用Sentinel实现限流、熔断等机制预防微服务雪崩。

熔断降级是微服务保护的一种方法,当使用Feign进行远程调用,在客户端通过熔断降级措施进行微服务保护。

如下图:

orders-manager订单服务请求customer查询地址簿,在进行feign远程调用过程出现异常将走降级方法,当异常比例或异常数达到一定的阈值将触发熔断,熔断期间将直接走降级逻辑快速响应。

当customer服务恢复后,熔断时间结束此时会再次尝试请求customer,如果成功请求将关闭熔断,恢复原来的链路。

 安装sentinal

Docsicon-default.png?t=O83Ahttps://mx67xggunk5.feishu.cn/wiki/QxFgwuGm4ibjemkyAxbc4dbRnFd 

引入sentinal依赖 

新建一个类,在这个类中中转一下方法的调用

使用sentinalResource注解  value只是名字,可任意,这里相当于中转了一下。出现异常先走异常,达到熔断后走熔断的逻辑

@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客户端能看到,然后点击熔断设置熔断规则。

订单号生成规则

常见的订单号生成规则
  1. 自增数字序列

使用数据库的自增主键或者其他递增的数字序列(比如redis的INCR命令)作为订单号的一部分。例如,订单号可以是"202310280001",其中"20231028"表示日期,"0001"是自增的订单序号。

  1. 时间戳+随机数

将年月日时分秒和一定范围内的随机数组合起来。例如,订单号可以是"20181028124523" + "1234",其中"20181028124523"表示日期和时间,"1234"是随机生成的数字。

使用时间戳+随机数作为主键有重复的风险。

  1. 订单类型+日期+序号

将订单类型(例如"01"表示普通订单,"02"表示VIP订单等)、日期和序号组合起来。例如,订单号可以是"0101028100001",其中"01"表示订单类型,"20181028"表示日期,"00001"是序号。

加上订单类型的好处是方便客户服务,根据订单号就可以知道订单的类型。

  1. 分布式唯一ID生成器

使用分布式唯一ID生成器(例如Snowflake雪花算法)生成全局唯一的ID作为订单号。这种方法保证了在分布式系统中生成的订单号的唯一性和有序性。

Snowflake 算法根据机器ID、时间戳、序号等因素生成,保证全局唯一性,它的优势在于生成的 ID 具有趋势递增、唯一性、高效性等特点.

Snowflake 算法对系统时钟的依赖性较强,如果系统时钟发生回拨,可能会导致 ID 生成出现问题。因此,在使用 Snowflake 算法时,需要定时进行时钟同步,确保系统时钟的稳定性。

本项目订单号生成规则

19位:2位年+2位月+2位日+13位序号

例如:2311011000000000001

实现方案:

1、前6位通过当前时间获取。

2、后13位通过Redis的INCR 命令实现。

@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就构成了循环依赖,如下图:

  1. 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的代理对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值