1. 项目相关 及 项目技术

1.1. 简介

报考岗位为 2025年 信息支援部队某单位-数据分析与管理 岗位。

1.2. 详细内容

1.2.1. 项目开发中熟练使用常见的设计模式

例如 构造器模式、工厂模式、策略模式、命令模式、责任链模式、原型模式等。

原型模式:定义实体的时候,有公用抽象类,实现了clone方法,当一个对象调用clone时会把数据拷贝到新对象一份。

构造器模式:页面数据对象,有的是复杂对象,如果写在一个方法中,方法变得很大,不方便维护,建造构造器对象,将不同操作逻辑拆分到构造器不同子方法中,原方法就会变的很简洁,生成复杂对象时步骤就变为a.getIntence().createUser().createPageInfo();步骤简洁明了。

工厂模式+策略模式:计算优惠券使用即优惠金额时,将不同的优惠券规则封装到不同的策略类里(共同实现一个接口),每个策略类对优惠金额的操作实现不同,例如满减,打折,纯抵扣现金等等。工厂模式是,建造一个工厂类,传入优惠券类别,根据类别返回不同优惠券策略的对象。方法调用时,首先,通过类别获取,优惠券的策略对象,通过接口来接收这个对象,然后调用接口的计算优惠金额方法,就可以了。好处是不用在一个方法中写大量的if else等代码,后期维护时需要通读一遍,维护性非常差。

命令模式:例如下单成功后会有其他操作,比如用户加积分,退款有用户减积分的操作。会把这两个操作封装到一个命令中,两个方法的实现是相反的操作,用于执行命令和撤销命令。使代码有更强的可读性,操作集中在一个命令中,方便维护。

责任链模式:例如审核操作需要调用多个不同类别的操作,如果放到一个方法中,方法变得非常复杂,不易维护,将不同类别操作封装到不同步骤中,然后使用责任链模式将各个步骤串成一个责任链条,每步执行完成后会调用下一步,这样查看逻辑时,看责任链条就清楚业务逻辑了。好处是例如中间想调整执行顺序,或者移除某一步骤,直接调整责任链条即可,不用大量修改代码。

1.2.2. 熟悉Spring Cloud微服务技术栈(Edgware.SR3版本)

阅读过Eureka,Ribbon,Feign,Hystrix,Zuul的源码。

eureka

主要流程是服务集成eureka client,启动后向eureka server进行注册,eureka server会维护一份注册表,服务启动后会从eureka server上拉去注册表,在本地维护一份,eureka项目中也包含eureka client,当配置成集群时,eureka之间相互注册,就会互相同步拉去注册表;

eureka server启动时会将配置信息都维护在一个实例中,然后处理注册相关事情,处理peer节点相关事情,初始化eureka server上下文,从其他节点拉取注册表,注册所有监控组件。eureka将自己作为eureka client时,会读取配置实例化对象获取需要的信息,然后判断是否处理注册和抓取注册表相关操作(不需要的话会释放一些资源),然后创建调度相关线程池,创建心跳相关线程池,创建刷新缓存的线程池,初始化网络通信的组件,判断是否抓取注册表,初始化调度任务定时任务等(心跳和刷新缓存等等操作);server 集群注册同步注册表,任何一个eureka server都是client,会将配置注册的地址封装为一个个peerEurekaNode,然后从里面选择一个拉去注册表,作为本地初始化注册表,然后找到任意一个server进行注册,这个server会将数据同步到集群其他server(其他server不会再同步);集群同步使用三层队列任务异步批处理机制,acceptorQueue(第一个队列,纯写入),后台线程读取队列,将队列数据按照时间打包成processingOrder(按照时间500ms打包成batch),然后将打包的对象放入batchWorkQueue(存放批处理的队列),后台线程发送批处理,相当于一次发送一批数据,节省网络开销。

eureka client启动时加载配置,初始化一个client对象,发送http请求,eureka server会拦截请求,交给ApplicationResource的addInstance,将服务信息放到map结构中,服务:服务实例:Lease信息,通过访问status.jsp可以看到注册信息。client启动过后会抓取全量的注册表,之后每30s会抓取一次增量注册信息与本地进行合并,全量抓取注册表时eureka server使用了多级缓存机制,eureka server查询全量注册表接口多级缓存机制,两个map做两级缓存,一个是readOnlyCacheMap(只读缓存)和readWriteCacheMap(读写缓存),多级缓存过期机制,主动过期(当芙服务下线,注册,故障时会更新readWriteCacheMap)、定时过期(readWriteCacheMap的数据默认等待180s自动过期掉)、被动过期(后台有线程每30s会比较只读缓存和读写缓存,进行信息同步);增量抓取的时候,增量接口会查询有改动的服务,recentlyChangedQueue里面会记录最近变化的服务实例(注册、下线),默认30s会判断一次服务变更记录,默认180s从这个队列中移除,把最近改变的数据与本地的注册表进行增删改之后,计算hash值,判断计算的hash与服务端全量的hash是否一致,如果不一致则全量拉去注册表;client会每隔30s发送一次心跳进行续约,续约操作就是发送请求到server,然后更新一下Lease信息中的lastUpdateTimestamp;服务下线的时候,会调用Lease的cacel方法,将map中服务实例信息清理掉,然后加到最近变化的队列中调整服务下线时间及状态,定时30s会将读写缓存的数据同步到只读缓存;服务故障感知,服务会通过心跳机制来进行续约,server初始化的时候开启了后台线程60s会定时调度一次,调用Lease的isExpired方法判断是否过期(90s2=180s,判断时将90s加了两次,官网文档注释也标注是bug,但是不打算修改了),server不会一次将所有有问题的实例摘除,只会摘除15%,分多次摘除;server自我保护机制,60s会执行一次判断故障服务,思想是判断上一分钟的心跳数量是否比期望的心跳数量小,如果小进入保护机制,不会摘除任何实例,否则正常摘除,期望心跳数量(拉取的数量2*0.85,上线+2,下线-2),上一分钟心跳数量(lastBucket、currentBucket两个变量,每次心跳currentBucket+1,每一分钟将currentBucket赋值给lastBucket,然后将currentBucket清0,上一分钟的心跳数量是lastBucket)。spring cloud 的server使用注解@EnableEurekaServer底层还是走的netflix的逻辑,client使用的是@EnableEurekaClient注解,实例化DiscoveryClient完成注册等逻辑。

eureka.server.responseCacheUpdateIntervalMs = 10000:增量拉取注册表时间
eureka.instance.leaseRenewalIntervalInSeconds = 10:发送心跳时间
eureka.server.evictionIntervalTimerInMs=60:定时判断没发送心跳的任务时间(判断实例故障)
eureka.instance.leaseExpirationDurationInSeconds=90:故障判断超时时间
eureka.server.enableSelfPreservation=false:关闭自我保护机制

ribbon

主要用来做负载均衡的,底层是ILoadBalancer(负载均衡器),IPing(定时30s ping每台服务器判断是否存活),IRule(负载均衡算法,默认RoundRobin轮询),从一堆实例中选择一个来请求。请求流程,ribbon会定时通过IPing来检查服务器是否存活,从注册表中获取服务实例相关信息,通过IRule选择一个地址,调用底层http组件发送网络请求。通过@LoadBalanced注解,触发执行AutoConfiguration相关类,初始化拦截器,会拦截请求,将请求交给RibbonLoadBalancerClient(与eureka集成,可以获取注册表,通过ILoadBalancer负载均衡器<传入ServerListUpdater,启动线程第一次1s执行更新,后面每30s拉去一次注册表>调用IRule选择一个服务实例信息,拼接替换服务名称),然后调用http组件发送http请求。

ribbon:
    ConnectTimeout:1000
    ReadTimeout:1000
    OkToRetryOnAllOperation:true
    MaxAutoRetries:1
    MaxAutoRetriesNextServer:3

ConnectTimeout:连接一台机器超时时间
ReadTimeout:向一台机器发起请求的超时时间
通过设置MaxAutoRetries和MaxAutoRetriesNextServer这两个参数来决定应该如何重试
MaxAutoRetriesNextServer:3,宕机故障的机器会重试3次,然后才会换其他机器重试

feign

集成ribbon,不想硬编码,通过与feign集成,不需要硬编码;请求来了之后,controller接受,然后调用@FeignClient注解的接口,打了Spring mvc的注解,会解析注解,请求的url,拼接成一个带服务名的url,然后与ribbon整合,调用ribbon的ILoadBalancer负载均衡器,拼接服务地址,生成完整的请求url,通过http组件发送请求,然后响应返回结果。

hystrix

资源隔离:让系统里某一块东西,在故障的情况下,不会耗尽系统里所有的资源,比如线程资源

限流:高并发时,涌入大量的请求,可以指定一定的数量的请求进入系统,剩下的一部分被拒绝

熔断:后端的一些依赖出了故障,每次都是报错的,熔断之后,后面的请求不再接受,拒绝访问,后面10分钟再尝试是否可用

降级:例如,mysql挂了,系统发现了,自动降级,从内存中存的少量数据中,提取使用

运维监控:监控+报警+优化,各种异常情况,有问题就及时的报警,优化配置、系统的参数和代码

hystrix_0001

信号量跟线程池资源隔离区别

线程池是用自己的线程去执行调用的;信号量是直接让tomcat的线程调用依赖服务的

线程池隔离技术并不是控制类似tomcat这种web容器的线程,控制tomcat线程的执行(线程池满了之后会fallback,不会夯住,不会卡死,快速返回支持做其他的事情)

信号量是一道关卡,一旦请求数量达到最大的容量之后,就会拒绝其他请求

线程池和信号量隔离技术分别在什么样的场景下使用?

线程池:适合绝大多数的场景,99%的,对依赖服务的网络调用,timeout这种问题

信号量:适合访问的不是外部的依赖访问,对内部的复杂的业务逻辑的访问

hystrix_0002

短路器工作原理

1、如果经过短路器的流量超过了一定的阈值(HystrixCommandProperties.circuitBreakerRequestVolumeThreshold())才会判断要不要短路

2、如果短路器统计到的异常调用占比超过了一定的阈值(HystrixCommandProperties.cricuitBreakerErrorThresholdPercentage()),满足1条件后,其中异常访问(报错、timeout、reject)数量占比达到了设定的值,会开启短路

3、短路器从close状态转为open

4、短路器打开的时候,所有经过该短路器的请求都短路,直接fallback降级

5、经过一段时间(HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()),会half-open(半开),让一个请求经过,看是否可以正常调用,如果调用成功,自动恢复转为close状态

线程池大小:每秒高峰访问次数 99%的访问延时 + buffer = 30 0.2 + 4 = 10 线程

timeout设置,99.5%的访问延时,判断每次访问延时最多250ms(99%的访问延时是200ms),再加一次重试时间50ms,就是300ms

生产环境配置feign+hystrix
hystrix:
    command:
        default:
            execution:
                isolation:
                    strategy: THREAD
                    thread:
                        timeoutInMilliseconds: 1000
                        interruptOnTimeout: true
                semaphore:
                    maxConcurrentRequests: 10
                timeout:
                    enabled: true
            circuitBreaker:
                enable: true
                requestVolumeThreshold: 20
                sleepWindowInMilliseconds: 5000
                errorThresholdPercentage: 50

指定某个服务
hystrix:
    command:
        ServiceAClient:
            execution:
                isolation:
                    strategy: THREAD
                    thread:
                        timeoutInMilliseconds: 1000
                        interruptOnTimeout: true
                semaphore:
                    maxConcurrentRequests: 10
                timeout:
                    enabled: true
            circuitBreaker:
                enable: true
                requestVolumeThreshold: 20
                sleepWindowInMilliseconds: 5000
                errorThresholdPercentage: 50

指定某个服务的某个接口
hystrix:
    command:
        ServiceAClient#sayHello(Long, String, Integer):
        execution:
            isolation:
                thread:
                    timeoutInMilliseconds: 1000
            circuitBreaker:
                requestVolumeThreshold: 4

计算线程池相关参数

高峰每秒请求数量/1000毫秒/(TP99请求延时 + buffer空间)

例如一个请求要50ms,TP99(99%的请求)里耗时最长是50ms

buffer空间 在原基础上增加的 缓冲时间 例如10ms

一个线程每秒处理的请求:1000/(50+10)=16

高峰期每秒请求数量1200,需要的线程数是:1200/16=75

服务B调用服务A线程池数量设置:假如高峰期是每秒1200请求,服务A处理请求时间是(50ms+10ms),要保正正常处理请求,至少需要75个线程,如果部署10个服务,那每个服务的线程池可以设置10个,总共线程数量就是100,大于75,基本没有什么问题

hystrix_0003

zuul(网关)

1、请求路由:屏蔽复杂的后台系统大量的服务,让前端工程师调用的时候非常简单

2、统一处理:把所有后台服务都需要做的一些通用的事情,都放到网关里

1)统一安全认证 2)统一限流 3)统一降级 4)统一异常处理 5)统一请求统计 6)统一超时处理

pre过滤器
-3:ServletDetectionFilter
-2:Servlet30WrapperFilter
-1:FromBodyWrapperFilter
1:DebugFilter
5:PreDecorationFilter:根据路由规则处理url

routing过滤器
10:RibbonRoutingFilter:与hystrix整合
100:SimpleHostRoutingFilter:处理请求头等参数
500:SendForwardFilter:转发请求

post过滤器
900:LocationRewriteFilter
1000:SendResponseFilter

error过滤器
0:SendErrorFilter

zuul_0001

@EnableZuulProxy注解,触发zuul相关的过滤器的执行

这个注解干了两件事:
第一件事,启用了zuul server,可以接收所有的http请求
第二件事,给zuul server加入一些内置的filter、过滤器等

preRoute()执行,如果失败了,会执行error过滤器error(e),然后执行post过滤器

如果pre过滤器执行成功,然后执行route过滤器,如果失败了,会执行error过滤器error(e),然后执行post过滤器

如果route过滤器没报错,就会执行postRoute(),就是去执行post过滤器,如果失败了,会执行error过滤器error(e)

本次请求执行完后直接清理掉RequestContext

1.2.3. spring

spring核心

ioc依赖注入,aop切面编程;

ioc,spring启动的时候会通过xml和注解等把类注入到ioc容器,通过注解标识可以把对应的实例注入到引用的地方,全都交给spring来管理,实现解偶;

aop,相同的代码不用到处写,编写好切面的代码,spring会通过代理的方式增强原来的方法,例如,事务和日志等。

spring mvc

请求到了tomcat后,线程会把请求交给spring mvc,交给DispatcherServlet,DispatcherServlet会根据@Controller注解和@RequestMapping,找到对应的controller和方法,然后执行对应的业务,返回结果

spring 事务传播机制

注解方式一般加到方法上,同时要加rollbackFor属性,指定哪些异常回滚

isolation属性,可以自己手动调整隔离级别,一般不调整,使用默认隔离级别(可重复读)

propagation属性,事务的传播行为,当@Transational的事务方法再次嵌套@Transactional的事务方法,事务怎么弄?

一共有7种事务传播行为:

1)PROPAGATION_REQUIRED:

最常见的一种,两个方法的事务嵌套时,被调用的方法不开启独立的事务,放在一个事务中执行,两个方法中任意地方抛异常,一起回滚。这就是默认的行为,一般都是使用这种。

2)PROPAGATION_SUPPORTS:

如果外层方法开启事务,内部方法自己会加入外层方法中运行;如果没开启事务,内部方法也不开启事务

3)PROPAGATION_MANDATORY:

必须开启事务的方法才能调用自己,否则报错

4)PROPAGATION_REQUIRES_NEW:

内部方法强制开启新的事务,外部事务会卡住等待内部事务执行完再继续。如果外部报错了,不会影响内部方法,内部方法报错,外部可以选择提交或回滚。

5)PROPAGATION_NOT_SUPPORTED:

代表不支持事务,外部方法执行到这个不支持事务的内部方法时,事务挂起,内部方法以非事务方式执行完,外部方法再继续事务执行。(好处是,内部方法报错,不会导致外部方法事务回滚)

6)PROPAGATION_NEVER:

不能被一个事务调用,外部方法开启事务,调用这个情况的内部方法,会报错。

7)PROPAGATION_NESTED:

开启嵌套事务,B的事务嵌套在A的事务中

示例

定义serviceA.methodA()以PROPAGATION_REQUIRED修饰;

定义serviceB.methodB()以表格中三种方式修饰;

methodA中调用methodB

异常状态 PROPAGATION_REQUIRES_NEW
(两个独立事务)
PROPAGATION_NESTED
(B的事务嵌套在A的事务中)
PROPAGATION_REQUIRED
(同一个事务)
methodA抛异常
methodB正常
A回滚,B正常提交 A与B一起回滚 A与B一起回滚
methodA正常
methodB抛异常
1.如果A中捕获B的异常,并没有继续向上抛异常,则B先回滚,A再正常提交;
2.如果A未捕获B的异常,默认则会将B的异常向上抛,则B先回滚,A再回滚
B先回滚,A再正常提交 A与B一起回滚
methodA抛异常
methodB抛异常
B先回滚,A再回滚 A与B一起回滚 A与B一起回滚
methodA正常
methodB正常
B先提交,A再提交 A与B一起提交 A与B一起提交

1.2.4. 分布式锁

阅读过curator和redisson分布式锁的源码

curator

加锁的时候,自己会创建临时节点,不断加锁,就会不断按照顺序累加数字,如果机器宕机的话,这个临时节点就会自动消失,可以保证锁自动释放,顺序节点,创建节点的时候,zk会自动按照顺序给创建的节点标上序号(默认就是公平锁)

curator_001

redisson

在客户端实现了看门狗机制,watchdog,主要监控持有一把锁的客户端是否还活着,如果活着,看门狗会不断的延长这个锁的过期时间,通过lua脚本调用redis底层送hash数据结构,实现各种复杂操作

redisson_001

1.2.5. 高并发架构

如何设计一个高并发系统?

分析

系统架构?怎么部署?部署多少台机器?缓存怎么用的?MQ怎么用的?数据库怎么用的?深挖如何坑下高并发的。

剖析

刚开始,系统都是连接数据库的,数据库支持每秒并发两三千时,基本就快扛不住了。

因为互联网的人越来越多,很多app网站,承受的都是高并发请求,可能每秒并发量几千,很正常。

回答

(1)系统拆分

将一个系统拆分为多个子系统,每个子系统连接一个数据库,多个数据库也可以扛高并发。

(2)缓存

必须用缓存。大部分高并发场景,都是读多写少,完全可以数据库一份,缓存一份,读的时候大量走缓存。毕竟redis轻松几万qps,考虑项目中如何抗高并发。

(3)MQ

必须用MQ。业务会出现频繁修改等操作,调用数据库几十次,redis不适合做数据存储,因为有LRU。 所以还需要mysql来存数据。

用MQ,把大量写请求灌入MQ里,排队慢慢消费写入。控制mysql能承受的范围内。考虑复杂写操作,如何用MQ来异步写,提升并发性能,MQ单机扛几万也是没问题的。(平时没有那么高,高峰超过mysql极限,削峰)

(4)分库分表

可能数据库层面避免不了高并发的请求。将一个数据库拆分为多个数据库,都个数据库扛高并发,然后将一个表拆分为多个表,每个表的数据量保持少一点,提高sql性能

(5)读写分离

就是大部分的时候,读多写少,没必要所有请求都集中在一个库上,主从架构,从库读取,搞读写分离。读流量太多的时候,可以加从库。

(6)Elasticsearch,可以考虑用es。

es是分布式的,可以随便扩容,分布式天然可以支持高并发,一些比较简单的查询、统计类操作,可以考虑用es来承载,还有一些全文检索类的操作,也可以考虑用es来承载

1.2.6. 分库分表

面试题目 1

为什么要分库分表(设计高并发系统的时候,数据库层面该如何设计)?

用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?

你们具体是如何对数据库如何进行垂直拆分或者水平拆分的?

分析

这块扯到高并发了,因为分库分表一定是为了支撑高并发、数据量大两个问题。现在基本互联网公司基本都会问问。

剖析

1、为什么要分库分表?(设计高并发系统的时候,数据库层面该如何设计?)

分库分表是两回事,不要搞混了,可能光分库不分表,可能光分表不分库。

高并发场景下,请求量和数据量的压力,分库分表后有什么好处

1)承受并发增加
2)数据分散,磁盘使用率降低
3)sql执行效率相对也会提高

2、用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?

比较常见的:cobar、TDDL、sharding-jdbc、atlas、mycat

cobar:阿里b2b团队开发和开源的,属于proxy层。最近几年没更新了,基本没什么人用了。不支持读写分离、存储过程、夸库join和分页等操作

TDDL:淘宝团队开发,属于client层方案。不支持join、多表查询等语法,就是基本的crud,支持读写分离。使用的也不多,因为需要依赖diamond配置管理系统

atlas:360开源的,属于proxy层方案,以前有些公司在用,现在比较少了。

sharding-jdbc:当当开源,属于client层方案。之前用的比较多一点,因为SQL语法支持也比较多一点,没有太多的限制,支持分库分表、读写分离、分布式id生产、柔性事务(最大努力送达型事务、TCC事务)。之前使用的公司会比较多一些,目前社区也一直在开发和维护,还算比较活跃。

mycat:基于cobar改造的,属于proxy层方案,支持功能非常完善,目前非常火的而且不断流行的数据库中间件,社区很活跃,一些公司开始用了。比sharding-jdbc来说年轻一些,锤炼少一些。

可以考虑sharding-jdbc或者mycat。

sharding-jdbc这种client层方案优点是不用部署,运维成本低,但是各个项目都要耦合它;

mycat这种proxy层方案缺点在于需要部署,自己运维一套中间件,运维成本高,好处是各个项目是透明的,遇到升级,在部署中间件那里调整就可以。

建议小型公司选用sharding-jdbc,client方案轻便,维护成本低,不需要额外派人手;

大型公司最好选择mycat,proxy层方案,项目多,非常庞大,团队大人员充足,大量项目直接透明使用。

3、具体是如何对数据库进行垂直拆分或水平拆分的?

水平拆分

就是把一个表的数据弄到多个数据库多个表里,每个库的表结构都一样,每个库每个表数据不同,所有库所有表加起来就是全部数据。水平拆分意义,将数据均匀的放更多的库里,然后用多个库来扛更高的并发,还有用多个库的存储容量进行扩容。(按时间拆分;按字段hash拆分<使用较多>)

垂直拆分

就是把一个有很多字段的表拆分为多个表,或者多个库上去。每个库的表结构都不一样,每个表包含部分字段。会将较少的访问频率很高的字段放到一个表里,然后将字段较多的 访问频率低的放到另一张表,数据库是有缓存的,访问频率越高,行字段越少,可以在缓存中存多行,性能越好。表层面做的比较多。

面试题目 2

现在有一个未分库分表的系统,未来要分库分表,如何设计才可以让系统从未分库分表动态切换到分库分表上?

分析

考察怎么迁移线上系统,看看全流程相关经验

剖析

(1)停机迁移

提前发送公告,凌晨停机维护,停服后,写好程序,迁移数据到各个分库分表中,处理好后,上线新系统,启动服务。

(2)不停机双写方案

同时写新库和老库,同时慢慢把老库的数据迁移到新库,都迁移完成后(检测逻辑,判断单库单表和分库分表是否一样),不再往老库里写数据了。

面试题目 3

如何设计动态扩容缩容的饭库分表方案?

分析

设计好的分库分表,迁移都没问题了,将来又达到瓶颈了还得扩容,怎么办?

剖析

(1)停机扩容

跟停机迁移差不多(数据量特别大时,没办法处理)

(2)优化方案

一个实践是利用32*32来分库分表,即32个库,每个库32张表。一共是1024张表。根据某个id取模路由到数据库,再根据id取模路由到表。

刚开始时是逻辑库,建在一个数据库上,一个mysql服务器可能建了n个库,后面如果要拆分了,就是不断的在库和mysql服务器之间做迁移就可以了。然后系统配合修改一下配置即可。比如可以迁移到32个数据库服务器上,每个数据库服务器是一个库,如果还不够,最多可以扩容到1024个数据库服务器,每个数据库一个库一张表(修改路由规则就好)

数据不用改变所在表的位置,只需要加服务器,增加空间和性能即可

路由规则(id%32=库,(id/32)%32=表)
扩容缩容,都是成倍的处理
dba迁移数据库,系统调整数据库配置即可,路由规则不用改变,然后重新上线系统

面试题目 4

分库分表之后,id主键如何处理?

分析

分成多个库,多个表都从1开始累加,肯定不对,需要一个全局唯一的id来支持

剖析

(1)数据库自增id(并发很低,但是数据量很大)

就是系统每次得到一个id,都是往一个库的一个表插上一条没有意义的数据,获取自增id,然后再拿这个id保存到分库分表

(2)uuid

好处是本地生成,不基于数据库来;不好的地方,uuid太长了,作为主键性能差,不适合做主键

适合场景:随机生成文件名,编号之类的

(3)获取系统当前时间

并发很高的时候,比如每秒并发几千,会有重复的情况,不合适,不考虑了

适合场景:一般将当前时间,跟很多其他业务字段拼接起来,作为一个id

(4)snowflake算法

twitter开源的分布式id生成算法,就是把一个64位的long型的id,1个bit是不用的,用其中41个bit作为毫秒数,用10个bit作为工作机器id,12个bit作为序列号

1个bit:因为二进制第一个bit如果是1,代表负数,生成的id都是正数,第一个bit都是0
41个bit:表示时间戳,单位是毫秒,41个bit可以表达2^41-1个数字(毫秒),换算成年就是69年
10bit:工作机器id,代表2^10个数字(1024个机器)。但是10个bit里5个代表机房,5个代表机器id。意思是最多代表2^5(32个机房),每个机房2^5个机器(32台机器)。
12bit:用来记录同一毫秒内产生不同id,12bit可以代表最大正整数是2^12=4096,就是代表一毫秒内4096个不同id

面试题目 5

有没有做mysql读写分离,如何实现mysql的读写分离?

mysql主从复制原理是什么?

如何解决mysql主从同步的延时问题?

分析

高并发肯定要做读写分离的。实际上大部分的互联网公司,一些网站,或者app,都是读多写少。针对这个情况,就是写一个主库,但是主库挂多个从库,然后从多个从库来读,支持高并发

剖析

1、如何实现mysql的读写分离?

基于主从复制架构,搞一个主库,挂多个从库,然后单单写主库,主库会自动把数据同步到从库上。查询走从库

2、Mysql主从复制原理是什么?

主库将变更写binlog日志,然后从库连接到主库之后,从库有一个IO线程,将主库的binlog日志拷贝到自己本地,写入一个中继日志中。接着从库中有一个SQL线程会从中继日志读取binlog,然后执行binlog日志中的内容,就是在自己本地再次执行一遍SQL,这样可以保证自己跟主库的数据一样。

非常重要的一点,从库同步主库数据的过程是串行化的,就是主库上并行的操作,在从库上会串行执行。所以从库从主库拷贝日志及串行化执行SQL的特点,在高并发场景下,从库的数据一定会比主库慢一些,是有延时的。所以经常出现刚写入主库的数据可能读不到,要过几十毫秒甚至几百毫秒才能读到。

还有一个问题,如果主库宕机,恰好数据还没同步到从库,从库上数据还没有,这些数据可能就丢失了

mysql在这个位置有两个机制,一个是半同步机复制机制,用来解决数据丢失问题;一个是并行复制,用来解决主从复制延时。

半同步复制(semi-sync复制):指的就是主库写入binlog日志后,就是将强制化此时立即将数据同步到从库,从库将日志写入到自己本地的relay log之后,接着会返回一个ack给主库,主库接收到至少一个从库的ack之后才认为写操作完成了。

并行复制:指的是从库开启多个线程,并行读取relay log中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。

1)产生主从延迟主要看主库的写并发
2)主库写并发达到1000/s,从库的延时会有几毫秒
3)主库的写并发达到2000/s,从库的延时可能会有几十毫秒
4)主库的写并发达到4000/s,6000/s及以上,主库都快死了,此时延迟会达到几秒

回答

1)主从复制的原理
2)主从延时问题产生的原因
3)主从复制的数据丢失问题,以及半同步复制的原理
4)并行复制的原理,多库并发重放relay日志,缓解主从延迟问题

3、mysql主从同步延时问题

show state;Senconds_Behind_Maste,可以看到从库复制主库的数据落后几毫秒

经常问到,所以考虑好应该在什么场景下来用这个mysql主从同步,使用场景,建议一般读远远多于写,而且读的时效性要求没有那么高的时候;

写了之后保证可以立即查到的场景,采用强制读主库的方式,这样可以保证肯定可以读到数据。一些数据库中间件可以保证。

1)分库,将一个主库拆分为4个主库,每个主库的写并发就500/s,此时主库延迟可以忽略不计
2)打开mysql支持的并行复制,多个库并行复制,意义不太大,因为少部分表并发高,大部分并发很低(可以拆库将并发高的拆出来,再分库等等)
3)重写代码,不要插入再查询,再更新(对应业务情况作调整)
4)如果必须要插入再查询在修改,可以这一块逻辑直连主库(有数据库中间件可以处理,不推荐)

1.2.7. 工作中相关项目整理

项目架构图

project_0001

Famy 项目情况

用户量760万注册用户,高峰并发量超过1000,日活用户10万以上,项目服务情况(服务器是4核4G):Eureka集群(2台机器)、网关服务(3台机器)、语音直播服务(6台机器)、送礼服务(6台机器)、消息服务(6台机器)、热度积分服务(2台机器)、充值订单服务(2台机器)

Famy 项目细节

Future<Map<String, Object>> c1Future = es.submit(() -> getLiveConsumeInfo(c1UserIds, sTime, eTime, startId));

开播时调用直播服务,然后发送mq消息(消息服务,消费消息处理发送消息逻辑,来获取开播信息,然后查询直播间礼物榜单的部分用户<没有的话查询最近聊过天的部分用户>,发送开播消息,如果失败有定时任务,如果失败试几次后就放弃);

rabbitTemplate.convertAndSend(
    RabbitConfig.EXCHANGE_RELIABLE_MESSAGE,
    RabbitConfig.ROUTINGKEY_RELIABLE_MESSAGE,
    message,
    correlationId);
@RabbitListener(queues = RabbitConfig.QUEUE_RELIABLE_MESSAGE)
@RabbitHandler
public void process(String message) {}

关播的时候,调用直播间关播消息,然后发送mq消息,热度积分服务,会统计该场直播礼物等数据,直播时长等等数据,维护直播间热度(开播和热度共用一个数字,前几个bit标识开播,后面记录热度;pk也使用这个数据做参考);

主播很多时,自动下线怎么处理:交给一个服务来做;先计算好之后,分散给多个服务来做;

充值订单服务,购买平台的货币(同事做的);送礼时,送礼服务扣减货币,购买礼物,将礼物赠送给主播,调用消息服务(分布式事务),保存消息记录,发送聊天室自定义消息

系统整体架构,项目架构(eureka集群、网关、直播服务、送礼服务、消息服务、热度积分服务、支付订单服务)

数据库设计(每个类别的服务对应一个数据库,直播、送礼,目前部署在一台数据库服务器上)

涉及到的相关数据表(直播相关:主播表,直播间表,直播记录表,用户访问直播间记录表,用户关注表,礼物表,购买礼物记录表,送礼记录表,平台货币表,平台货币记录表)

统计观看用户相关表的设计,进入离开表的相关设计,点赞关注相关表设计?(包括表相关的数据量等等,开播日志每天200主播开播5次700天,不到1000万开播记录;每天50万进入直播间的记录,700天3亿5千万条记录<因为用户不关心,工资都是月结,保留最近3个月的数据>;收礼记录,每天平均一个主播100个,200主播700天,1400万条记录)

年轻代老年代比例(4C8G,6台每台请求大概80左右,年轻代3G,老年代1G,其他占用1~2G)

开发分布式系统间调用,标记请求链路的相关功能,使用ThreadLocal开发公用组件

分布式系统调用过程中,为了标记各个服务之间的请求关系,通过拦截器,aop等拦截请求,获取唯一标识,设置数据到请求线程的ThreadLocal中,该请求在当前服务中任何位置都可以获取到这个标识

主播pk匹配功能开发,使用线程池变量高低位设计的思想及redis的zset结构

主播开关播及送礼过程中会维护主播状态及热度,高3位标记开播状态,后29位标记主播的热度,pk时会将开播的热度相近的主播组队进行pk,尽量保证主播pk的公平性

检测主播异常下线情况,采用eureka的心跳思想,关闭异常下线主播

当主播直播时,每隔一段时间会发送心跳,当超过很长一段时间没有发送心跳,判定为主播异常下线,自动关闭直播

开播关播需要调用其他服务,使用最大努力通知方案和可靠消息最终一致性方案保证数据一致性准确性

开播的时候需要通知部分好友粉丝,采用的是最大努力通知方案

关播的时候需要调用热度积分服务根据直播情况来计算热度等数据,采用了可靠消息最终一致性方案,保证数据准确性

使用平台货币购买礼物,送礼流程,使用Redisson分布式锁保证数据准确性

购买礼物和送礼是核心的流程,肯定要保证数据准确,因为服务会部署多台机器,扣减平台货币时需要分布式锁来解决,经过调研和项目情况的评估,采用Redisson分布式锁

觅兔 项目情况

接近5万注册用户,日活用户近1万,高峰并发接近100,项目服务情况(服务器是2核4G):Eureka集群(2台机器)、网关服务(2台机器)、用户服务(2台机器)、数据业务服务(4台机器)、私聊群聊消息服务(2台机器)

觅兔 项目细节

数据业务服务,下单时生成预订单,扣减/锁定库存,扣减/锁定优惠券等(一段时间后没有支付,解锁锁定的库存),支付成功后回掉;调用数据业务服务,写入mq,消费mq消息,真正扣件库存,消耗优惠券等,维护用户积分相关操作;发送进入群聊消息,群聊相关服务消费消息,创建群聊,加入群聊,发送消息等操作。(可靠消息最终一致性)

支付订单服务、优惠券团购票服务、私聊群聊消息服务、用户服务:

支持下单、进群聊、发送消息整套流程顺利执行,方便用户之间可以进行交流,并可以提前熟悉彼此,或者游戏后有更多交流,提升游戏体验

用户下单购买团购票或者使用优惠券、团购票购买场次后,需要加入对应的群聊,并且记录用户积分,发送消息

使用策略模式、工厂模式、命令模式使代码结构更清晰,方便维护

使用可靠消息最终一致性方案,保证用户加入群聊,接收到对应的消息

results matching ""

    No results matching ""