首页 > 其他分享 >千万级订单的生成方案

千万级订单的生成方案

时间:2024-12-20 14:26:52浏览次数:10  
标签:userid long id 订单 private 千万级 生成 order

  • 了解随订单量的提升,数据库系统经历了哪些变化,这些变化带来哪些痛点
  • 分库分表环境下,订单的id生成有哪些办法
  • 雪花算法的原理及实现
  • 支付环境下,对订单系统的架构设计带来哪些影响

1、架构体系深入剖析

1.1 演进与背景

随着数据量的增长,一般db的架构,经历如下演进:

  1. 单库主从
    • 业务请求并发量大到一定量级后,单一主库无法承受,将读写剥离,从库诞生。
    • 挑战:开发层框架支持,多数据源,数据读从延迟问题。
  2. 单库双主多从架构
    • 实战较少,多为灾备而生,双主单写,灾备切换
  3. 分区表

数据库层面做数据分区策略,对开发层透明。
适用场景:

  • 适合订单场景,最后部分有热点数据,其他都是历史订单(不活跃)
  • 分区表的数据更容易维护,可以直接针对分区做删除、优化、检查、修复、备份等操作
  • 支持多硬件设备,不同分区分散到不同设备,如硬盘
  • 优化查询,只使用必要的分区来提高查询效率,涉及sum()和count()聚合查询时,也可以实现分区并发再汇总。

局限性:

  • 数量上限,一个表最多只能有1024个分区(mysql5.6之后支持8192个分区)
  • 分区表达式类型受限,多为整数或日期。
  • 如果表中有主键或唯一索引,那么分区键必须是主键或唯一索引
  • 分区表中无法使用外键约束
  1. 横向分表
    • mysql单表性能超过千万级别会导致性能严重下降,横向,切成多张表。
    • 挑战:分表策略,量级估算,分多少表?查询问题,扩容问题
  2. 多库
    • 超大量级的单库,备份,主从同步臃肿不堪,
    • 即使拆了表,单服务器依然扛不住,io成为瓶颈。扩充物理节点,就必须分库
    • 挑战:多数据源写,开发框架支持。数据分发难度进一步上升。

1.2 痛点

  • 主流架构一般分库分表都会涉及,追求性能的同时,带来各种痛点
  • 分库分表并不是一门创新技术,它只是由于数据体系结构的限制而做的无奈之举
  • 机器配置无法无限上升,成本飙升,迫不得已衍生的方案

1.2.1 连接

  1. jdbc直连

    开发层面维护,最原始的,sql拼接
    简单粗暴,sql代码写死,扩容会变得极其糟糕。

    String year = getYear();
    String sql = "select * from order_"+year+" where xxxx";
    
  2. 中间件:一般来讲,两种手段

DBproxy,对DB层面,针对机器做代理,一般需要 LVS/F5 等手段来实现流量的负载均衡,跨机房可能需要DNS分发,常见组件:

组件公司功能
Atlas360读写分离、静态分表
Meituan Atlas美团读写分离、单库分表,目前已经在原厂逐步下架。
Cobar阿里 (B2B)Proxy 的形式位于前台应用和实际数据库之间,开放MySQL 通信 协议,开源版中只支持 MySQL,不支持读写分离。
MyCAT阿里基于 Cobar ,是一个实现了 MySQL 协议的服务器,可以把它看 作是一个数据库代理
Heisenberg百度热重启配置、可水平扩容、遵守 MySQL 原生协议、无语言限制。
KingshardKingshard由 Go 开发高性能 MySQL Proxy 项目,在满足基本的读写分离的 功能上,Kingshard 的性能是直连 MySQL 性能的80%以上。
Vitess谷歌、 YoutubeRpc方式,集群基于ZooKeeper管理
DRDS阿里专注于解决单机关系型数据库扩展性问题.
  1. JDBC Proxy,从jdbc连接层面下手,需要对不同的语言编写 Driver
组件公司功能
TDDL阿里淘宝动态数据源、读写分离、分库分表,很久没更新了
Zebra美团点评动态数据源、读写分离、分库分表、CAT监控,接入复杂、限制多。
MTDDL美团点评动态数据源、读写分离、分布式主键生成、分库分表、连接池、SQL监控
  1. sharding-jdbc:
    在这里插入图片描述

轻量级Java框架,在Java的JDBC层提供的额外服务。以jar包形式使用客户端直连数据库,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。

  • ORM框架,JPA, Hibernate, Mybatis, Spring JDBC,甚至直接使用JDBC。
  • 连接池,DBCP, C3P0, BoneCP, Druid, HikariCP。
  • 数据库,MySQL,Oracle,SQLServer和PostgreSQL。

1.2.2 数据

  1. 分库分表课题:
    • 分表维度矛盾(用户,时间)
    • 查询复杂度上升(买家,卖家)
    • 数据聚合运算难度增加(数据统计)
  2. 亿级数据扩容课题:
    • 扩容变得复杂(影响数据分片)
  3. 本课题:
    • 多库多表怎么保证生成的订单号唯一

2、分布式订单生成策略

  • springboot下,基于sharding-jdbc的框架简介。
  • 分表下的订单表案例介绍,userid维度(分库雷同)
  • 启动与调试,按userid验证数据落库,再查询
  • 重点:分布式id的生成策略

2.1 自增

2.1.1 问题背景

  1. 业务代码

    	@GetMapping("/incadd")
    	public Incorder add(int userid){
    		Incorder incorder = new Incorder();
    		incorder.setUserid(userid);
    		mapper.insert(incorder);
    		return incorder;
    	}
    
  2. 运行结果
    在这里插入图片描述在这里插入图片描述

  3. 分析

    • 单表下自增功能不会造成数据错乱,数据库自身特性保障了主键的安全
    • 会泄露id规律,数据隔离做不好的话,不法分子可能会循环撞库窃取订单数据
    • 自增是表维度,一旦拆表,多个自增,有序性被打破

2.1.2 起始点分段

  1. 方案
    设置表2的起始点,再来跑试试……

    #用以下sql,或者客户端工具设置:
    ALTER TABLE incorder_1 AUTO_INCREMENT=10;
    
  2. 优缺点

    • 简单容易,数据库层面设置,代码是不需要动的
    • 边界的切分人为维护,操作复杂,触发器自动维护可以实现但在高并发下不推荐

2.1.3 分段步长自增

  1. 方案

    --查看
    show session variables like 'auto_inc%';
    show global variables like 'auto_inc%';
    --设定自增步长
    set session auto_increment_increment=2;
    --设置起始值
    set session auto_increment_offset=1;
    --全局的
    set global auto_increment_increment=2;
    set global auto_increment_offset=1;
    
  2. 问题

    • 影响范围不可控,要么session每次设置,忘记会出乱子。要么全局设置,影响全库所有表
    • 结论:不可取!!!

2.1.4 Sequence特性

仅限于oracle和sqlserver,主流mysql不支持

-- 创建一个sequence:
create sequence sequence_name as int minvalue 1 maxvalue 1000000000 start with 1
increment by 1 no cache;
-- sequence的使用:
sequence_name.nextval
sequence_name.currval
-- 在表中使用sequence:
insert into incorder_0 values(sequence_name.nextval,userid);

2.2 业务规则

2.2.1 方案思想

不用自增,自定义id,加上业务属性,从业务细分角度对并发性降维。例如淘宝,在订单号中加入用户id。

加上用户id后,并发性维度降低到单个用户,每个用户的下单速度变的可控。

时间戳+userid,业务角度,一个正常用户不可能1毫秒内下两个单子,即便有说明是刻意刷单,应该被前端限流。

2.2.2 实现

	@GetMapping("/busiadd")
	public Strorder busiadd(int userid){
		Strorder order = new Strorder();
		order.setId(System.currentTimeMillis()+"-"+userid);
		order.setUserid(userid);
		strorderMapper.save(order);
		return order;
	}

2.3 集中式分配

2.3.1 MaxId表

  1. 通过一张max表集中分配

    CREATE TABLE `maxid` (
    	`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    	`name` varchar(50) DEFAULT NULL,
    	`nextid` bigint(20) DEFAULT NULL,
    	PRIMARY KEY (`id`)
    );
    insert into maxid(name,nextid) values ('orders',1000);
    
  2. 创建函数

    	DROP FUNCTION getid;
    	-- 创建函数
    	CREATE FUNCTION getid(table_name VARCHAR(50))
    	RETURNS BIGINT(20)
    	BEGIN
    		-- 定义变量
    		DECLARE id BIGINT(20);
    		-- 给定义的变量赋值
    		update maxid set nextid=nextid+1 where name = table_name;
    		SELECT nextid INTO id FROM maxid WHERE name = table_name;
    		-- 返回函数处理结果
    		RETURN id;
    	END
    
  3. StrorderMapper调整id策略,借助mybatis的SelectKey生成id,注意Before=true

    	@Insert({
    		"insert into strorder (id,userid)",
    		"values (#{id},#{userid,jdbcType=INTEGER})"
    	})
    	@SelectKey(statement="SELECT getid('orders') from dual",keyProperty="id", before=true, resultType=String.class)
    	int getIdSave(Strorder record);
    
    	/**
    	* maxid表验证
    	*/
    	@GetMapping("/maxId")
    	public Strorder maxId(int userid){
    		Strorder order = new Strorder();
    		order.setUserid(userid);
    		strorderMapper.getIdSave(order);
    		return order;
    	}
    
  4. 启动验证分表的id情况,maxid表的记录情况。

  5. 优缺点

    • 不需要借助任何中间件,数据库内部解决
    • 表性能问题感人,下单业务如果事务过长,会造成锁等待

2.3.2 分布式缓存

通过redis的inc原子属性来实现

  1. 配置redis服务器
    	# Redis服务器地址
    	spring.redis.host=127.0.0.1
    	# Redis服务器连接端口
    	spring.redis.port=6379
    
  2. 使用redis主键
    	@GetMapping("/redisId")
    	public Strorder redisId(int userid){
    		Strorder order = new Strorder();
    		order.setId(template.opsForValue().increment("next_order_id").toString());
    		order.setUserid(userid);
    		strorderMapper.save(order);
    		return order;
    	}
    
  3. 优缺点
    • 需要额外的中间件redis
    • 与db相比不够直观,不方便查看当前增长的id值,需要额外连接redis服务器读取
    • 性能不是问题,redis得到业界验证和认可
    • 对redis集群的可靠性要求很高,禁止出现故障,否则全部入库被阻断
    • 数据一致性需要注意,尽管redis有持久策略,down机恢复时需要确认和当前库中最大id的一致性

2.4 uuid

2.4.1 代码生成

  1. 业务代码
    	@GetMapping("/uuid")
    	public Strorder uuid(int userid){
    		Strorder order = new Strorder();
    		order.setId(UUID.randomUUID().toString());
    		order.setUserid(userid);
    		strorderMapper.save(order);
    		return order;
    	}
    
  2. 启动,数据库验证save结果
  3. 优缺点
    • 最简单的方案,数据迁移方便
    • 缺点也是非常明显的,太过冗长,非常的不友好,可读性极差
    • 需要使用字符串存储,占用大量存储空间
    • 在建立索引和基于索引进行查询时性能不如数字

2.5 雪花算法

2.5.1 概论

在这里插入图片描述

UUID 能保证时空唯一,但是过长且是字符,雪花算法由Twitter发明,是一串数字。 Snowflake是一种约定,它把时间戳、工作组ID、工作机器 ID、自增序列号组合在一起,生成一个 64bits 的整数 ID,能够使用(2^41)/(1000606024365) = 69.7 年,每台机器每毫秒理论最 多生成 2^12 个 ID

1 bit:固定为0
二进制里第一个bit如果是 1,表示负数,但是我们生成的 id都是正数,所以第一个 bit 统一都是 0。
41 bit:时间戳,单位毫秒
41 bit 可以表示的数字多达 2^41 - 1,也就是可以标识 2 ^ 41 - 1 个毫秒值。
注意!这个时间不是绝对时间戳,而是相对值,所以需要定义一个系统开始上线的起始时间
10 bit:哪台机器产生的
代表的是这个服务最多可以部署在 2^10 台机器上,也就是 1024 台机器。
官方定义,前5 个 bit 代表机房 id,后5 个 bit 代表机器 id。
这10位是机器维度,可以根据公司的实际情况自由定制。
12 bit:自增序列
同1毫秒内,同一机器,可以产生2 ^ 12 - 1 = 4096个不同的 id。
优缺点:

  • 不依赖第三方介质例如 Redis、数据库,本地程序生成分布式自增 ID
  • 只能保证在工作组中的机器生成的 ID 唯一,不同组下可能会重复
  • 时间回拨后,生成的 ID 就会重复,所以需要保持时间是网络同步的。

2.5.2 实现

  1. 自己用java代码实现
    工具类:

    @Component
    public class Snowflake {
    
        /** 序列的掩码,12个1,也就是(0B111111111111=0xFFF=4095) */
        private static final long SEQUENCE_MASK = 0xFFF;
    
        /**系统起始时间,这里取2020-01-01 **/
        private long startTimeStamp = 1577836800000L;
    
        /** 上次生成 ID 的时间截 */
        private long lastTimestamp = -1L;
    
        /** 工作机器 ID(0~31) */
        private long workerId;
    
        /** 数据中心 ID(0~31) */
        private long datacenterId;
    
        /** 毫秒内序列(0~4095) */
        private long sequence = 0L;
    
    
        /**
         * @param datacenterId 数据中心 ID (0~31)
         * @param workerId     工作机器 ID (0~31)
         */
        public Snowflake(@Value("${snowflake.datacenterId}") long datacenterId, @Value("${snowflake.workerId}") long workerId) {
            if (workerId > 31 || workerId < 0) {
                throw new IllegalArgumentException("workId必须在0-31之间,当前="+workerId);
            }
            if (datacenterId > 31 || datacenterId < 0) {
                throw new IllegalArgumentException("datacenterId必须在0-31之间,当前="+datacenterId);
            }
    
            this.workerId = workerId;
            this.datacenterId = datacenterId;
        }
    
        /**
         * 加锁,线程安全
         * @return long 类型的 ID
         */
        public synchronized long nextId() {
            long timestamp = currentTime();
    
            // 如果当前时间小于上一次 ID 生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
            if (timestamp < lastTimestamp) {
                throw new RuntimeException("时钟回退!时间差="+(lastTimestamp - timestamp));
            }
    
            // 同一毫秒内,序列增加
            if (lastTimestamp == timestamp) {
                //超出阈值。思考下为什么这么运算?
                sequence = (sequence + 1) & SEQUENCE_MASK;
                // 毫秒内序列溢出
                if (sequence == 0) {
                    //自旋等待下一毫秒
                    while ((timestamp= currentTime()) <= lastTimestamp);
                }
            } else {
                //已经进入下一毫秒,从0开始计数
                sequence = 0L;
            }
    
            //赋值为新的时间戳
            lastTimestamp = timestamp;
    
            //移位拼接
            long id = ((timestamp - startTimeStamp) << 22)
                    | (datacenterId << 17)
                    | (workerId << 12)
                    | sequence;
    
            System.out.println("new id = "+id);
            System.out.println("bit id = "+toBit(id));
    
            return id;
        }
    
    
        /**
         * 返回当前时间,以毫秒为单位
         */
        protected long currentTime() {
            return System.currentTimeMillis();
        }
    
        /**
         * 转成二进制展示
         */
        public static String toBit(long id){
            String bit = StringUtils.leftPad(Long.toBinaryString(id), 64, "0");
            return bit.substring(0,1) +
                    " - " +
                    bit.substring(1,42) +
                    " - " +
                    bit.substring(42,52)+
                    " - " +
                    bit.substring(52,64);
        }
        public static void main(String[] args) {
            Snowflake idWorker = new Snowflake(1, 1);
    
            for (int i = 0; i < 10; i++) {
                long id = idWorker.nextId();
                System.out.println(id);
                System.out.println(toBit(id));
            }
            System.out.println(toBit(503226745374126080L));
    
        }
    }
    

    springboot启动参数,指定机器编号:

    snowflake.datacenterId=1
    snowflake.workerId=1
    

    业务部分:

    /**
     * 自定义雪花算法
     */
    @GetMapping("/myflake")
    public Strorder myflake(int userid){
        Strorder order = new Strorder();
        order.setId(String.valueOf(snowflake.nextId()));
        order.setUserid(userid);
        strorderMapper.save(order);
        return order;
    }
    
    • 代码启动生成,分析位数
    • 更改机器id,分析位数
  2. 借助sharding配置
    配置信息,非常简单

    spring.shardingsphere.sharding.tables.strorder.key-generator.column=id
    spring.shardingsphere.sharding.tables.strorder.key-generator.type=SNOWFLAKE
    spring.shardingsphere.sharding.tables.strorder.key-generator.props.worker.id=3
    

    Mapper代码

    @Insert({
    	"insert into strorder (userid)",
    	"values (#{userid,jdbcType=INTEGER})"
    })
    @SelectKey(statement="SELECT max(id) from strorder where userid=#{userid,jdbcType=INTEGER}", keyProperty="id", before=false,resultType=String.class)
    int shardingIdSave(Strorder record);
    

    业务代码

    	/**
    	* sharding的雪花算法
    	*/
    	@GetMapping("/shardingFlake")
    	public Strorder shardingFlake(int userid){
    		Strorder order = new Strorder();
    		order.setUserid(userid);
    		strorderMapper.shardingIdSave(order);
    		System.out.println(Snowflake.toBit(Long.valueOf(order.getId())));
    		return order;
    	}
    

    结果分析

    • 生成的id号由sharding-jdbc自动添加到maper的sql中
    • 机器编号为3,所以打印的bit中机器为 00011,修改为其他机器,测试结果

    sharding源码分析:

    package org.apache.shardingsphere.core.strategy.keygen;
    
    import com.google.common.base.Preconditions;
    import java.util.Calendar;
    import java.util.Properties;
    import lombok.Generated;
    import org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator;
    
    public final class SnowflakeShardingKeyGenerator implements ShardingKeyGenerator {
        public static final long EPOCH;
        private static final long SEQUENCE_BITS = 12L;
        private static final long WORKER_ID_BITS = 10L;
        private static final long SEQUENCE_MASK = 4095L;
        private static final long WORKER_ID_LEFT_SHIFT_BITS = 12L;
        private static final long TIMESTAMP_LEFT_SHIFT_BITS = 22L;
        private static final long WORKER_ID_MAX_VALUE = 1024L;
        private static final long WORKER_ID = 0L;
        private static final int DEFAULT_VIBRATION_VALUE = 1;
        private static final int MAX_TOLERATE_TIME_DIFFERENCE_MILLISECONDS = 10;
        private static TimeService timeService = new TimeService();
        private Properties properties = new Properties();
        private int sequenceOffset = -1;
        private long sequence;
        private long lastMilliseconds;
    
        public SnowflakeShardingKeyGenerator() {
        }
    
        public String getType() {
            return "SNOWFLAKE";
        }
    
        public synchronized Comparable<?> generateKey() {
            long currentMilliseconds = timeService.getCurrentMillis();
            if (this.waitTolerateTimeDifferenceIfNeed(currentMilliseconds)) {
                currentMilliseconds = timeService.getCurrentMillis();
            }
    
            if (this.lastMilliseconds == currentMilliseconds) {
                if (0L == (this.sequence = this.sequence + 1L & 4095L)) {
                    currentMilliseconds = this.waitUntilNextTime(currentMilliseconds);
                }
            } else {
                this.vibrateSequenceOffset();
                this.sequence = (long)this.sequenceOffset;
            }
    
            this.lastMilliseconds = currentMilliseconds;
            return currentMilliseconds - EPOCH << 22 | this.getWorkerId() << 12 | this.sequence;
        }
    
        private boolean waitTolerateTimeDifferenceIfNeed(long currentMilliseconds) {
            try {
                if (this.lastMilliseconds <= currentMilliseconds) {
                    return false;
                } else {
                    long timeDifferenceMilliseconds = this.lastMilliseconds - currentMilliseconds;
                    Preconditions.checkState(timeDifferenceMilliseconds < (long)this.getMaxTolerateTimeDifferenceMilliseconds(), "Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds", new Object[]{this.lastMilliseconds, currentMilliseconds});
                    Thread.sleep(timeDifferenceMilliseconds);
                    return true;
                }
            } catch (Throwable var5) {
                Throwable $ex = var5;
                throw $ex;
            }
        }
    
        private long getWorkerId() {
            long result = Long.valueOf(this.properties.getProperty("worker.id", String.valueOf(0L)));
            Preconditions.checkArgument(result >= 0L && result < 1024L);
            return result;
        }
    
        private int getMaxVibrationOffset() {
            int result = Integer.parseInt(this.properties.getProperty("max.vibration.offset", String.valueOf(1)));
            Preconditions.checkArgument(result >= 0 && (long)result <= 4095L, "Illegal max vibration offset");
            return result;
        }
    
        private int getMaxTolerateTimeDifferenceMilliseconds() {
            return Integer.valueOf(this.properties.getProperty("max.tolerate.time.difference.milliseconds", String.valueOf(10)));
        }
    
        private long waitUntilNextTime(long lastTime) {
            long result;
            for(result = timeService.getCurrentMillis(); result <= lastTime; result = timeService.getCurrentMillis()) {
            }
    
            return result;
        }
    
        private void vibrateSequenceOffset() {
            this.sequenceOffset = this.sequenceOffset >= this.getMaxVibrationOffset() ? 0 : this.sequenceOffset + 1;
        }
    
        @Generated
        public static void setTimeService(TimeService timeService) {
            SnowflakeShardingKeyGenerator.timeService = timeService;
        }
    
        @Generated
        public Properties getProperties() {
            return this.properties;
        }
    
        @Generated
        public void setProperties(Properties properties) {
            this.properties = properties;
        }
    
        static {
            Calendar calendar = Calendar.getInstance();
            calendar.set(2016, 10, 1);
            calendar.set(11, 0);
            calendar.set(12, 0);
            calendar.set(13, 0);
            calendar.set(14, 0);
            EPOCH = calendar.getTimeInMillis();
        }
    }
    
  3. 时钟回退问题
    关于snowflake算法的缺陷(时钟回拨问题),sharding-jdbc没有给出解决方案

2.5.3 第三方实现

  1. 百度UidGenerator
    • 位数不太一样,1-28-22-13
    • 需要mysql数据库建表,来自动配置工作节点
    • 支持spring配置与集成
    • 支持bit位自定义,及bit分配相关建议
    • https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md
  2. 美团Leaf-snowflake
    • 位数沿用snowflake方案的bit位设计
    • 使用Zookeeper持久顺序节点的特性自动对snowflake节点配置wokerID
    • 解决了时钟回退问题
    • 线上可靠性验证,美团的金融、支付交易、餐饮、外卖、酒店旅游、猫眼电影等众多业务
    • https://tech.meituan.com/2017/04/21/mt-leaf.html

3、支付场景下的订单系统

3.1 支付政策

  1. 支付牌照是干啥的?

理论上,没有支付牌照,电商只能做自营。凡是涉及B端用户在平台开展业务,就会涉及资金流动问题。别人的钱通过平台支付转手,就需要经过批准。这个批准所获得的资格就是支付牌照。

  1. 217号文的下达

主要给出了无证经营支付业务的主要认定标准:采取平台对接或“大商户”模式,即客户资金先划转至网络平台账户,再由网络平台结算给该平台二级商户,均属于无证经营支付业务。

  1. “一清”与“二清”是什么:

一清公司的支付不需要支付牌照,但是也不做资金结算,而是交给银联来结算。
二清公司则是没有支付牌照却做着资金结算工作。
简单来说就是平台接不接触到钱的问题。

  1. 为什么这么做?二清的风险在哪里

买家的钱应该给卖家才对,现在给到平台再由平台转交。
那么平台无授权无牌照的情况下,跑了怎么办?

  1. 那么对订单系统的影响在哪里?
    如果你所在公司涉及二清不合规问题。那么订单系统要注意。一般来说,银行接口要求以下操作:
    • 商户入驻,确立平台方,商户方的虚拟子账户。买方也就是支付方不需要入驻
    • 支付下单时条目带分成(一般两种方式,比例和金额)
    • 确认收货后,平台调银行订单结算接口完成交易

3.2 条目折扣

活动折扣比例折算到条目
回顾满减活动问题。满99减9,那么设计订单条目表时,要带有实际折扣价,而比例放在订单上
订单表记录:
在这里插入图片描述
条目表记录:
在这里插入图片描述平账:
30x0.9090 + 40x0.9090 +(50*0.909+1.92)= 111

3.3 退货换货

  1. 退货设计:

    退货要生成退货单,关联旧订单id,条目也关联旧条目id,而原始订单不做任何改动。
    调取银行时,调接口,对应的条目退货即可,资金会由银行原路返回。
    注意赠品返还和实际退款金额问题。

  2. 换货设计:

    换货也要生成退货单,关联新旧两条订单id,条目关联旧条目id,用来记录要拿哪些条目换。
    同时生成新的订单,表示要换成的新商品。订单类型标注为换货单
    换货时的价格折算问题:多退少补。如果多好办,抵扣后,剩余条目走银行退货接口
    补的时候比较麻烦。这就涉及到下面的支付单。

3.4 分期支付

支付单的设计:
常规情况下,一笔订单一笔支付单,支付单上挂订单号,金额=订单应付金额
如果是上面的补单,支付金额就需要作为差价记录实际支付金额,条目标记为换货差价 涉及分期支付,对应多笔支付单,形成虚拟条目标注支付内容。

3.5 订单状态与接口关系

下单 → 新建,不需要调银行接口
支付 → 支付成功,调银行支付接口,只是支付成功,没有分成
确认收货 →结算,调银行结算接口,银行会进行清算操作

3.6 超时订单取消

在这里插入图片描述

根据库存设计对订单有不同的处理策略:
下单减库存的,要注意超时取消,大订单量及分库分表条件下,扫表方案不可取,应该设计为延迟消费支付减库存的,不需要额外处理。

3.7 对账单与结算单

在“二清”政策下,对账流程变成两步,类比旧的支付,对应单子也会变成两种
支付成功的:进入支付对账
确认收货的:进入结算对账

4、系统级可用性保障

4.1 数据一致性

  1. 订单周边服务:

    下订单过程,业务极其复杂,不只是订单号的生成插入,除了订单系统,还可能涉及库存系统,促销系统,支付系统,结算系统,积分系统,同时可能有下游的订单统计中心。

  2. 双向接口:

    调用需要返回值的交互,比如调促销系统,获取促销信息。
    多为强依赖的关联,使用分布式框架,基于框架层面的重试机制,接口幂等设计,保障数据的最终一致性。

  3. 单向接口:

    属于通知类调用,不需要返回值。如下单后通知给下游的订单统计中心,或大屏展示可以采用扔消息队列,基于消息队列层面做高可用与调优。对订单系统来说,只需要保障投放时的消息 确认即可。下游消费端是消费方需要关注的事情。

  4. 重复数据问题:

    一般的重复性数据,只要做到幂等设计,不会发生。
    多见于支付环节。即一条订单对应了多条支付单。
    策略:人工确认,接口退单,对账保底。

4.2 数据库高可用

  1. 日常备份

    冷备:文件级备份,快速且完整。读写操作均不可进行,需要停机。作为灾难时恢复到某个时间点
    热备:读写操作均可执行,作为备用库待命,down机时及时顶上去。

  2. 机房灾备

    同一服务下,日志文件与数据文件分盘放
    跨机房,双主单写,事务日志校验,灾备切换。

4.3 应用级保护

  1. 限流,防止刷单

    nginx,lua+redis,sentinel

  2. 异步排队,秒杀消息队列排队,异步消费

    秒杀请求到来后进入mq,后端下单服务异步消费,前台轮询查询排队状态。

  3. 周边服务降级

    如积分,评价,某些统计,爬虫,推荐服务降级,延迟处理。

标签:userid,long,id,订单,private,千万级,生成,order
From: https://blog.csdn.net/qq_54698124/article/details/144510206

相关文章

  • 最新Midjourney/AI绘画系统+分销推介,GPT4.0模型支持,联网提问总结,AI文生图/图生图/垫图
    目录一、人工智能系统介绍文档二、功能模块系统快速体验三、系统功能模块3.1AI全模型支持/插件系统AI大模型多模态模型文档分析多模态识图理解能力联网搜索回复总结3.2AI智能体应用3.2.1AI智能体/GPTs商店3.2.2AI智能体/GPTs工作台3.2.3自定义创建AI智能体......
  • 零代码生成管道+粒子特效!炫酷智慧管廊可视化!
    本系统通过数字孪生技术,实现智慧管廊可视化管理,系统打破了传统管理模式下数据分散、孤立的壁垒,将GIS地理信息数据与城市管道、设备数据深度融合,实现了数据的互联互通与共享协同,助力管理者做出科学决策。1.丰富的标绘功能丰富的标绘功能是本系统的一大亮点,其创新性地实现了零......
  • 阿里云百炼大模型生成贪吃蛇小游戏
    阿里云百炼大模型生成贪吃蛇小游戏为了在贪吃蛇游戏中添加背景音乐,我们可以使用Pygame的mixer模块。以下是修改后的代码,包含了背景音乐的加载和播放功能:安装Pygame(如果你还没有安装):pipinstallpygame准备音乐文件:确保你有一个音乐文件(例如background_music.mp3),并将......
  • 强烈推荐!在线免费体验AI图像生成的最佳平台
    随着人工智能技术的迅速发展,AI图像生成工具已经成为创作领域的重要助手。今天我要向大家推荐一个可以在线免费体验AI图像生成的平台。这不仅是一个探索创意的工具,更是开启视觉艺术之旅的钥匙。为什么推荐这个平台?1.无需下载,在线即用不需要安装任何软件,只需访问网站,就能立刻体......
  • AI大模型 | 2024年AI生成式营销产业研究蓝皮书
    生成式AI正在开启营销的新纪元,它通过赋能生产工具,全面提升营销业务流程中的生产力,并重构营销组织生产关系。这一变革不仅限于传统广告和社交媒体营销,还扩展到了电商运营、客户运营和产品创新等新兴领域。人工智能在营销领域的应用正变得越来越多元化,其多维能力主要体现在创......
  • 一键生成图片绘制过程 | 大模型发展竟这般快速!
    大家好,这里是白泽,详细演示一个今年7月份由StableDiffusion的ControlNet插件的作者张博士所推出的最新模型——Paints_UNDO。地址https://github.com/lllyasviel/Paints-UNDO简介Paints_UNDO旨在提供人类绘画行为的基础模型,希望未来的人工智能模型可以更好地与人类艺术家......
  • 字体图片生成:Rectified Flow控制条件
    在《AIGC文生图技术入门:RectifiedFlow字体生成》一文中,控制条件是字符编码,我们难以控制生成图片的字体风格,只能通过改变输入高斯噪声,生成大量图片,从中筛选符合要求的,这样使用起来很不方便。在本实验中,我们将修改控制条件为:条件1:字符编码条件2:字体类型编码条件1是严格条件,条......
  • AIGC生成星际探险游戏
    工具:豆包提示词生成星际探险游戏,角色为星际旅行者王伟,飞船名词星际探险号,要求简洁清晰,直接进行对话,提示内容简介,通过选择数字进行对话,立即游戏生成内容如下《星际探险》游戏简介你将扮演星际旅行者王伟,驾驶着“星际探险号”飞船在浩瀚宇宙中展开惊险刺激的探险之旅。......
  • vb编译环境运行没问题,生成exe运行时报错,错误"48"加载dll错误,右键以管理员身份运行可以
    解决办法(推荐):打开vb后,弹出新建工程标准exe,要点打开。然后再打开已建的工程,这样生成的exe可以直接双击运行,就不会报错了。 2、如需重装vb,要记得“数据访问”点“更改选项”去掉ADO和RDS前面的勾选,不然会一直停在更新状态。1.打开安装包点击SETUP.EXE(如果会跳出一个兼容性......
  • hibernate2中实体字段太长并且相似导致生成的原生sql字段名重复
    1:错误的hbm.xml<propertyname="holiday1"type="java.lang.String"><columnname="HOLIDAY1"/></property><propertyname="holiday1Start"type="java.util.Date"&......