文章目录
- Spring
- 第一问:BeanFactory和ApplicationContext的对比
- 第二问:bean的生命周期
- 第三问:BeanPostProcessor
- 第四问:BeanFactoryProcessor
- 第五问:Aware接口和IntializingBean接口
- 第六问:初始化销毁顺序
- 第七问:scope类
- 第八问:aop实现
- 第九问:RequestMappingHandlerMapping 与 RequestMappingHandlerAdapter
- 第十问:mvc 处理流程【重点】
- 第十一问:循环依赖
- 第十二问:Spring 事务失效
- 第十三问:SpringBoot 自动配置原理
- 第十四问:SpringBoot 设计模式
- 第十五问:Spring Refresh流程
- SpringCloud生态
- MyBatis
- Ribbon的工作原理
- 讲一讲springcloud的5大组件,工作流程?
- Eureka
- Eureka和zookeeper作为注册中心的区别?
- NACOS
- open feign远程调用
- Gateway
- Nginx
- Redis
- Redis基础知识
- Redis为什么这么快?
- 数据库一致性问题
- 什么是Redis大key
- MyBatis-Plus的使用
- XXL-job
- 商城相关业务方案
- 下单+支付链路
- 登录
- 发短信验证码
- 秒杀
- MQ
- Kafka、ActiveMQ、RabbitMQ、RocketMQ对比
- 消息丢失、积压、重复等问题
- 线上mq消息如何保证99.99999%不丢失?
- RabbitMQ实现延时队列的需求
- 顺序消费
- ElasticSearch性能优化方案
Spring
第一问:BeanFactory和ApplicationContext的对比
BeanFactory是Spring的核心接口,主要实现ApplicationContext组合了它的功能,BeanFactory表面上只有getBean接口,实际上控制反转、依赖注入、直至Bean的生命周期的各种功能,都由它的实现类提供
常见的ApplicationContext实现:
- ClassPathXmlApplicationXml
- FileSystemXmlApplicationContext
- AnnotationConfigApplicationContext
- AnnotationConfigWebServletContext
第二问:bean的生命周期
构造器->依赖注入->初始化->销毁(不完整)
- 处理名称,检查缓存:处理别名,检查一级、二级、三级缓存
- 处理父子容器
- 处理 dependsOn
- 选择 scope 策略
- 创建 bean
5.1 创建bean实例
5.2 依赖注入
5.3 初始化
5.4 注册可销毁bean
- 类型转换处理
- 销毁 bean
第三问:BeanPostProcessor
BeanPostProcessor模板设计模式的应用:BeanFactory的基本流程已确定,通过扩展BeanPostProcessor扩展功能
常见的BeanPostProcessor:
- AutowiredAnnotationBeanPostProcessor 解析 @Autowired 与 @Value
- CommonAnnotationBeanPostProcessor 解析 @Resource、@PostConstruct、@PreDestroy
- ConfigurationPropertiesBindingPostProcessor 解析 @ConfigurationProperties
另外
ContextAnnotationAutowireCandidateResolver 负责获取 @Value 的值,解析 @Qualifier、泛型、@Lazy 等
第四问:BeanFactoryProcessor
- ConfigurationClassPostProcessor 可以解析
- MapperScannerConfigurer 可以解析Mapper 接口
自定义BeanFactoryProcessor:实现BeanDefinitionRegistryPostProcessor
第五问:Aware接口和IntializingBean接口
Aware 接口提供了一种【内置】 的注入手段,InitializingBean 接口提供了一种【内置】的初始化手段。
@Autowired和@PostContrust需要使用Bean后处理器,Aware接口、InitializingBean接口属于内置功能。
@Autowired等注解失效注入了BeanFactoryPostProcessor会导致要先创建Java配置类,此时BeanPostProcessor还未准备好,因此@Autowired等注解失效
第六问:初始化销毁顺序
对于init, 三个初始化方法的执行顺序:@PostConstruct -> InitializingBean接口 -> @Bean的initMethod
对于destory, 三个销毁方法的执行顺序是:@PreDestroy -> DisposableBean接口 -> @Bean的destroy
第七问:scope类
- singleton单例
- prototype多例
- request web请求
- session web会话
- applicaion:web的ServletContext
scope失效问题:单例scope注入其他scope实例失效
解决方法推迟其它scope bean的获取
- @Lazy、添加@Scope属性
- proxyMode=ScopedProxyMode.TARGET_CLASS
- 使用ObjectFactory注入
- 使用applicationContext获取Bean
第八问:aop实现
1、aop之ajc增强这是一种编译时的代码增强
2、aop之agent增强这是一种类加载时的代码增强
3、jdk proxy方法
Target target = new Target();
Foo proxy = (Foo)Proxy.newProxyInstance(JdkProxyDemo.class.getClassLoader(), new Class[]{Foo.class}, (p, method, args1) -> {
System.out.println("...before...");
// 目标.方法(参数)
// 方法.invoke(目标,参数)
Object result = method.invoke(target, args1);
System.out.println("...after...");
return result;
});
代理一点都不难,无非就是利用了多态、反射的知识
- 方法重写可以增强逻辑,只不过这【增强逻辑】千变万化,不能写死在代理内部
- 通过接口回调将【增强逻辑】置于代理类之外
- 配合接口方法反射(是多态调用),就可以再联动调用目标方法
- 限制⛔:代理增强是借助多态来实现,因此成员变量、静态方法、final 方法均不能通过代理实现
4、cglib proxy方法
Target target = new Target();
Target proxy = (Target) Enhancer.create(Target.class, (MethodInterceptor) (p, method, args, methodProxy) -> {
System.out.println("before...");
// Object result = method.invoke(target, args); // 用方法反射调用目标
// methodProxy 它可以避免反射调用
// Object result = methodProxy.invoke(target, args); // 内部没有用反射, 需要目标 (spring)
Object result = methodProxy.invokeSuper(p, args); // 内部没有用反射, 需要代理
System.out.println("after...");
return result;
});
代理的目前是方法对象的增强,对于传统的JDK代理,只能实现带接口的对象,然后通过newPrxoxyInstance实现需要增强的方法,底层原理就是多态和反射。而cjlib代理则是用到了methodPrxoy,代理对象内部初始化methodProxy,通methodProxy无需可以代理任何,并且可不需要使用反射和目标对象。
第九问:RequestMappingHandlerMapping 与 RequestMappingHandlerAdapter
RequestMappingHandlerMapping处理 @RequestMapping 映射;RequestMappingHandlerAdapter调用控制器方法、并处理方法参数与方法返回值。
1、DispatcherServlet 初始化
- DispatcherServlet 是在第一次被访问时执行初始化, 也可以通过配置修改为 Tomcat 启动后就初始化
- 在初始化时会从 Spring 容器中找一些 Web 需要的组件, 如 HandlerMapping、HandlerAdapter 等,并逐一调用它们的初始化
- RequestMappingHandlerMapping 初始化时,会收集所有 @RequestMapping 映射信息,封装为 Map,其中
- key 是 RequestMappingInfo 类型,包括请求路径、请求方法等信息
- value 是 HandlerMethod 类型,包括控制器方法对象、控制器对象
- 有了这个 Map,就可以在请求到达时,快速完成映射,找到 HandlerMethod 并与匹配的拦截器一起返回给 DispatcherServlet
- RequestMappingHandlerAdapter 初始化时,会准备 HandlerMethod 调用时需要的各个组件,如:
- HandlerMethodArgumentResolver 解析控制器方法参数
- HandlerMethodReturnValueHandler 处理控制器方法返回值
2、自定义参数与返回值处理器
第十问:mvc 处理流程【重点】
当浏览器发送一个请求 http://localhost:8080/hello
后,请求到达服务器,其处理流程是:
- 服务器提供了 DispatcherServlet,它使用的是标准 Servlet 技术
- 路径:默认映射路径为
/
,即会匹配到所有请求 URL,可作为请求的统一入口,也被称之为前控制器
- jsp 不会匹配到 DispatcherServlet
- 其它有路径的 Servlet 匹配优先级也高于 DispatcherServlet
- 创建:在 Boot 中,由 DispatcherServletAutoConfiguration 这个自动配置类提供 DispatcherServlet 的 bean
- 初始化:DispatcherServlet 初始化时会优先到容器里寻找各种组件,作为它的成员变量
- HandlerMapping,初始化时记录映射关系
- HandlerAdapter,初始化时准备参数解析器、返回值处理器、消息转换器
- HandlerExceptionResolver,初始化时准备参数解析器、返回值处理器、消息转换器
- ViewResolver
- DispatcherServlet 会利用 RequestMappingHandlerMapping 查找控制器方法
- 例如根据 /hello 路径找到 @RequestMapping(“/hello”) 对应的控制器方法
- 控制器方法会被封装为 HandlerMethod 对象,并结合匹配到的拦截器一起返回给 DispatcherServlet
- HandlerMethod 和拦截器合在一起称为 HandlerExecutionChain(调用链)对象
- DispatcherServlet 接下来会:
- 调用拦截器的 preHandle 方法
- RequestMappingHandlerAdapter 调用 handle 方法,准备数据绑定工厂、模型工厂、ModelAndViewContainer、将 HandlerMethod 完善为 ServletInvocableHandlerMethod
- @ControllerAdvice 全局增强点1️⃣:补充模型数据
- @ControllerAdvice 全局增强点2️⃣:补充自定义类型转换器
- 使用 HandlerMethodArgumentResolver 准备参数
- @ControllerAdvice 全局增强点3️⃣:RequestBody 增强
- 调用 ServletInvocableHandlerMethod
- 使用 HandlerMethodReturnValueHandler 处理返回值
- @ControllerAdvice 全局增强点4️⃣:ResponseBody 增强
- 根据 ModelAndViewContainer 获取 ModelAndView
- 如果返回的 ModelAndView 为 null,不走第 4 步视图解析及渲染流程
- 例如,有的返回值处理器调用了 HttpMessageConverter 来将结果转换为 JSON,这时 ModelAndView 就为 null
- 如果返回的 ModelAndView 不为 null,会在第 4 步走视图解析及渲染流程
- 调用拦截器的 postHandle 方法
- 处理异常或视图渲染
- 如果 1~3 出现异常,走 ExceptionHandlerExceptionResolver 处理异常流程
- @ControllerAdvice 全局增强点5️⃣:@ExceptionHandler 异常处理
- 正常,走视图解析及渲染流程
- 调用拦截器的 afterCompletion 方法
精简版
- 客户端发送请求至前端控制器
DispatcherServlet
接收请求 -
DispatcherServlet
收到请求并调用HandlerMapping
处理映射器 -
HandlerMapping
通过系统或者自定义的映射器配置找到对应的处理器handler
,生成HandlerExecutionChain
{handler
(处理器对象)、HandlerInterceptor
(处理拦截器)}返回给DispatcherServlet
。 -
DispatcherServlet
调用HandlerAdapter
处理适配器 -
HandlerAdapter
经过适配调用具体的处理器(Controller
,也叫后端控制器),Controller
执行完成后返回ModelAndView
,HandlerAdapter
将执行结果ModelAndView
返回给DispatcherServlet
。 -
DispatcherServlet
将ModelAndView
传给视图解析器ViewResolver
,ViewResolver
解析后返回具体的View
,DispatcherServlet
根据View
进行渲染视图,DIspatcherServlet
响应客户端
第十一问:循环依赖
一级缓存作用:限制bean在beanFactory中只存一份,即实现sinleton scope,无法解决循环依赖(singletonObjects)
二级缓存作用:a执行依赖注入前,需要将被依赖注入的半成品对象b注入,执行依赖注入的从singletonFactories取出半成品b,b的流程顺利走完后,将b的成品放入到singletonObject一级缓存,返回到a的依赖注入流程(singletonFactories)
三级缓存作用:只有发生循环依赖时,从singletonFactories拿到工厂对象FacoryBean,注入代理对象,同时将代理对象放入三级缓存earlySingletonObject。(earlySingletonObject)
总结:
单例 set方法(包括成员变量)循环依赖,Spring会利用三级缓存解决,无需额外配置
- 一级缓存存放成品对象
- 二级缓存存放发生了循环依赖时的产品对象(可能是原始bean,也可能是代码bean)
- 三级缓存存放工厂对象,发生循环依赖时,会调用工厂获取产品
- Spring期望在初始化时创建代理,但如果发生了循环依赖,会由工厂提前创建代理,后续初始化时就不重复创建代理
- 二级缓存的意义在于,如果提前创建了代理对象,在最后的阶段需要从二级缓存中获取此代理对象,作为最终结果
构造方法及多例循环依赖解决方法
- @Lazy
- @Scope
- ObjectFactory & ObjectProvider
- Provider
第十二问:Spring 事务失效
- 抛出检查导致事务不能正确回滚
- 业务方法内自己try-catch异常导致事务不能正确回滚
- aop 切面顺序导致导致事务不能正确回滚
- 非public方法导致的事务失效
- 父子容器导致的事务失效
- 调用本类方法导致传播行为失效
- @Transactional没有保证原子行为
- @Transational方法导致的synchrionized失效
第十三问:SpringBoot 自动配置原理
@SpringBootConfiguration是一个组合注解,由@ComponetScan、@EnableAutoConfiguration和@SpringBootConfiguration组成
- @SpringBootConfiguration 与普通 @Configuration 相比,唯一区别是前者要求整个 app 中只出现一次
- @ComponentScan
excludeFilters - 用来在组件扫描时进行排除,也会排除自动配置类 - @EnableAutoConfiguration 也是一个组合注解,由下面注解组成
- @AutoConfigurationPackage – 用来记住扫描的起始包
- @Import(AutoConfigurationImportSelector.class) 用来加载 META-INF/spring.factories 中的自动配置类
第十四问:SpringBoot 设计模式
Singleton:spring bean scope
Builder:BeanDefinitionBuilder
Adapter:HandlerMappingAdapter
Composite:HandlerMethodArgumentResolverComposite、HandlerMethodReturnValueHandlerComposite
Observer:ApplicationListener、ApplicationEvent
Chain of Responsibility:HandlerInterceptor
第十五问:Spring Refresh流程
- prepareRefresh:做好准备工作
- obtainFreshBeanFactory:创建或获取BeanFactory
- prepareBeanFactory:准备BeanFactory
- postProcessBeanFactory:子类扩展BeanFactory
- invokeBeanFactoryPostProcessors:后处理器扩展 BeanFactory
- registerBeanPostProcessors:准备Bean后处理器
- initMessageSource:为ApplicationContext 提供国际化功能
- initApplicationEventMulticaster:为ApplicationContext 提供事件发布器
- onRefresh:留给子类扩展
- registerListeners:为ApplicationContext准备监听器
- finishBeanFactoryInitialization:初始化单例Bean,执行Bean后处理器扩展
- finishRefresh:准备生命周期管理器,发布ContextRefreshed 事件
SpringCloud生态
Eureka
OpenFeign
Hystrix
Gateway
Nacos
Sentinel
Seata
xxl-job
MyBatis
mybatis分页插件实现原理:实现一个拦截器结合反射改写SQL
mybatis拦截器的应用:通用属性的注入
不修改对象null、空字符串类型的SQL:mybatis-plus可配置策略:对 not_null,not_empty,ignored
动态SQL:if
、where
、trim
、choose、when、otherwise
、foreach
mybatis设置一级缓存和二级缓存的意义
xml配置的bean优先级比注解扫描的bean优先级高
Ribbon的工作原理
集中式LB:即在服务的消费方和提供方之间使用的独立的LB
设施,如F5、Nginx
进程内LB:将LB
逻辑继承到消费方,消费者从服务注册中心获知哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。Ribbon
就属于进程内LB
Ribbon
客户端组件提供一 系列完善的配置项如连接超时重试等
Ribbon
在工作时分为两步:
- 选择
EurekaServer
,它优先选择在同一个区域内负载较少的Server
- 根据用户指定的策略,在从
Server
取到的服务注册列表中选择一个地址
其中Ribbon
提供多种策略:比如轮询、随机、和根据响应时间加权。
拦截器拦截@LoadBalance
注解,根据给定算法从实例列表选择对应的实例,
Ribbon
中的IPing 会有一个定时任务,每隔30秒执行一下pingTask 任务,把server list 里的服务都ping 一遍,然后通过那个实例调用。
负载均衡是如何判断调用哪个服务的?
访问次数(AtomicInteger) % 实例数 (CAS)
讲一讲springcloud的5大组件,工作流程?
Eureka:注册中心,里面有一个注册表,保存了各个服务所在的机器和端口号等注册信息
Ribbon:客户端代理,服务间发起请求的时候,基于Ribbon服务做到负载均衡,从一个服务的对台机器中选择一台
Feign:基于fegin的动态代理机制,根据注解和选择机器,拼接Url地址,发起请求
Hystrix:
- 服务降级:服务器忙,请稍后再试,不让客户端等待并立刻返回一个友好提示
fallback
;以下情况会发生服务降级:程序运行异常、超时、服务熔断出发服务降级、线程池/信号量已满 - 服务熔断:类似保险丝,达到一些限制条件时,直接跳闸不允许请求
- 实时监控
Zuul(Netflix)/Gateway(SpringCloud):如果前端后端移动端调用后台系统,统一走zull网关进入,有zull网关转发请求给对应的服务
- 反向代理
- 鉴权
- 流量控制
- 熔断
- 日志监控
Eureka
Eureka和zookeeper作为注册中心的区别?
Eureka(AP)
- eureka优先保证可用性。
- Eureka不会有类似于ZooKeeper的选举leader的过程。
- 当网络分割故障发生时,每个Eureka节点,会持续的对外提供服务(注:ZooKeeper不会)
- Eureka各个节点都是平等的,个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。
ZooKeeper(CP)
- 作为一个分布式协同服务,ZooKeeper非常好,但是对于Service发现服务来说就不合适了。
- 当master节点因为网络故障与其他节点失去联系时,剩余节点会重新进行leader选举,选举期问整个zk集群都是不可用的,这就导致在选举期间注册服务瘫痪。
NACOS
是客户端主动拉取配置文件
@RefreshScope注解,该注解可以动态的从Nacos Config 中获取相应的配置
命名空间和配置分组
加载多配置
Ribbon
为什么需要Ribbon的客户端负载均衡?一半Ribbon的使用一般是客户端和客户端之间的调用,而客户端可能是多实例的,这时需要Ribbon的负载均衡
Gateway
配置routes
open feign远程调用
feigin的原理:
- 开启Feign的支持:两个核心注解
@EnableFeignClients
和@FeignClient
- 服务进行扫描,通过FeignInvocationHandle为每个远程接口创建JDK Proxy代理对象,并将这些对象注入到IOC容器中
- FeignInvocationHandler根据要调用的远程方法找到他对应的MethodHandler方法处理器
- MethodHandler通过RequestTemplate根据参数和URL等信息封装成Request对象,并调用Encoder进行编码
- Client接口根据http请求框架发送http请求,将Request对象发送至对应的远程服务地址,并获取远程服务的Response对象,调用Decoder对Response对象进行解码
feign调用存在的问题:
① 远程调用丢失请求头
注入RequestInterceptor 拦截器,将原RequestContextHolder.getRequestAttributes()属性注入
② 异步调用feign丢失上下文问题
问题描述:由于feign
请求拦截器为新的request
设置请求头底层是使用ThreadLocal
保存刚进来的请求,所以在异步情况下,其他线程并不能获取到主线程的ThreadLocal
,所以也拿不到请求。
解决:先获取主线程的requestAttributes
,再分别向其他线程中设置
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture.runAsync(() ->{
RequestContextHolder.setRequestAttributes(requestAttributes);
});
Gateway
通过cros解决跨域问题
Nginx
- Http服务器, 部署静态资源
- 反向代理,负载均衡
Nginx配置代码
Nginx 负载到网关
Nginx动静分离
将静态资源放到Nginx
Redis
Redis基础知识
**Redis是什么?**C语言,高性能非关系型键值对数据库, 与传统数据库做对比,Redis是存放在内存中,读写快,被广泛用于缓存方法。redis可以将数据写入到磁盘中,保证了数据安全不丢失
Redis的应用方式:缓存 、分布式锁、解决表单重复提交(前端跳转的时候生成唯一ID存放到Session中),比较一直则处理,处理完之后将唯一标识符删掉,不相等是重复提交,就不再处理。
Redis配置文件重点:
【server块】:bind、port、timeout
【append only mode】appendonly no
Redis几种数据类型场景的应用:、
Rediskey的删除策略:主动删除、被动删除、内存不够时清理
内存淘汰策略:LRU、LFU
安全策略:网络安全,不对未知用户开发端口,设置密码,禁止某些指令
Redis为什么这么快?
- Redis是一个纯内存数据库,一般都是简单的存取操作,线程占用的时间很多,时间的花费主要集中在IO上,所以读取速度快
- Redis使用的是非阻塞IO、IO多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切换和竞争
- Redis采用了单线程模型,保证了每个操作的原子性,也减少了线程的上下文切换和竞争
- Redis避免了多线程的锁的消耗
- Redis采用自己实现的事件分离器,效率比较高,内部采用了非阻塞的执行方式,吞吐能力比较大。
数据库一致性问题
双写模式 x 失效模式 x
解决方案:
1、实时性、一致性要求低的数据直接采用加过期时间
2、要求高的数据直接查看,cannal订阅binglog,采用双写模式写的时候加锁
什么是Redis大key
- string类型的值大于10kb
- hash、list、set、zset元素个数(元素个数超过5000)
如何找到大key
- String类型通过命令查找:
redis-cli -h 127.0.0.1 -p6379 -a "password" --bigkeys
- RabTool工具:
rbd dump.rdb -c memory --bytes 10240 -f redis.csv
直接删除大key会造成阻塞,为redis是单线程执行,阻塞期间,其他所有请求可能都会超时。超时越来越多,会造成redis连接会耗尽,产生各种异常
- 低峰期删除:凌晨,观察qps,选择低的时候,无法彻底解决
阻塞 - 分批次删除:对于hash ,使hscan扫描法,对于集合采用srandmember每次随机取数据进行删除。对于有序用zremrangebyrank直接删除,对于列表直接pop即可。
- 异步删除法:用unlink代替del来删除,这样redis会将这个key
放入到一个异步线程中进行删除,这样不会阻塞主线程。
MyBatis-Plus的使用
继承
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements
UserService {
}
service是baseMapper的扩展,但是我们可以通过mybatis xml方式给baseMapper进行扩展。
常用主键:@TableName、@TableId、@TableField、@TableLogic
XXL-job
传统定时任务的不足:
- 不支持集群
- 不支持任务重试
- 不支持动态调用
- 无报警机制
- 不支持生命周期的统一管理
- 任务数据难以统计
xxl-job使用步骤:
- 调度中心的部署
- 集成xxl-job执行器和任务
- 任务相关配置:集群调度策略、父子任务、动态参数任务、分片任务、日志回调、生命周期
商城相关业务方案
下单+支付链路
前往订单页面/toTrade
:这里有一个关键点就是设置令牌(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()
)
提交订单/submitOrder
:
- 令牌的对比和删除必须保证原子性,使用lua脚本
- 订单生成之前,让feign接口调用库存系统去锁库存(stock.locked),发锁库存延时队列消息(主动去锁库存);
rabbitTemplate.convertAndSend("stock-event-exchange", "stock.locked", lockedTo);
//Binding的配置
@Bean
public Binding stockLockedBinding() {
return new Binding("stock.delay.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.locked",
null);
}
//延迟队列
@Bean
public Queue stockDelay() {
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "stock-event-exchange");
arguments.put("x-dead-letter-routing-key", "stock.release");
// 消息过期时间 2分钟
arguments.put("x-message-ttl", 120000);
Queue queue = new Queue("stock.delay.queue", true, false, false,arguments);
return queue;
}
解库存消息消息队列代码
@Slf4j
@RabbitListener(queues = "stock.release.stock.queue")
@Service
public class StockReleaseListener {
@RabbitHandler
public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
log.info("******收到解锁库存的信息******");
try {
//当前消息是否被第二次及以后(重新)派发过来了
// Boolean redelivered = message.getMessageProperties().getRedelivered();
//解锁库存
wareSkuService.unlockStock(to);
// 手动删除消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch( Exception e){
// 解锁失败 将消息重新放回队列,让别人消费
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
}
}
}
- 订单发送成功,发订单成功的消息(order.create.order);这个时候订单就有两种情况,一种是成功完成订单,支付完成之后修改订单状态即可。当这个消息进入到死信队列里面,查询状态状态为未消费,给库存系统发送释放库存。消费了,就正常Ack即可;
rabbitTemplate.convertAndSend("order-event-exchange", "order.create.order", order.getOrder());
- 支付成功后,两个重要参数
notify_url
(异步回调)和return_url
(返回的页面)
notify_url:首先做的是验签,
登录
首先定义一个注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
boolean isNeedLogin() default true;
}
验证方法/verify:通过jwt解密token,如果成功获取token中的userId,返回给
登录login:生成token,并将token放入到cookie中
创建一个拦截器去拦截具有@LoginRequired的注解
备注:
JWT定义
public static String encode(String key,Map map,String salt){
if(salt!=null){
key+=salt;
}
JwtBuilder jwtBuilder = Jwts.builder().signWith(SignatureAlgorithm.HS256, key);
jwtBuilder.addClaims(map);
String token = jwtBuilder.compact();
return token;
}
JWT定义
public static Map decode(String key,String token,String salt)throws SignatureException{
if(salt!=null){
key+=salt;
}
Claims map = null;
map = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
return map;
}
发短信验证码
通常我们会将短信服务放到第三方组件微服务(sms、oss)中,发短信一半是要对他做接口防刷,一般是用redis(sms-手机号:随机数+时间戳)
秒杀
秒杀单独服务,这样好做限流
- 商品的上架,通过定时任务
- 先查询范围内的时间场次然后通过场次查询所在的sku,返回秒杀场次信息
- 如果不为空则上架商品,缓存商品信息到Redis,保存各个Sku值Hash结构(skill-sesession_id-skuInfo(uuid))中,通过Reddision分布式信号量(sku-uuid)保存秒杀数量。同事将活动商品信息放到list类型中(session_id)
- 基于分布式信号量去做
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
//TODO 秒杀成功,快速下单
boolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
超卖问题
秒杀的时候分两步:
- 判断库存名额是否充足
- 减少库存名额,扣减成功就是抢到
使用Lua脚本
少卖问题
扣成库存之后,订单没生成。
我们扣减库存后,发送消息队列,这里要有重试策略,如果发送消息失败进行重试,超过重试次数后,则要持久化磁盘,由补偿服务来进行扫描,进行后续业务的修改。类似mysql的日志持久化。
MQ
Kafka、ActiveMQ、RabbitMQ、RocketMQ对比
MQ的应用场景:应用解耦、流量削峰、数据分发
缺点:系统可用性降低,系统复杂度提高、一致性问题
JMS协议、AMQP协议对比:java api/网络线级协议,AMQP支持跨语言、跨平台
ActiveMQ: JMS规范,支持事务、支持XA协议,没有生产大规模支撑场景、官方维护越来越少
RabbitMQ: erlang语言开发、性能好、高并发,支持多种语言,社区、文档方面有优势,erlang语言不利于java程序员二次开发,依赖开源社区的维护和升级,需要学习AMQP协议、学习成本相对较高
kafka:高性能,高可用,生产环境有大规模使用场景,单机容量有限(超过64个分区响应明显变长)、社区更新慢卡夫卡:高性能,高可用,生产环境有大规模使用场景,单机容量有限(超过64个分区响应明显变长)、社区更新慢;只支持主要的MQ功能,像一些消息查询、消息回溯等功能没有提供,在大数据领域应用广
吞吐量单机百万吞吐量单机百万
rocketmq:java实现,方便二次开发、设计参考了kafka,高可用、高可靠,社区活跃度一般、支持语言较少,吞吐量单机十万
消息丢失、积压、重复等问题
消息发送出去,由于网络问题没有抵达服务器
- 做好容错方法(try-catch),发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发的方式
- 做好日志记录,每个消息状态是否都被服务器收到都应该记录
- 做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进行重发
消息抵达Broker,Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚未持久化完成,宕机。
- publisher也必须加入确认回调机制,确认成功的消息,修改数据库消息状态。
- 手动ACK,消费成功才移除,失败或者没来得及处理就noAck并重新入队
消息重复:
- 消费者的业务消费接口应该设计为幂等性的。比如扣库存有工作单的状态标志。
- rabbitMQ的每一个消息都有redelivered字段,可以获取是否是被重新投递过来的,而不是第一次投递过来的。
消息积压问题:
- 消费者宕机积压
- 消费者消费能力不足积压
- 发送者发送流量太
- 上线更多的消费者,进行正常消费
- 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理
线上mq消息如何保证99.99999%不丢失?
confirm消息机制
生产者投递消息到mq,mq收到后,会回复一个confirm消息,代表我收到了。如果没生产者没收到confirm则要重新发送消息。
持久化
mq收到消息之后,是先存在内存中,那么如果不持久化,可能会由于mq的重启或者宕机,导致内存消息丢失。所以必须要持久化。一旦持久化,mq的重启也不会丢。主要是基于raid的刷盘机制。raid0主要是磁盘的集成,可以将多块磁盘变为1块使用,提高存储。raid1是磁盘镜像,将磁盘分两半,同样的数据存储在两块区域,一般我们用的是raid 0 1。两种结合。
极端情况,mq还没有进行持久化,就挂掉了。或者由于网络原因之类的,生产者收不到confirm。我们对于极其重要的消息要做数据入库。
例如,生产者要发送一条消息之前,先插入数据库,status标识设置为0。收到回调成功之后设置为1。通过定时任务来定期扫描重发,进行消息的补偿。要设置最大重试次数。这种方式会加到数据库交互的压力,建议也可以直接通过在缓存中操作状态的变更,数据库定时清扫,减少压力,视我们的实际情况而定
消费者 ack机制
mq需要接受到消费者的应答后,才能确定当前消息消费完毕。如果超时或者未收到,则要进行重试,这也是我们说在消费消息的时候,一定要保证幂等。
Kafka:主从复制+同步刷盘
RabbitMQ实现延时队列的需求
- 设置队列的过期时间
- 设置消息的过期时间(TTL+死信队列)
采用方式1,RabbitMQ
采用的是惰性检查机制,如果发送的是带过期时间的消息,RabbitMQ会从队列顶端读取消息的过期时间,然后到了过期时间才会检查,这样会存在时间误差,通常我们采用带过期时间的队列。
顺序消费
单线程消费保证消息的顺序性;对消息进行编号,消费者根据编号处理消息;比如xxl-job中选择执行策略选择第一个。
ElasticSearch性能优化方案
filesystem cache
filesystem cache使我们操作系统的文件缓存机制。es就是依赖这种文件缓存机制,但是例如mysql这种就是自己实现的存储引擎机制。所以当我们遇到操作系统内存分配的时候其实要视情况而定,es的机器自然是给文件缓存多些,mysql的话就直接给mysql即可。
回到es,es的数据存储到磁盘上,我们在进行查询的时候,会从磁盘读取数据后,缓存到内存里,也就是我们的filesystem cache。如果我们读取数据的时候直接走磁盘逮度可能会到秒级,但是如果我们走内存,毫秒级就可以查到数据。可以想到redis为啥快,因为内存操作。所以我们要尽可能找到平衡点,提高文件存储内存。
减少非搜索字段的存储
我们有时候一般会这样搞,mysql里面存了什么,我们就往es里面继续存什么。那么可能我们实际搜索的字段只是其中某一个字段,例如content内容。其他的title啊,digest啊,都是不会被查询到的,那么这种就不要存进来。查的时候可以减少一点内存的占用,提高我们整体的有效数据存储。
数据预热
拿大家平时在京东上购物来说,就比如可口可乐吧,大家搜索的会比较多,非常可乐可能搜索的就会少一些。由于我们的内存缓存数据是具有时效性的,所从大家可以针对热点数据进行提前预刷。通过热数据探测啊,记录搜索次数等等啊,动态的去查询一次数据,保证每次尽量命中内存,这样可从极大的提高效率。
冷热分离
我们也可以通过不同的索引来存储不同的数据,把我们业务产品认为搜索可能性极大的数椐单独在一个索引,搜索可能性极小的走另一个索引,而不是为了方便直接存在一个里面,做聚合,这样可以保证我们的热数据大量保存在内存中,不被冷数据占用。这样也可以极大提高我们的速度。
分页优化
es的分页是聚合产生的。如果说我们每页10条数据,我查询第100页的数据,es的执行逻辑是会把每个shard上存储的前1000条数据都查到个协调节点上,如果你有个5个shard,那么就有5000条数据,接着协调节点对这5000条数据进行一些合并、处理,再获取到最终第100页10条数据。那么随着页的越来越深,就会导致效率极慢。所从我们从产品设计上要避免深分页问题。我们一般是使用scroll api来进行解决。scroll会生成数据的快照,每次进行翻页进行数据的滚动,比如你在京东上买货,你会发现他一般是触底加载的,b站也同样如此。