简历上描述

**项目名称:**12306铁路购票系统
**项目描述:**12306 铁路购票系统,帮助用户完成互联网在线购票,提高居民买票效率以及减少售票人员工作。项目基础架构采用 JDK17、SpringBoot3 和SpringCloud Alibaba 构建,完成会员注册、车票查询、车票下单以及支付等业务。底层采用缓存、消息队列以及分库分表等技术支持海量用户购票以及数据存储。
**核心技术: **SpringBoot + SpringCloudAlibaba + RocketMQ + ShardingSphere + Redis + MySQL+ Sentine+ Hippo4j
功能描述:

  • 使用责任链模式重构请求数据准确性检验,比如:查询购票、购买车票下单以及支付结果回调等业务。
  • 通过 RocketMQ 延时消息特性,完成用户购票 10 分钟后未支付情况下取消订单功能。
  • 封装缓存组件库避免注册用户时,用户名全局唯一带来的缓存穿透问题,减轻数据库访问压力。
  • 通过引入路由表解决查询用户信息时不带分片键将会触发的读扩散问题。完成通过不同的登陆方式进行登录的功能。
  • 通过开发防重复提交注解,实现了接口幂等和消息队列消费幂等,实现了数据一致性。
  • 封装分布式雪花算法组件库,解决Mybatis-Plus的雪花算法在集群条件下可能会生成重复id的问题。
  • 负责设计和实施了敏感数据加密与脱敏机制,确保用户的个人信息安全。
  • 引入Hippo4j动态可监控线程池,优化一个用户购买多种类型的车座功能,极大程度上节省执行效率,提升系统吞吐量。
  • 使用 BinLog 配合 RocketMQ 消息队列完成 MySQ数据库与 Redis 缓存之间的数据最终一致性。
  • 通过 Redis Lua 脚本原子特性,完成用户购票令牌分配,通过令牌限流以应对海量用户购票请求。
  • 通过订单号和用户信息复合分片算法完成订单数据分库分表,支持订单号和用户查询维度。
  • 创建订单明细与乘车人的关联表,分库分表规则同订单,完成乘车人账号登录查询本人车票功能。

使用责任链模式重构请求数据准确性检验,比如:查询购票、购买车票下单以及支付结果回调等业务。

学习设计模式有什么用?

设计模式主要是为了应对代码的复杂性,让其满足开闭原则,提高代码的扩展性
什么是责任链模式?
举个很经常用的例子,在springmvc中可以定义拦截器,而且可以定义多个,当一个用户发起请求的时候,顺利的话请求会经过所有拦截器(请求不满足拦截器自定义规则会被打回),最终到达业务代码逻辑,SpringMVC的拦截器设计就是使用了责任链设计模式

责任链模式中多个处理器形成的处理器链在进行处理请求时,有两种处理方式:
1请求会被所有的处理器都处理一遍,不存在中途终止的情况,比如MyBatis 拦截器,这种类型的链路重点在于对请求过程中的数据或者行为进行改变,比如MyBatis 中的分页语句就是经过拦截器进行加工之后实现的。
2二则是处理器链执行请求中,某一处理器执行时,如果不符合自制定规则的话,停止流程,并且剩下未执行处理器就不会被执行,比如 SpringMvc 拦截器,一个例子就是token拦截器,如果token失效就不能继续访问系统,拦截器会将请求打回。

查询购票

在实际购票业务场景中,用户发起一次购票请求后,购票接口在真正完成创建订单和扣减余票行为前,需要验证当前请求中的参数是否正常请求,或者说是否满足购票情况。
1购票请求用户传递的参数是否为空,比如:车次 ID、乘车人、出发站点、到达站点等。
2购票请求用户传递的参数是否正确,比如:车次 ID 是否存在、出发和到达站点是否存在等。
3需要购票的车次是否满足乘车人的数量,也就是列车对应座位的余量是否充足。
4乘客是否已购买当前车次,或者乘客是否已购买当天时间冲突的车次。

购买车票

只要是跟购买车票区间有交集或者是说有重叠的车站,余票都需要扣减
购买车票时候座位分配逻辑:
如果购票人数为两人,购买同一车厢,座位优先检索两人相邻座位并并排分配。
假设当前正在检索的车厢不满足两人并排,就执行搜索全部满足两人并排的车厢
如果搜索了所有车厢还是没有两人并排坐的位置,就执行同车箱不相邻座位。
如果所有车厢都是仅有一个座位,最后执行降级操作,进行不同车厢的分配。

通过 RocketMQ 延时消息特性,完成用户购票 10 分钟后未支付情况下取消订单功能。

订单延时关闭功能技术选型/为什么选择rocketmq来实现这个功能?

  1. 定时任务/xxl-job 他的原理是根据订单的创建时间扫描所有到期的订单,并对过期的执行关闭的操作。 但是这也导致了一个问题,他是每隔一段时间来进行扫描的,比如说一分钟,这就可以导致有1-59s的时间误差,也就是延迟的时间不够精确
    而且这种实现方式在高并发场景下,可能导致大量的定时任务同时执行,造成系统负载过大
    而且还存在分库分表问题:拿 12306 来说,订单表按照用户标识和订单号进行了分库分表,那这样的话,和上面说的根据订单创建时间去扫描一批订单进行关闭,自然就行不通。因为根据创建时间查询没有携带分片键,存在读扩散问题。
  2. RabbitMQ(延迟插件) rabbitmq的架构导致了没有rocketmq和kafka做的可靠性那么好
  3. Redis过期监听 可用性,可能会宕机,可能会丢数据,不存在mq的那种ack机制,专业的能力交给专业的人去做,就直接上mq就行
  4. Redission
  5. RocketMQ 重复消息(用幂等注解去做)
    高并发的时候引入动态线程池hippo4j,消费不过来,说明是两种情况,一种是生产者生产的太快了,一种是消费者消费的太慢了,前者是不可控的。所以就后者而言,有两种解决方案,一种就是添加客户端的数量,去消费。另一种是提高单个客户端的消费速度,通过动态线程池去提高消费速度

封装缓存组件库避免注册用户时,用户名全局唯一带来的缓存穿透问题,减轻数据库访问压力。

缓存穿透问题:用户注册时,需要验证用户名是否已经存在,这通常需要去查询缓存和数据库,如果缓存没有就会去数据库查询,如果数据库也没有,说明这个用户名可用。
在高并发情况下,可能有大量新用户同时注册,这将导致缓存中没有这个新用户名,都去查询数据库,造成缓存穿透。
为了防止缓存穿透,可以有以下的处理方式:

  1. 对不存在的key进行缓存,值设置为null,并且设置短暂的过期时间。这种办法的好处就是实现简单,缺点就是对用户体验不友好(因为缓存空对象就意味着这个用户名在这段过期时间中不可用了)。而且如果有大量并发请求查询不存在的用户名,仍然可能会导致缓存穿透。
  2. 使用布隆过滤器,将所有已注册的用户名存入布隆过滤器,判断时先判断是否在布隆过滤器中(不在的话一定不存在用户使用这个用户名),这样可以避免请求过多打到数据库。但是,布隆过滤器无法删除元素,所以这个业务无法使用到生产,我了解过一些布隆过滤器,底层是bit数组,利用几个hash函数进行运算,然后把标志位给标志成1,因为使用到了hash函数,所以有误判的可能和hash冲突的可能,这也是为什么布隆过滤器说存在但是数据库里可能不存在的原因,而且布隆过滤器对元素的删除不太支持,因为hash冲突存在,所以删除不会太方便。而且因为存储的是二进制数据,存储的是0&1,所以保密性很好,并且我们可以自己设计误差率,误差率是根据二进制数据的大小和使用hash函数的个数决定的
  3. 使用redis的set集合来存储用户名,判断时检查是否在集合中。这个的缺点就是把用户数据全部存在了内存中,消耗了大量内存资源

12306 如何解决注册穿透
如果没有用户名注销后可以重复使用的需求,布隆过滤器无疑是最好的解决方案,但是考虑到需求的多样化,设计的时候就要做好全方面的准备。
所以我们可以通过布隆过滤器+一层缓存来解决这个问题,也就是上面提到的2和3组合起来使用。
具体的流程是这样的:

1.用户名 “haohao” 成功注册后,将其添加至布隆过滤器。
2当其他用户查询”haohao”是否已被使用时,首先检查布隆过滤器是否包含该用户名。
3如果布隆过滤器中不存在该用户名,根据布隆过滤器的特点,可以确认该用户名一定没有被使用过,因此返回成功,证明该用户名可用。
4如果布隆过滤器中存在该用户名,进一步检查Redis Set结构中是否包含该用户名。如果存在,表示该用户名已被注销,同样可被再次使用。
5如果布隆过滤器中存在该用户名,但 Redis Set 结构中不存在,说明该用户名已被使用且尚未被注销,因此不可用。

但是使用这种方法也会有缺点,比如查询性能会降低,因为查询的时候会进行两次查询,第一次查询布隆过滤器,第二次查询set结构。另一个是会增加存储损耗,需要多存储一个set结构,不过为了需求这些也是必要的。

不过这个需求仍然会有一些问题:
如果用户频繁的注销用户名,导致set结构变得很庞大,导致bigkey问题
为了防止这种情况,可以采取以下措施:

  1. 对异常行为进行限制:每次用户注销后,记录用户的证件号,并且限制证件号仅可以注销5次,超过五次就会被封号。
  2. 进行分片处理:把一个bigkey通过用户名进行hash运算,分散到1024个set中,可以有效解决这个问题。

通过引入路由表解决查询用户信息时不带分片键将会触发的读扩散问题。完成通过不同的登陆方式进行登录的功能。

在登录功能中,用户一栏明确标出可以使用用户名、邮箱或者手机号中的任意一个搭配密码进行登录,并且在用户表的分库分表中,我们是把用户名当成分片键进行分片的,随意,如果在查询用户信息的时候不带用户名,就会触发读扩散的问题。由于去查询用户信息的时候没带用户名,导致无法确定用户的信息在哪个分片上(分片指的是某个库的某个表上),只能对全部的相关数据库和表进行全表扫描(如果携带了分片键,可以直接找到对用的分片,直接只查询对应的分片就可以了),这就是读扩散。这种情况的话会导致用户的请求响应时间变长。
为了解决这个问题,我们引入了两张路由表:用户手机号表和用户邮箱表,他们存储的内容分别是手机号和用户名、邮箱和用户名。他们的分片键分别是手机号和邮箱,这样能方便我们找到分片。
思考一下,我们怎么区分这是手机号还是用户名还是邮箱,其实我们最终还是要得到用户名,所以可以先从手机号和邮箱中去查询,那么谁最好区分,肯定是邮箱了,他有@标识,所以我们可以通过一个判断标记看这个字符串里是否有@,就说明这个字符串是邮箱还是其他,如果不是邮箱,直接去手机号表中去查(因为这时候这个字符串只可能是手机号和用户名)如果是用户名的话,查不出来内容,就还用原来的字符串当成username就可以了,这样的话就不会造成读扩散了,之后得到了username,就可以去查询username主表进行登录了。
路由表很像一个中间层,通过这个中间层得到我们想要的东西,之后再去查询就可以了,不过引入路由表也有一定的坏处,比如会对查询性能造成影响,因为多加了一次访问数据库的请求。而且增加了维护成本,因为多引入了表,不过为了实现需求,做这些工作也是可以接受的。

通过开发防重复提交注解,实现了接口幂等和消息队列消费幂等,实现了数据一致性。

封装分布式雪花算法组件库,解决Mybatis-Plus的雪花算法在集群条件下可能会生成重复id的问题。

我们为什么使用雪花算法不使用UUID,因为UUID一般都是无序的,这样的话会对数据库性能有影响
雪花算法生成后是一个64bit的数值,组成部分因为有时间戳,所以基本可以保持自增,可以提高索引效率。
雪花算法的组成部分
不使用:1bit,最高位是符号位,0 表示正,1 表示负,固定为 0。
时间戳:41bit,毫秒级的时间戳(41 位的长度可以使用 69 年)。
标识位:
5bit 数据中心 ID
5bit 工作机器 ID,
序列号:12bit 防止同一个机器同一毫秒生成重复,所以有这个。
这样的结构保证了他在索引结构下有高性能。
但是这样的雪花算法存在可能生成重复id。
比如有三个节点,他们部署在同一个k8s机器上,机器号相同,数据中心也相同,而且在同一时刻生成id,就有可能相同,因为是不同的节点,所以没法保证序列号一定不一致。
所以这是一种比较潜在的风险,如果节点少并发少这个雪花算法还可以进行使用,但是如果节点多并发多的话就可能会出现重复id,造成很严重的事故,所以我们自定义了分布式雪花算法。
主要是对标志位进行的分配,因为mybatis是通过mac和进程pid进行生成标志位的,但是这样做会有风险进行重复。
为了生成不重复的标志位,我们有两种方案,一种是进行预分配,也就是人工去申请标识位置,但是这样做的话会有坏处,比如数以万计的节点,都去申请吗。
第二种是进行动态分配,就是通过redis来分配标志位,比如都用一个redis来生成数据中心id和机器号id,这其实已经偏离了原来这个名字的意思了,只是做一个标志的意思,然后每个节点通过使用lua脚本去redis进行申请这两个,并且不会出现并发问题,具体逻辑是:
1.第一个服务节点在获取时,Redis 可能是没有 snowflake_work_id_key 这个 Hash 的,应该先判断 Hash 是否存在,不存在初始化 Hash,dataCenterId、workerId 初始化为 0。
2.在进行分配时,先分配 workerId。
3.判断 workerId 是否 != 31,条件成立对 workerId 自增,并返回;如果 workerId = 31,自增 dataCenterId 并将 workerId 设置为 0。

负责设计和实施了敏感数据加密与脱敏机制,确保用户的个人信息安全。

加密

涉及一些客户安全的数据或者一些商业性的敏感数据,如身份证号、手机号等个人信息按照有关部门的规定,需要对数据加密
在实际的业务场景里需要把这些数据进行加密然后存储到数据库中,在使用的时候再进行解密处理。
这里我们使用的是ShardingSphere进行的加密存储,通过修改shardingsphere的配置文件,进行配置需要加密的字段,然后选择加密算法,他自带的有许多加密算法,我们使用的是里面的aes加密算法,然后把加密密钥进行填入进去就可以了,当我们需要把这些加密数据从数据库中查询出来的时候,shardingsphere会自定帮我们把加密数据转换成明文数据,形成一个加密敏感信息落库闭环。
他的实现原理其实就是对用户发起的sql进行拦截,然后对sql语句进行解析,再根据我们在配置文件中配置的加密规则以及密钥等等,找出需要加密的字段,用加密算法进行加密处理后,再去请求数据库。解密也是同样的道理。
这样通过屏蔽对数据的加密解密处理,使用户无感知在使用加密数据,就像和使用普通数据一样使用加密数据。
如何防止配置文件敏感信息泄漏?

  1. 使用环境变量代替明文配置信息,而不是把明文写到配置文件中。
  2. 加密配置文件,可以使用对称加密或者非对称加密来加密配置文件,以保护敏感信息,在加密配置文件的时候,需要指定加密算法和密钥,以及解密时使用的密钥。
  3. 禁止将配置文件提交到代码仓库,可以添加gitignore
  4. 限制配置文件的权限,防止修改和访问,可以使用操作系统的权限控制功能,比如linux中的文件权限。

脱敏

比如说我们在查看乘车人的时候,把一些数据返回给前端的时候要进行脱敏处理,这样可以保证信息泄漏的风险,并且可以保护用户的隐私,同时保证一定用户信息的可读性。
这里最简单的其实就是前端进行处理,但是有些人会绕过前端,直接获取数据,所以我们必须从后端接口进行处理。
我们这里的实现思路是:在返回数据的时候,通过对序列化器进行替换,也就是在实体类上加上一个JsonSerialize注解来指定这个字段用的序列化器,之后我们就可以进行无感知的替换了,这个序列化器可以根据我们的需求对这个字段的数据进行处理。
但是这样的化还有一个问题,万一我们不想要脱敏后的数据呢,我们想要脱敏前的数据,我们的解决办法是再创建一个DTO实体类,然后这个类的字段上不添加自定义的序列化器,之后再创建一个接口,专门用于返回不脱敏的数据,这样的话,我们就可以自己选取是得到脱敏后的数据还是脱敏前的数据了。

引入Hippo4j动态可监控线程池,优化一个用户购买多种类型的车座功能,极大程度上节省执行效率,提升系统吞吐量。

这里之前的场景是,如果一个用户购买了多种类型的车座,比如同一订单购买了2个一等座和3个二等座,那么就要按照不同的座位逻辑进行选座(也就是对这个订单的一等座的2个人进行分配座位,之后在对二等座的3个人分配座位)这个流程是串行的,但是这个逻辑是可以并行的,如果这个是并行的话,可以极大程度上提高执行效率,提高系统的吞吐量。
所以我们后来引入了动态可监控线程池Hippo4,把这些工作变成异步多线程进行执行,但是还存在一些问题,也就是多线程问题,我们得到的结果是用arraylist存储的,这个集合类在多线程下会存在并发问题,所以我们采用了CopyOnWriteArrayList这个类来存储返回结果,这个类的原理是:再写操作的时候复制数组来保证线程安全,具体一点来说的话,在执行写操作的时候,他会先复制一下原来的数组,并且让写操作在新数组上进行修改,最后再将新数组替换老数组。如果写的时候有一个读线程过来了,读取的还是老数组。换句话说,就是读操作和写操作发生在不同的数组上,所以不会出现线程安全问题,所以他是线程安全集合。
又因为是异步执行嘛,所以有可能我们购票流程都结束了,还没执行完那个分配座位的逻辑,所以,我们需要拿到线程池执行的结果后再继续执行后续的提交订单逻辑,这里我们使用的是Future类,它有一个get方法,是一个阻塞方法,之后等到线程池里面所有的任务结束之后,他才会停止阻塞,我们再进一步执行下面的提交订单逻辑就可以了。
这样做的话还有一个问题,就是如果用户下单的话如果仅选择一种座位的话,还通过线程池进行执行的话会额外增加性能消耗,因为一种座位的话用单线程就可以了,所以我们可以提前一步进行判断下单的是一种座位还是多种座位,然后再判断是否采用线程池进行优化就可以了。

Hippo4j主要做的功能
动态线程池:核心线程数、最大线程数、阻塞队列容量、拒绝策略
监控:查看线程池运行时的数据
运行报警:线程池线程活跃度、任务执行时间

使用 BinLog 配合 RocketMQ 消息队列完成 MySQ数据库与 Redis 缓存之间的数据最终一致性。

先写缓存再写数据库:
这种情况无法保证数据的最终一致性,因为在多请求多并发的场景下,执行的结果和预期的不符
比如写请求A去redis更新缓存为16,然后B请求来了,先把Redis改成15,再去更新Mysql,更新数据库余票为15,之后A又去更新数据库更新余票为16,这导致数据库本该是15的时候又变成了16。
先写数据库再写缓存:
跟上面的这种情况一样,都会导致无法最终一致。
先删除缓存再写数据库:
这种情况也不能保证数据的最终一致性,比如有两个请求,一个写请求一个读请求。首先写请求去删除余票为16的缓存,然后读请求去redis中读取,发现为空,然后去mysql读取发现余票为16,之后写请求更新车站余票更新为15,然后读请求回写缓存余票为16,仍然没有保证最终一致性。

下面的是能保证最终一致性的方案:
延迟双删:
image.png
其实比上面那个就多了最后一步的删除,但是我们无法保证删除车站余票(也就是最后一步操作)在回写缓存之前,那我们就需要吧这个第二次的删除操作进行延迟,比如说睡眠500ms再删除,这个操作我们可以用消息队列来实现,所以上图就可以变成以下
image.png

先写数据库,再删除缓存:
image.png

这种模型缓存与数据库不一致会存在一个很小周期,不过对于绝大多数的情况来说,是可以容忍的。除去一些电商库存、列车余票等对数据比较敏感的情况,比较适合绝大多数业务场景。
先写数据库,再通过Binlog异步更新缓存:
image.png

如果是扣减库存的方案,比如说你将列车余票扣减为 16,但是同时又有一个请求将列车余票扣减为 15,这个时候,扣减为 15 的这个请求先到消息队列执行,将缓存更新为余票 15,但是随之而来的是第一个请求余票为 16,会将缓存余票为 15 给覆盖掉。
类似于这种逻辑,会存在一些数据一致性的问题,需要我们通过其它技术手段完善,比如数据库添加版本号,或者根据最后修改时间等技术规避这些问题。

通过 Redis Lua 脚本原子特性,完成用户购票令牌分配,通过令牌限流以应对海量用户购票请求。

之前我们的购票流程是:
首先通过责任链进行参数校验,比如说传入的参数是否合法,是否还有余票之类的。之后再去获取分布式锁,进行座位分配和选座,再去调用订单服务,之后返回订单号,再扣减票数,再释放锁。这个版本存在一些问题,比如说:SpringBoot Tomcat 容器默认情况下,同一时间最多能处理 200 个请求。如果要应对上千万的 TPS 明显是不可能的。大量请求会因为分布式锁的申请而发生阻塞,这会导致后续的请求长时间被阻塞,使系统陷入假死状态,此外随着请求的积累,还会存在内存溢出的风险。更糟糕的是,如果tomcat线程池全部被分布式锁占用(因为锁的是列车的id)查询请求也将无法得到响应。
所以我们必须要进行限流操作,12306的限流算法类似于令牌桶,但是令牌桶会以固定的速率生成令牌,但是我们这个不是这样的,是将没有出现的座位当成一个个令牌放到一个容器中,这个容器可以称作是令牌容器。如果用户来购票,根据所选择的乘车人数量以及座位类型去令牌容器中获取,如果获取成功则证明余票充足,可以进入之后的座位分配和选座的扣减 下订单环节,如果是获取失败的话会直接进行返回。加入令牌容器之后,只有少量拿到令牌的用户请求可以获取分布式锁
但是在这之前还有一个问题,也就是我们之前使用固定分布式锁是不公平的(因为默认的就是不公平的),虽然我们之前使用了令牌容器来限流,但是仍然可能购票失败,比如两个用户选择同一个座位然后提交,这时候后提交的会购票失败,然后再去获取令牌,所以我们要使用公平锁,因为公平锁的特点就是公平吗,也就是说他维护了一个等待队列,然后记录等待锁的线程,并按照顺序分配锁,先到先得,虽然他在高并发下性能可能会相比于非公平锁比较低,但是我们业务需要所以就使用了公平锁。具体的改造就是改一个方法就行了、或者改一个参数就是公平锁了,默认不公平。公平锁的缺点就是他维护一个等待队列嘛。肯定会增加锁的管理开销,而且涉及到线程的状态切换。我之前有在想过一个问题,为什么大家如此偏爱非公平锁,他肯定有他的优点,通过我的研究,非公平锁的性能比公平锁高很多,原因就是线程的状态进行相互切换,非公平锁会优先选择处于醒着的状态的线程,因为唤醒阻塞的线程需要耗费很多资源。但是这样对于某种业务会造成线程饥饿,可以用调整优先级或者超时机制来解决线程饥饿的问题。再一个,因为都是集群化部署,这里我们做的优化是先去竞争单个服务的内部锁(本地锁),竞争成功后再去竞争分布式锁,这样做的好处是可以减轻redis分布式锁的压力。具体实现是用一个currenthashmap进行存储ReentrantLock,key为根据列车id构建的,value为那个ReentrantLock类,但是这样的话又出现了一些问题,因为列车id随着天数增加一直在改变,所以这个map里面的数据一直在增加,没有清除,因为没有任何的过期策略和内存溢出策略,所以会内存溢出。所以我们采用了caffeine创建本地的安全锁容器,指定这个key在一天后会过期。这样就解决了这个问题。但是加本地锁还有可能存在一些问题,也就是说全局的购票顺序会变成局部顺序。我们必须要在下面的问题做些折中:是要通过分布式锁来确保完全的有序性,还是通过本地锁+分布式锁的组合来牺牲一部分有序性提升性能呢?
得出的结论是需要根据具体的业务需求和系统规模来选择合适的锁策略,在购票人数较多且缓存压力较大的情况下,本地锁和分布式锁的组合可以在一定程度上平衡性能和购票顺序,如果是业务规定一定要按照顺序,那就直接只上分布式锁就可以了。
还有一个可以优化的点:
也就是优化锁的粒度,之前是锁定的是列车的id值,现在我们可以优化到锁定到具体的列车座位类型,但是有一个问题,购票过程中可以为多个乘车人选择不同的座位,这种情况我们可以加多把锁,锁全部获取到才可以执行购票流程,就比如说张三购买了三个座,分别是商务座、一等座、二等座,按之前的流程来看,张三需要获取六把锁(本地三把、分布式三把)这样可能会出现一些死锁问题,但是我们只要破坏形成死锁的四个条件就好了,比如可以规定获取锁的顺序只能是这样,或者如果没有得到锁立即释放就可以了。在这种情况下,理想状态下会提升300%的性能,但是由于座位的问题,多申请了锁,会带来一些性能上的开销。

通过订单号和用户信息复合分片算法完成订单数据分库分表,支持订单号和用户查询维度。

这里我们参考了阿里的订单号设计,采用了基因法,基因法其实就是我们把用户id的后六位数据给冗余到了订单号里,这样的话,我们就可以根据用户id后六位进行分库分表,因为订单号和用户id里面都有这六位,所以我们分片键定为用户id和订单号,只要查询中携带这两个字段中的一个,我们就可以通过这六位找到分片表的位置,而不至于导致读扩散问题导致全表全库扫描。
这里我们使用了自定义的分片算法:
如果传入用户id或者订单号,我们就直接截取后六位进行分片映射。

创建订单明细与乘车人的关联表,分库分表规则同订单,完成乘车人账号登录查询本人车票功能。

对于订单管理,我们一共有三张表:
一张是订单的主表,记录12306用户购买的车票订单,但是这个订单中会有多个乘车人,所以还有订单明细表。
第二张表是订单明细表:一个订单有多个乘车人,多个乘车人对应多个订单明细,和第一张表是一对多的关系,跟第一张表的分库分表规则是根据订单号/用户id进行的复合分片,采用复合分片的原因是因为业务的需求,一个分片键只能支持单个查询场景,用户 ID 可以支持查询当前用户下所有订单,订单 ID 支持查询明细。这两张表互为绑定表的关系。
第三张表是订单明细乘车人表:
这张表是关联表,通过证件号关联订单,因为订单表和订单明细表通过用户ID和订单号进行复合分片,所以导致乘车人无法查看本人车票,为什么不能看呢,因为没有分片字段参与sql,会执行全路由,性能较差,所以为了满足这个需求,创建了这个表,方便乘车人可以进行查看自己的车票
那么为什么通过乘车人的证件号进行关联呢?因为用户在添加乘车人进行购票的时候,有可能乘车人是没有注册账号的,但是他肯定有证件号,所以就用证件号来关联自己本人的车票