12306自己实现的优化点

项目在后端高并发的优化:1.微服务-拆分 (相当于加了机器处理请求,物理上提升效率,但是要考虑微服务之间通信等问题) 2.负载均衡(与微服务拆分一起使用,将请求按负载均衡策略进行转发) 3.限流降级 4.缓存 5.令牌 6.消息队列+异步处理。将一个购票流程进行拆分,不用等到完全处理完才返回给客户,这样可以减少客户等待时间,并且异步处理可以是另一台服务器。

数据库方面:分库,将不同业务的表放在不同的数据库里面,相互隔离

分表(项目中没体现):横向分表按时间或地区分表,比如一月分一张表,二月分一张表。

纵向分表是值把一张完整的表竖着切一刀变成两张表,按主键id一一对应,比如文章,把文章标题作者时间分到一张表,把文章内容分到另一张表,这两张表用一个文章id一一对应。

冗余设计,反范式。空间换时间,项目中有体现,在一张表里面加一些冗余字段,比如在座位表里面存放

注意:项目中用的Feign微服务通信技术是通过基于http实现的RPC协议完成的。

分时段秒杀。将票分时段发票。

1.对于选座的优化

优化前:如果一个用户帮多个乘客选座,每个乘客选座的座位都要遍历一遍座位表才能确保选座成功。

优化后:因为我们选座系统是只给两排信息,用户只能选择两排座位信息的相对关系,所以只要找到第一个座位满足的位置,其他座位可以根据第一个符合条件的座位,加上偏移量来得到,减少了循环的次数。这需要为每个座位加上一个index编号。

座位表里面有一个sell字段,以二进制的形式存储该座位在不同车站区间的售卖信息,这里就有一个问题,用户进行余票查询,如果通过遍历座位表动态计算的话是很影响性能的,所以要额外增加一张余票表,当用户买完票后,计算一下还有多少余票,这个功能是比较复杂的,比如有ABCDE五个站,买了A-B,A-C,A-D,A-E这些区间余票都会受影响。

关于购票业务功能说明:

因为用户查询余票的时候是通过站站查询的,填入一个出发站和到达站,查询是否有余票,所以我们数据库也是这么设计的,比如有ABCDE五个站,他对应的站站余票字段应该有10个,A-B,A-C,A-D等等,如果一个用户购买了A-B区间的票,我们应该把所有包含这个区间的站站余票都减一,但是我们又不能简单的就这么减一,还需要设计一个区间算法,举个例子,有10个座位,一个用户选择了A-B的区间,那么所有包含A-B区间的车票都减一变成了9,另一个用户选择了C-D,这时候就出现问题了,我们不能将所有包含C-D的车票都减一,如果C-D选择的是A-B用户选择的座位的话,B-D区间是不用减一的,而选择的不是的话,就要减一,所以单纯减去包含区间会导致少卖问题。

选座购票业务逻辑:1.前端选好座之后会输入验证码削峰(验证码存redis),验证码通过后需要抢令牌锁,为了防止机器人刷票,令牌锁key设计是date+trainCode+memeberID,设置5秒过期时间,不手动删除,如果锁存在则直接返回,不存在进行下一步。如果抢到了令牌就继续往下走,抢不到直接返回前端提示当前抢票人数过多请5秒后再尝试,然后去redis获得令牌余数,然后判断余数--,如果剩余的令牌数充足,就继续执行,否则返回抢票人数过多请5秒后再尝试,如果令牌充足,则重新刷新过期时间(60s),并判断当前剩余令牌数m%5==0,如果成立更新数据库令牌数。然后创建订单信息,将订单信息状态置为INIT,然后将订单信息记录一下,同步过程就结束了,到这里请求就可以返回前端了,后续的操作可以异步完成。(用消息队列rocketMQ),然后进行出票,出票的时候需要用到分布式锁(redis实现),否则高并发场景下可能会出现超卖,分布式锁的粒度越小越好,我们的key设计是自定义前缀(如ticketLock)+date+taincode,这样设计只有买同一个日期并且同一列车的才会竞争。抢到锁的人负责这整个列车的订单的售卖操作,它会每次从订单表取init状态的本列车的订单,然后去尝试分配座位(可能分配失败,因为余票不足等原因),如果分配失败则将订单状态改为EMPTY,在尝试分配的时候将订单状态改为pending,避免订单重复处理。

购票细节:选座,业务上选座是给出两排位置供用户选择,用户可以为多个乘客选座,我们记录每个乘客的相对位置,以第一个乘客的位置为基准,我们遍历第一个乘客的位置,在满足第一个乘客位置的基础上,去看看其他乘客的位置是否满足(这个过程可以通过相对位置来得到其他乘客的位置),当所有乘客的位置都满足的时候,就把余票树扣除,更新座位sell信息,座位sell信息是二进制存储的,我们把当前乘客乘坐的区间的子串截出来,0都变成1,然后填充前缀0和后缀0,与原来的二进制数进行|运算。就得到售卖后的座位信息。最后更新订单状态为success,为会员模块添加一个购票记录就结束了。

做法:我们在用户购票完,去更新余票信息,如何更新呢?策略是这样的:我们只更新未被买过的且与当前购买的区间有交集的区间,举个例子1000001是这个座位之前的购买信息,现在要买的区间是0001110那受影响的就是1000001,黄色部分,我们维护minStart,maxStart,minEnd,maxEnd,四个变量,minStart取1后面的第一个0,Maxsart取endIndex-1,MinEnd取startIndex+1,MaxEnd取遇到1的前一个0。把所有满足这四个变量的区间车间都减一。

数据库ID都采用雪花算法生成

为什么要用雪花算法?

常见的ID生成方式还有自增ID,然而自增ID不适合于分布式项目,因为我们数据库不只有一个,自增的话会出现ID重复的情况。另一种是UUID,然而UUID会影响索引效率,UUID是无序的,在插入索引树的时候,树的结构会改变比较大,影响性能,我们希望的ID是增长的,而不是忽大忽小的,这样就可以尽可能的减小树的结构的改变,所以我们选用雪花算法。

雪花算法生成的id是64位的,最高位是符号位,然后接下来的41位的时间戳,这个可以解决ID都是增长的,机器ID可以分为数据中心和机器号,比如5位拿来当数据中心(比如上海,北京编个号),5位拿来当机器号。最后的序列号是自增的,也就是说同一毫秒内同一台机器有多个并发请求,他就会以自增的方式进行生成

机器ID,数据中心编号怎么设?服务启动的时候去Redis里面拿。

2.对于余票查询使用redis缓存优化

余票查询是用户最常用的功能,所以需要对余票信息进行缓存,提高效率。

可见余票信息是一个热点数据,当缓存过期大量请求访问数据库,可能会导致数据库无法承受访问压力导致崩溃。也就是缓存击穿。

项目采用的解决方案:1.用定时任务主动刷新缓存,重置过期时间。

                                    2.第一种方案可以保证在redis正常的情况下,余票缓存永不过期,但是redis突然宕机了或者因为其他原因,缓存没了,这时候就要有一个备用方案,当访问数据库的时候,加一个分布式锁,每次只允许一个请求访问数据库,访问完数据库更新缓存后,再释放锁。只有抢到锁的才能访问,其他没有抢到锁的请求会返回客户请求失败的信息,不会阻塞等待卡在服务端,因为查询余票请求量很高,把请求信息都放在服务端进行阻塞等待会消耗服务端资源。

解决缓存穿透问题:

客户发起的请求既不在缓存也不存在数据库,比如客户搜索的车票信息,起始地址和终点地址是不存在车次的,如果大量发起这种请求,就会造成缓存穿透。

项目解决方案:首先访问数据库的时候还是要加分布式锁,同一时刻只允许一个请求访问。第二点在查询到数据库没有该数据的时候,返回一个空列表缓存在redis里面,下一次访问的时候发现缓存里是空列表而不是null,说明数据库也没数据直接返回,注意redis是null的话有可能是过期了,还是要到数据库查,如果是空列表说明数据库没数据。

缓存雪崩:

有可能多个车站的票同一时间都过期了。

解决方案:1.首先给每个车站余票加上一个随机时间,将过期时间打散

                  2.访问数据库时加分布式锁

                    3.定时任务主动刷新缓存。

使用分布式事务seata解决不同数据库事务的一致性

TM:每个应用都可以发起分布式事务(busniess,member中开启分布式事务使用注解@grobalTransaction)

RM:管理数据库资源

业务场景:在进行下单的时候,需要在busniess模块中调用member模块,两个模块用的是不同数据库,用户下单完要修改余票信息,座位信息,订单信息。member模块要添加购票记录。

项目解决方案:使用seata默认的at模式,添加一个undo-log表,不用写补偿方法,在某个库执行失败的时候,根据undo-log里的内容,进行相反的操作,来保证事务的一致性。注意细节:当调用member模块出现异常的时候,需要在全局异常处直接返回异常,而不应该进行处理,因为seata调用是判断返回码进行判断的(如果全局异常处理了会返回200,则回滚不了了)。

使用redis分布式锁解决高并发场景下火车票座位超卖的情况

key设计:自定义前缀+使用日期+车次作为key。原因:不同日期和车次的火车票互不影响,为了提高吞吐量,减少锁的粒度,因此这样设计。

使用redisson引入看门狗机制处理过期时间不好设定问题,使用看门狗时,只要获得锁的线程没结束,会有一个守护线程不断的刷新锁的过期时间,防止一个占有锁的线程因为key过期导致超卖。还在finally中引入只有当前占有锁的线程才能手动删除锁,其他线程无法删除锁,解决没抢到锁的线程也能删除锁的问题。

红锁机制(项目没体现,主要解决redis宕机问题):当redis宕机后,数据就无法访问分布式锁了,这时候就可以用集群,主从来解决,但是这又会引出一个问题,当redis主从切换,如果没有数据备份,很有可能丢失分布式锁信息,在宕机前一个线程获得锁,宕机后,导致另一个线程也成功获得锁,这就有可能引起超卖问题,所以要引入红锁,红锁的理论:比如准备多台redis服务器

A,B,C,D,E,只有获得一半以上的锁才算成功获得锁。

使用令牌锁(redis分布式锁)解决机器人刷票问题

key设计:使用日期+车次+memberId作为key。

只有抢到锁的才能进行购票,并且不手动删除锁,给锁设定一个过期时间,比如5,如果该乘客没有购票成功,则只能5秒后再进行购买(因为下一次请求会因为令牌锁未释放导致不成功)。

使用redis缓存令牌数量

首先先在redis判断一下是否有令牌数量缓存,命中的话直接在redis里将数量减去,刷新key的过期时间返回,每隔5次(count%5==0)更新数据库,如果缓存未命中就查库,查库也不用立即更新数据库,将缓存加到redis就行。这里可能会出现redis的脏数据未及时更新到数据库就过期了的情况,但是没关系,令牌只是入场券,我们的目的是防止机器人刷票,降低请求量。

前端使用验证码降低请求数防止机器人刷票

后端生成验证码,结合前端传过来的唯一token,这个唯一token作为key存到redis,生成对应图片返回给前端,前端输入后,后端进行验证码比对。

使用sentinel限流

sentinel有个控制台可以配置规则,可以新建限流规则,但是它没有降级处理,如果需要自定义降级处理可以在限流规则上面用@SentinelResource注解,然后自定义降级方法,降级方法参数和返回值要和限流方法一样,限流方法多一个BlockException参数。

注意控制台制定的规则是推送到客户端(我们的cloud项目比如12306)内存中保存的,所以程序重启规则会消失,所以我们希望规则持久化,这时候就用到了nacos,将规则记录到nacos里面,就能保证规则持久化。

使用rocketMQ进行异步处理

因为购票流程比较长,如果整个过程都是同步进行,用户等待时间可能会比较久不利于用户体验。此时用异步的方法,将购票流程进行拆分,验证码和拿令牌是同步的,然后实际购票操作是异步的,用户前端会不断轮训的询问出票结果(可以间隔1秒询问一次),会增加一定的后端服务压力,所以可以将购票流程抽离出一个模块,分散请求压力,用MQ进行消息通信。

细节:抢完令牌锁,返回给前端一个正在抢票中状态,这个状态更新操作可以有两种做法:1.前端定时器轮询请求订单状态,如果为success则显示抢票成功。2.后面经过学习查到有更好的方案,前面那种方案会有无效请求和消息不及时问题,可以用回调,长连接的方式,使用webSocket,在后端用一个map维护用户连接信息,userid为key,websocketSession为value,等到后台消费者处理完购票请求的时候,通过这个map找到连接信息,主动回调。

二.只用一个leader会不会太慢了?是的,可以优化,我们可以再细粒化范围,比如一个车厢一个leader,这样也不会出现重复消费的问题,(其实这是经典的多个消费者重复消费的问题)(一个车厢一个leader解决方案不太合适,因为用户购票时不会选择车厢)

令牌数量是由历史业务数据计算得来的,令牌的作用是防止机器人刷票,限流操作我们交给了sentinel来实现了。

如何实现一人一单?

一张有效身份证件同一乘车日期同一车次只能购买一张车票

所以在下订单的时候,需要判断一下订单表有没有在同一乘车日期同一车次的订单信息,有的话,直接返回失败。

系统吞吐量实验

在未优化前(没使用redis缓存,mq排队异步处理购票操作)系统的吞吐量是17左右。

在优化后,系统的吞吐量达到了400左右,大约提升了25倍。

系统并发量实验

在未优化前(没使用redis缓存,mq排队异步处理购票操作)系统的并发量大概是不到50。标准是无异常,响应时间2s左右。

在优化后,系统的并发量达到了1000左右。也提高了25倍左右。

综上系统优化后性能提升25倍左右。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值