目录
三高
- 异步、推迟执行、批量执行
springboot
SpringBoot启动流程
- new springApplication对象,利用spi机制加载applicationContextInitializer, applicationLister接口实例(META-INF/spring.factories);
- 调run方法准备Environment,加载应用上下文(applicationContext),发布事件 很多通过lister实现
- 创建spring容器, refreshContext() ,实现starter自动化配置,spring.factories文件加载, bean实例化
SpringBoot自动配置的原理
- @EnableAutoConfiguration找到META-INF/spring.factories(需要创建的bean在里面)配置文件
- 读取每个starter中的spring.factories文件
分布式事务
强一致如扣下订单用seata at 弱一致用最终一致性
消息表跟mq解决不了如扣库存服务,需要账户服务返回结果,这时需要补偿型tcc
2PC
- 准备阶段和提交阶段反馈给事务协调者,是一种尽量保证强一致性的分布式事务,因此它是同步阻塞的,而同步阻塞就导致长久的资源锁定问题,总体而言效率低,并且存在单点故障问题,在极端条件下存在数据不一致的风险。
3pc
- 3PC 的准备阶段协调者只是询问参与者的自身状况,比如你现在还好吗?负载重不重?这类的。
- 比2pc多了中间阶段,预提交,并引入了超时机制。一旦事务参与者迟迟没有收到协调者的Commit请求,就会自动进行本地commit,这样相对有效地解决了协调者单点故障的问题。3PC 的引入是为了解决提交阶段 2PC 协调者和某参与者都挂了之后新选举的协调者不知道当前应该提交还是回滚的问题。
- 而预提交阶段的引入起到了一个统一状态的作用,它像一道栅栏,表明在预提交阶段前所有参与者其实还未都回应,在预处理阶段表明所有参与者都已经回应了。
- 所以说 3PC 通过预提交阶段可以减少故障恢复时候的复杂性,但是不能保证数据一致,除非挂了的那个参与者恢复。
- 相较于2PC,3PC最大的优点就是降低了参与者的阻塞范围;如果网络出现分区,此时协调者所在的节点和参与者无法进行正常的通信会数据不一致
tcc
2PC 和 3PC 都是数据库层面的,而 TCC是业务层面的分布式事务,就像我前面说的分布式事务不仅仅包括数据库的操作,还包括发送短信等,这时候 TCC 就派上用场了!和业务紧耦合因为要重试还要保证操作的幂等
TCC 指的是Try - Confirm - Cancel。
Try 指的是预留,即资源的预留和锁定,注意是预留。
Confirm 指的是确认操作,这一步其实就是真正的执行了。
Cancel指的是撤销操作,可以理解为把预留阶段的动作撤销了。
本地消息表+mq 最终一致性
- 源于eBay经典的BASE方案,将远程分布式事务拆分成一系列的本地事务
- 优点:基本避免了分布式事务,实现了“最终一致性”;开发简单;缺点:需设计DB消息表,同时还需要一个后台任务,不断扫描本地消息。导致消息的处理和业务逻辑耦合额外增加业务方的负担。适用于对一致性要求不高的非高并发场景。(本地消息队列是BASE理论,是最终一致模型)。
错误处理
步骤:1,2:任意一个出错,整个事务回滚,不会出错
步骤3:投递消息成功,结果接收MQ响应时网络出错。不会出错
步骤4:MQ宕机(解决方法:MQ一般支持持久化)。消费者消费失败(解决方法:重试3次)
步骤4-8:4-8任意一个失败次数超过重试次数。解决方法:发送报警,让人工处理。
从工程实践角度讲,这种整个流程自动回滚的代价是非常巨大的,不但实现复杂,还会引入新的问题。
比如自动回滚失败,又怎么处理?对应这种极低概率的case,采取人工处理,会比实现一个高复杂的自动化回滚系统,更加可靠,也更加简单。
步骤6:增积分完成之后,断网,导致增积分成功的消息没有发布出去。解决方法:定时任务会再次将消息投递,步骤5会判断到增积分已完成,直接跳到步骤8:发布消息
一个原子性问题:如果保证消息消费 + insert message到判重表这2个操作的原子性?消费成功,但insert判重表失败,怎么办?关于这个,在Kafka的源码分析系列,第1篇,exactly once问题的时候,有过讨论。
rocketmq 事务消息 最终一致性
RocketMQ中实现了分布式事务,实际上其实是对本地消息表的一个封装,将本地消息表移动到了MQ内部。
具体来说,就是把消息的发送分成了2个阶段:Prepare阶段和确认阶段。
具体来说,上面的2个步骤,被分解成3个步骤:
第一步:生产者:向RocketMQ发送Prepared消息,生产者会拿到消息的地址。
第二步:生产者:执行本地事务(修改数据库的数据)。
第三步:生产者:
若第二步执行成功,用第一步拿到的地址去访问消息,并修改状态(Confirm),消息接收者就能使用这个消息。
若第二步执行失败,用第一步拿到的地址去访问消息,并取消Prepared消息,消息接收者就得不到这个消息。
- Producer向broker端发送消息 TransactionMQProducer 事务消息producer
- 服务端将消息持久化成功之后,向发送方ACK确认消息已经发送成功,此时消息为半消息(half message)
- 发送方 通过实现TransactionListener 接口executeLocalTransaction方法 ,执行本地事务逻辑
- 发送方根据本地事务执行结果向服务端提交二次确认(LocalTransactionState.COMMIT_MESSAGE或者LocalTransactionState.ROLLBACK_MESSAGE)。服务端收到Commit状态则将半消息标记为可投递,订阅方最终将收到该消息;服务端收到Rollback状态则删除半消息,订阅方将不会接受该消息。
- 在断网或者是应用重启等特殊情况下,上述步骤4提交的二次确认最终未到达服务端,RocketMQ 会定期扫描消息集群中的事物消息,若发现未经确认的Prepared 消息,会对该消息发起消息回查,通过实现TransactionListener 接口checkLocalTransaction方法实现回查
- 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果
- 发送方根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照4对半消息进行操作
错误处理
消费方重试全部失败怎么办?
解决方法:发送报警,让人工处理。
从工程实践角度讲,这种整个流程自动回滚的代价是非常巨大的,不但实现复杂,还会引入新的问题。
比如自动回滚失败,又怎么处理?对应这种极低概率的case,采取人工处理,会比实现一个高复杂的自动化回滚系统,更加可靠,也更加简单。
最大努力通知
- 最大努力通知是分布式事务中要求最低的一种,适用于一些最终一致性时间敏感度低的业务;允许发起通知方处理业务失败,在接收通知方收到通知后积极进行失败处理,无论发起通知方如何处理结果都会不影响到接收通知方的后续处理;发起通知方需提供查询执行情况接口,用于接收通知方校对结果。典型的使用场景:银行通知、支付结果通知等。
seata
- 解决脏写 所有操作都加@GlobalTransactional
- 执行前,undolog数据快照
TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
XID 在微服务调用链路的上下文中传播。
RM 向 TC 注册分支事务,接着执行这个分支事务并提交(重点:RM在第一阶段就已经执行了本地事务的提交/回滚),最后将执行结果汇报给TC。
TM 根据 TC 中所有的分支事务的执行情况,发起全局提交或回滚决议。
TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
jvm
jmm 内存模型(java memory model)
- 对特定的内存或者高速缓存进行读写访问的过程抽象描述
- JMM 抽象出主内存(Main Memory)(所有变量都存储在主内存中,主内存是共享内存区域)和 工作内存(Working Memory) 线程对变量的操作(读取赋值等)必须拷贝到自己的工作内存中进行。
- 原子性,可见性除了volatile关键字能实现可见性之外,还有synchronized,Lock,final也是可以的。有序性Java提供了两个关键字volatile和synchronized来保证多线程之间操作的有序性,volatile关键字本身通过加入内存屏障来禁止指令的重排序,而synchronized关键字通过一个变量在同一时间只允许有一个线程对其进行加锁的规则来实现,
- 先行发生(happens-before)原则: happens-before是JMM定义的2个操作之间的偏序关系:如果操作A线性发生于操作B,则A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。如果两个操作满足happens-before原则,那么不需要进行同步操作,JVM能够保证操作具有顺序性
- volitile “观察加入volatile关键字生成的汇编代码发现,会多出一个lock前缀指令”,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
spring
- Bean容器在配置文件中找到Person Bean的定义,这个可以说是妈妈怀上了。 Bean容器使用Java
- 反射API创建Bean的实例,孩子出生了。
- Person声明了属性no、name,它们会被设置,相当于注册身份证号和姓名。如果属性本身是Bean,则将对其进行解析和设置。
- Person类实现了BeanNameAware接口,通过传递Bean的名称来调用setBeanName()方法,相当于起个学名。
- Person类实现了BeanFactoryAware接口,通过传递BeanFactory对象的实例来调用setBeanFactory()方法,就像是选了一个学校。
- PersonBean实现了BeanPostProcessor接口,在初始化之前调用用postProcessBeforeInitialization()方法,相当于入学报名。
- 如果有@PostConstruct 在这一步执行
- PersonBean类实现了InitializingBean接口,在设置了配置文件中定义的所有Bean属性后,调用afterPropertiesSet()方法,就像是入学登记。
- 配置文件中的Bean定义包含init-method属性,该属性的值将解析为Person类中的方法名称,初始化的时候会调用这个方法,成长不是走个流程,还需要自己不断努力。
- Bean Factory对象如果附加了Bean后置处理器,就会调用postProcessAfterInitialization()方法,毕业了,总得拿个证。
- Person类实现了DisposableBean接口,则当Application不再需要Bean引用时,将调用destroy()方法,简单说,就是人挂了。
- 配置文件中的Person Bean定义包含destroy-method属性,所以会调用Person类中的相应方法定义,相当于选好地儿,埋了。
IOC/DI
-
applicationContext继承beanFactory,通过 ApplicationContext 这个 IoC 容器的入口,用它的两个具体的实现子类,从 ClassPathXmlApplicationContext 或者 FileSystemXmlApplicationContext 中读取数据,用 getBean() 获取具体的 bean instance。
-
第一个过程是Resource定位资源过程:
通过 ResourceLoader 来完成资源文件位置的定位,DefaultResourceLoader 是默认的实现,同时上下文本身就给出了 ResourceLoader 的实现,可以从类路径,文件系统, URL 等方式来定为资源位置。这个过程类似于容器寻找数据的过程。 -
第二个过程是BeanDefinition的载入过程:
BeanDefinition载入过程指的是把用户定义好的Bean表示为容器内部的数据结构BeanDefinition,
通过 BeanDefinitionReader来完成Bean信息的解析, 往往使用的是XmlBeanDefinitionReader 来解析 bean 的 xml 定义文件 - 实际的处理过程是委托给 BeanDefinitionParserDelegate 来完成的,从而得到 bean 的定义信息 -
第三个过程就是BeanDefinition的向IOC容器的注册过程:
这个注册过程是通过调用BeanDefinitionRegistry接口来完成的,就是把载入过程中解析得到的BeanDefinition向IOC容器进行注册。通过下面的源码可以得到,注册过程就是在IOC容器将BeanDefinition注入到一个HashMap中,IOC容器就是通过这个HashMap来持有BeanDefinition数据的。
管理对象的创建和依赖关系的维护。
解耦,由容器去维护具体的对象。
托管了类的产生过程,
**依赖注入(Dependency Injection)**就是由IOC容器在运行期间,动态地将某种依赖关系注入到对象之中
aop
循环依赖 只有单例bean 非单例bean每次获取
-
构造器注入循环依赖无法解决 BeanCurrentlyInCreationException 根本原因:Spring解决循环依赖依靠的是Bean的“中间态”这个概念,而这个中间态指的是已经实例化,但还没初始化的状态。而构造器是完成实例化的东东,所以构造器的循环依赖无法解决~~~
-
Map<String, Object> singletonObjects: 一级缓存,也就是我们平常理解的单例池,存放已经完整经历了完整生命周期的bean对象。
-
Map<String, Object> earlySingletonObjects: 二级缓存,存储早期暴露出来的bean对象,bean的生命周期未结束。(属性还未填充完)
-
Map<String,ObjectFactory<?> > singletonFactories: 三级缓存,存储生成bean的工厂。
-
A在创建过程中需要B,于是A先将自己放到三级缓存里面,去实例化B
-
B实例化的时候发现需要A,于是B先查一级缓存,没有再查二级缓存,还是没有,再查三级缓存,找到了A;然后把三级缓存里面的这个A放到二级缓存里面,并删除三级缓存里面的A
-
B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态);然后回来接着创建A,此时B已经创建结束,直接从一级缓存里面拿到B,然后完成创建,并将A放入到一级缓存中
-
是否可以没有二级缓存如果只在两个对象AB产生循环依赖时,可以不需要。
-
是否需要三级缓存 Spring无法提前知道这个对象是不是有循环依赖的情况,而正常情况下(没有循环依赖情况),Spring都是在创建好完成品Bean之后才创建对应的代理,Spring选怎么做到提前曝光对象而又不生成代理呢?Spring就是在对象外面包一层ObjectFactory,提前曝光的是ObjectFactory
@Component 和 @Bean 的区别是什么?
- 作用对象不同: @Component 注解作用于类,而@Bean注解作用于方法。
@Component通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用 @ComponentScan
注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。@Bean
注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean告诉了Spring这是某个类的示例,当我需要用它的时候还给我。
@Bean 注解比 Component 注解的自定义性更强,而且很多地方我们只能通过 @Bean
注解来注册bean。比如当我们引用第三方库中的类需要装配到 Spring容器时,则只能通过 @Bean来实现。
spring 单例bean线程安全问题
-
在Bean对象中尽量避免定义可变的成员变量(不太现实)。
在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。
Spring AOP 和 AspectJ AOP 有什么区别?
-
Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而
AspectJ 基于字节码操作(Bytecode Manipulation)。Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP
框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比Spring AOP 快很多。
redis
###缓存雪崩 大量key过期
- 加锁,失效时间+随机值
- 使用快速失败的熔断策略,减少 DB 瞬间压力;
使用主从模式和集群模式来尽量保证缓存服务的高可用。
缓存穿透
- 不存在的key 缓存空,bitmap黑名单
- 对不存在的用户,在缓存中保存一个空对象进行标记,防止相同 ID 再次访问 DB。不过有时这个方法并不能很好解决问题,可能导致缓存中存储大量无用数据。
- 使用 BloomFilter 过滤器,BloomFilter 的特点是存在性检测,如果 BloomFilter 中不存在,那么数据一定不存在;如果 BloomFilter 中存在,实际数据也有可能会不存在。非常适合解决这类的问题。
缓存击穿
- 提前缓存热点key
- 可以使用互斥锁更新,保证同一个进程中针对同一个数据不会并发请求到 DB,减小 DB 压力。
- 使用随机退避方式,失效时随机 sleep 一个很短的时间,再次查询,如果失败再执行更新。
- 针对多个热点 key 同时失效的问题,可以在缓存时使用固定时间加上一个小的随机数,避免大量热点 key 同一时刻失效。
rdb aof
- rdb 适合大规模的数据恢复,对数据完整性和一致性要求不高更适合使用,节省磁盘空间,恢复速度快
- aof 最少可以设置sec 秒级存储操作数据集,但文件大恢复慢
- 官方建完整性要求不高用rdb,或rdb+aof ,不建议aof单独使用 有bug
消息队列
rabbitmq
顺序消息
- 拆分多个 queue,每个 queue 一个 consumer,就是多一些 queue 而已,确实是麻烦点;
- 或者就一个 queue 但是对应一个 consumer,然后这个 consumer 内部用内存队列做排队,然后分发给底层不同的 worker 来处理。
消息丢失
- 队列持久化durable开启
- 消费者和生产者开启手动消息确认ack
- mandatory交换机未找到对应队列,通过return方法回退给生产者
- 异步发布确认 生产者通过ConfirmCallback处理
- 镜像队列
- 死信交换机 Dead-Letter-Exchange
- 备份交换机 参数alternate-exchange
重复消费
- 利用 redis 执行 setnx 命令,天然具有幂等性。从而实现不重复消费
消息防堆积
- 加强对不合理使用MQ的审批。
- 监控消费能力(耗时<300ms),及时预警。
- 框架层实现发送方限流。(默认值:100条/s)
- 设置消息TTL。
- 使用惰性队列 x-queue-mode
- qos 消费者在接收到队列里的消息但没有返回确认结果之前,队列不会将新的消息分发给该消费者,必须设置为手动ack
死信的来源
- 消息 TTL 过期
- 队列达到最大长度(队列满了,无法再添加数据到 mq 中)
- 消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false.
延时队列
- 如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。必须使用delay插件
交换器无法根据自身类型和路由键找到符合条件队列时,有哪些处理?
- mandatory :true 返回消息给生产者。
- mandatory: false 直接丢弃。
- 这时候可以通过调用channel.addReturnListener来添加ReturnListener监听器实现。
- 错误消息记录Warn级别日志,进行监控。
java基础
抽象类和接口
接口可以继承接口,不能实现
抽象类能实现接口,也能继承抽象类
语法区别
在这里插入图片描述
设计层面上的区别
- 如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系,比如鸟是否能飞(或者是否具备飞行这个特点),能飞行则可以实现这个接口,不能飞行就不实现这个接口。
- 设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。
- 对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动。
string为什么设计成final
-
字符串变量都指向字符串池的同一个字符串,也就是共享,final 且不可变,来保证其安全性。
-
声明String类是Final的,那么就不会存在其他类继承String,而改写值
-
-
Java的类加载是否一定遵循双亲委托模型?
在实际开发中,我们可以通过自定义ClassLoader,并重写父类的loadClass方法,来打破这一机制
2.序列化
-
数据结构
hashmap
java1.8 中put 源码:put 中调用 putVal()方法:
-
首先判断map中是否有数据,没有就执行resize方法
-
如果要插入的键值对要存放的这个位置刚好没有元素,那么把他封装成Node对象,放在这个位置上即可
-
如果这个元素的key与要插入的一样,那么就替换一下。
-
如果当前节点是TreeNode类型的数据,执行putTreeVal方法
-
遍历这条链子上的数据,完成了操作后多做了一件事情,判断长度是否大于等于8 ,并且数组长度大于64 ,执行treeifyBin方法
-
数据put完成后,如果HashMap的总数超过threshold就要resize
HashMap内部的bucket数组长度为什么一直都是2的整数次幂?
- 可以通过(table.length - 1) & key.hash()这样的位运算快速寻址
- 在HashMap扩容的时候可以保证同一个桶中的元素均匀地散列到新的桶中,具体一点就是同一个桶中的元素在扩容后一般留在原先的桶中,一般放到了新的桶中。
HashMap如何处理key为null的键值对?答:放置在桶数组中下标为0的桶中
为什么HashMap要树化
- 我们知道链表查询是线性的,会严重影响存取的性能 为什么要将链表中转红黑树的阈值设为8?默认是链表长度达到 8就转成红黑树,而当长度降到 6 就转换回去,这体现了时间和空间平衡的思想
hashmap如何解决哈希冲突
1.7版本的时候用了9次扰动,5次异或,4次位移,减少hash冲突,但是1.8就只用了两次,觉得就足够了一次异或,一次位移。
- 开放定址法也称线性探测法,就是从发生冲突的那个位置开始,按照一定次序从Hash表找到一个空闲位置然后把发生冲突的元素存入到这个位置,put时发生冲突时,顺着链表向前找到一个空闲的位置,来存储这个冲突的key
- put 时计算hashcode时将 hashCode 值右移 16 位(h >>> 16 代表无符号右移 16 位),也就是取int 类型的一半,刚好可以将该二进制数对半切开,并且使用位异或运算(如果两个数对应的位置相反,则结果为 1,反之为 0),
- 链式寻址法,这是一种常见的方法,简单理解就是把存在Hash冲突的key,以单向链表来进行存储,比如HashMap
ConcurrentHashmap
- key 或value为空 抛异常
- 获取key 的hashcode ,循环table,如果没初始化则初始化容器,
- 如果已经初始化,则判断该hash位置的节点是否为空,如果为空,则通过CAS操作进行插入。
- 如果该节点不为空,再判断容器是否在扩容中,如果在扩容,则帮助其扩容。
- 如果没有扩容,则进行最后一步,先加锁,然后找到hash值相同的那个节点(hash冲突),
- 循环判断这个节点上的链表,决定做覆盖操作还是插入操作。 最后达到红黑树阈值转换链表类型
- get没有加锁 因为有volatile修饰
并发编程
AQS
- 通过维护一个共享资源状态( Volatile Int State )和一个先进先出( FIFO )的线程等待队列来实现一个多线程访问共享资源的同步框架。
- 独占式:只有一个线程能执行,具体的 Java 实现有 ReentrantLock。
- 共享式:多个线程可同时执行,具体的 Java 实现有 Semaphore和CountDownLatch。
- ReentrantLock为例,默认非公平锁,state初始化为0,表示未锁定状态,A线程lock()时,会调用tryAcquire()独占锁并cas 设置state+1,未设置成功加入fifo队列,成功将ownerThread设置为当前线程,之后其他线程再想tryAcquire的时候就会失败,直到A线程unlock()到state=0为止,其他线程才有机会获取该锁。A释放锁之前,自己也是可以重复获取此锁(state累加),这就是可重入的概念。与非公平锁不同,reenTrantLock设置为公平锁时不会上来就获取锁,而是先看state是否为0,再去获取锁,不为0将当前线程加入队列
- 公平锁 :按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁 :当线程要获取锁时,先通过两次 CAS 操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒。
countDownLatch
CountDownLatch对AQS的共享方式实现为:CountDownLatch 将任务分为N个子线程去执行,将 state 初始化为 N, N与线程的个数一致,N个子线程是井行执行的,每个子线程都在执行完成后 countDown()1次, state 执行 CAS 操作并减1。在所有子线程都执行完成( state=O)时会LockSupport.unpark()主线程,然后主线程会从 await()返回,继续执行后续的动作。
threapoolexecutor 线程数的设置
- CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
- I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
- io/cpu 混合 最佳线程数 =CPU 核数 * [ 1 +(I/O 耗时比例 / CPU 耗时比例)]
- 比如IO/CPU的比率很大,比如10倍,2核,较佳配置:2*(1+10)=22个线程,
线程数更严谨的计算的方法应该是:最佳线程数 = N(CPU 核心数)∗(1+WT(线程等待时间)/ST(线程计算时间)),其中 WT(线程等待时间)=线程运行总时间 - ST(线程计算时间)。
我们可以通过 JDK 自带的工具 VisualVM 来查看 WT(线程等待时间)ST(线程计算时间) 情况。
CPU 密集型任务的 WT/ST 接近或者等于 0,因此, 线程数可以设置为 N(CPU 核心数)∗(1+0)= N,和我们上面说的 N(CPU 核心数)+1 差不多。
IO 密集型任务下,几乎全是线程等待时间,从理论上来说,你就可以将线程数设置为 2N(按道理来说,WT/ST 的结果应该比较大,这里选择 2N 的原因应该是为了避免创建过多线程吧)。
io密集型visualVM截图
偏向锁、轻量级锁、重量级锁
-
偏向锁 只有一个线程,没有其他线程竞争的情况下,将mark word 对象头锁标志位设置为01,是否偏向锁设置为1t,hread ID设置为当前线程,如果下次再执行该同步块时,判断当前偏向锁所偏向的对象是否是当前线程,不需要再进行获取与释放锁的过程,如果明显存在其他线程申请锁,那么偏向锁将很快膨胀为轻量级锁。如果需要,使用参数-XX:-UseBiasedLocking禁止偏向锁优化(默认打开)。
-
轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。****锁标志位设置为00
-
重量级锁synchronized 锁标志位10
-
synchronized https://www.jianshu.com/p/d53bf830fa09
-
lock
-
volatile
-
Java中如何停止一个线程?
Java提供了很丰富的API但没有为停止线程提供API。JDK 1.0本来有一些像stop(), suspend() 和 resume()的控制方法但是由于潜在的死锁威胁因此在后续的JDK版本中他们被弃用了,之后Java API的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。当run() 或者 call() 方法执行完的时候线程会自动结束,如果要手动结束一个线程,你可以用volatile 布尔变量来退出run()方法的循环或者是取消任务来中断线程。
-线程间共享数据?
-
为什么wait, notify 和 notifyAll这些方法不在thread类里面?
一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象
-
final Segment<K,V> segmentFor(int hash) {
return segments[(hash >>> segmentShift) & segmentMask];
}
- a b c 顺序执行
public class SignTest implements Runnable{
private String name;
private Object prev;
private Object self;
private SignTest(String name, Object prev, Object self) {
this.name = name;
this.prev = prev;
this.self = self;
}
public static void main(String[] args) {
Object a = new Object();
Object b = new Object();
Object c = new Object();
SignTest pa = new SignTest("A", c, a);
SignTest pb = new SignTest("B", a, b);
SignTest pc = new SignTest("C", b, c);
new Thread(pa).start();
try {
Thread.sleep(100); //确保按顺序A、B、C执行
new Thread(pb).start();
Thread.sleep(100);
new Thread(pc).start();
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
int count = 10;
while (count > 0) {
synchronized (prev) {
synchronized (self) {
System.out.print(name);
count--;
self.notify();
}
try {
prev.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
mybatis
Mybatis原理
- sqlsessionFactoryBuilder生成sqlsessionFactory(单例)工厂模式生成sqlsession执行sql以及控制事务
- Mybatis通过动态代理使Mapper(sql映射器)接口能运行起来即为接口生成代理对象将sql查询到结果映射成pojo
- sqlSessionFactory构建过程解析并读取配置中的xml创建Configuration对象 (单例)使用Configruation类去创建sqlSessionFactory(builder模式)
Mybatis一级缓存与二级缓存
二级缓存是指可以跨 SqlSession 的缓存。是 mapper 级别的缓存;原理:是通过 CacheExecutor 实现的。CacheExecutor其实是 Executor 的代理对象
Mybatis 的一级、二级缓存:
默认情况下一级缓存是开启的,而且是不能关闭的。
1)一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为
Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就
将清空,默认打开一级缓存。
2)二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap
存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,
如 Ehcache。原理:是通过 CacheExecutor 实现的。CacheExecutor其实是 Executor 的代理对象,默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现 Serializable 序列化接口(可用来保存对象的状态),可在它的映射文件中配置
;
读取大量数据
通过游标cursor
①首先在dao层mapper文件中写查询方法(下面的示例带有查询条件)
// 策略为向下滚动
@Options(resultSetType = ResultSetType.FORWARD_ONLY)
@Select({"select * from task where status!=#{status} order by create_time desc"})
Cursor<Task> getTasksStatistic(Integer status);
②在service层写方法并在serviceimpl中实现
// 普通mapper在查询一次后就会断开连接,所以必须用sqlSessionTemplate
TaskCursorVo getTaskStatus(Integer currentIndex, Integer pageSize);
public TaskCursorVo getTaskStatus(Integer currentIndex,Integer pageSize) {
TaskCursorVo taskCursorVo=new TaskCursorVo();
List<TaskStatusVo> collect=new ArrayList<>();
// 第一个参数为方法名,第二个参数是查询条件的值(如果没有查询条件就写null),第三个条件是从下标为currentIndex的开始查pageSize条数据
Cursor<Task> tasksStatistic = sqlSessionTemplate.selectCursor("getTasksStatistic",DELETED.getCode(),new RowBounds(currentIndex,pageSize));
// 对结果进行遍历封装
Iterator<Task> iterator = tasksStatistic.iterator();
while (iterator.hasNext()) {
Task task = iterator.next();
TaskStatusVo taskStatusVo = new TaskStatusVo();
BeanUtils.copyProperties(task, taskStatusVo);
taskStatusVo.setStatusName(Objects.requireNonNull(getByCode(task.getStatus())).getDesc());
collect.add(taskStatusVo);
}
taskCursorVo.setTaskStatusVos(collect);
// 需要把当前下标返回给前端
taskCursorVo.setCurrentIndex(tasksStatistic.getCurrentIndex());
Example e=new Example(Task.class);
Example.Criteria c = e.createCriteria().andNotEqualTo("status", DELETED.getCode());
List<Task> tasks = taskMapper.selectByExample(e);
// 把总条数查出来给前端
taskCursorVo.setTotalSize(tasks.size());
return taskCursorVo;
}
流式查询
数据库
mysql
页16kb->区(64个页*16=1M) -> 段(256个区 M)
为啥默认rr
- 假设rc级别,binlog 默认为statement, binlog是按commit顺序存储的,如果两个事务并发修改相同数据,后修改的先提交了,在binlog里顺序就乱了造成不一致
使用Innodb的情况下,一条更新语句是怎么执行的?
update T set c = c +1 where id = 2
-
执行器首先查询到id=2的这一行。如果这行数据本身就在内存中,那么直接返回给执行器;否则从磁盘加载到内存,然后再返回。
-
执行器拿到数据执行c+1的操作并调用更新接口写入这行新数据。
-
引擎将这行新数据更新到内存中,同时将这个更新操作记录到redo log中。此时redo处于preppare状态。
-
执行器生成这个操作的binlog 并把binlog写入磁盘。
-
执行器调用引擎的事务提交接口,将redolog改为commit状态。
order by原理
- 通过sort_buffer实现
- sort_buffer_size够大时用全字段排序,查询的字段都放到sort_buffer中排序
- 反之用row_id排序,sort_buffer只放排序字段和id,如果有其他查询字段排序时要回表通过id查询,
- 优化器优先用全字段排序
慢查询
-
SHOW STATUS LIKE ‘last_query_cost’; 查询最后一次SQL读取的页的数量。
-
开启慢查询日志slow_query_log ,设置时间阈值long_query_time 通过mysqldumpslow对日志分析
-
show profiles CPU 内存等开销
-
explain或describe 是一样的
| ALL | 全表扫描| index | 索引全扫描
| range| 索引范围扫描,常用语<,<=,>=,between等操作
| ref| 使用非唯一索引扫描或唯一索引前缀扫描,等值匹配
| eq_ref| 类似ref,区别在于使用的是唯一索引,使用主键的关联查询
| const/system | 单条记录,系统会把匹配行中的其他列作为常数处理,如主键或唯一索引查询
| null | MySQL不访问任何表或索引,直接返回结果
-
key_len 根据索引长度观察命中了组合索引几个字段,只针对where字段,分组排序中不算。
int 固定4个长度,
varchar(10)和char(10) 10*(utf8=3,gbk=2,latin1=1)+允许null+1+可变+2 -
mysql 的schema视图查看表io信息 索引信息
ICP 索引下推
select * from user2 where username like 'j%' and age=99;
- 搜索引擎中提前判断对应的搜索条件是否满足,满足了再去回表,通过减少回表次数进而提高查询效率。第
哪些情况适合创建索引
- 字段的数值有唯一性的限制
- 频繁作为 WHERE 查询条件的字段
- 经常 GROUP BY 和 ORDER BY 的列
- UPDATE、DELETE 的 WHERE 条件列
- DISTINCT 字段需要创建索引
- join时 对用于连接的字段创建索引,类型必须一致
- 使用列的类型小的创建索引
- 使用字符串前缀创建索引
- 使用字符串前缀创建索引
- 使用最频繁的列放到联合索引的左侧
不适合索引
- 数据量小的表最好不要使用索引
- 有大量重复数据的列上不要建立索引
- 避免对经常更新的表创建过多的索引
- 不建议用无序的值作为索引
索引失效
- 使用函数
- 用索引字段计算
- 隐式类型转换 如int类型判断加引号
- 范围条件右边的列索引失效
- 不等于(!= 或者<>)索引失效
- is null可以使用索引,is not null无法使用索引
- like以通配符%开头索引失效
- OR 回表:主键+索引字段 或联合索引首字段,单个或组合都可以。联合索引多个字段反而不行;没回表:前后存在非索引的列,索引全部失效
索引优化
- 优先考虑覆盖索引,一个索引包含了满足查询结果的数据就叫做覆盖索引。 索引列+主键 包含 SELECT 到 FROM之间查询的列 。
- LEFT JOIN用于确定如何从右表搜索行,左边一定都有,则右边是我们的关键点,一定需要建立索引,左表建没用
- LEFT JOIN 时,选择小表作为驱动表, 大表作为被驱动表 。减少外层循环的次数。不是数据量小的表,而是过滤完条件之后参与join的数据量
- INNER JOIN 时,MySQL会自动将 小结果集的表选为驱动表 。选择相信MySQL优化策略。
- 能够直接多表关联的尽量直接关联,不用子查询。
- 需要JOIN 的字段,数据类型保持绝对一致。
b+tree优点
- 平衡、数据有序
- B+树有更低的树高
- 左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它。
- 叶子结点,数据间有指针,更适合mysql范围查询及buffer pool预读
- 只有叶子节点存数据,这样的存储结构,索引数据多,树的层数更低
当2000w数据时,你的mysql B+tree 有多少层?
这里的指针在mysql中是占用6个字节
我们假设:我们的主键采用的是BIGINT类型,这个类型是8个字节,一行的数据是800字节。
当B+ tree高度=1时:一个节点,也就是一个块(16KB)直接存放的是数据(一条记录是800字节),那么可以存放 16k / 800字节 = 16 * 1024 / 800 = 20条记录
当B+ tree高度=2时:需要计算非叶子节点可以放多少指针(6个字节)和 多个主键值(BIGINT类型是8个字节),那么我们的非叶子节点可以指向多少个叶子节点了? 16KB / (6 + 8) = 16 * 1024 / 16 = 1170 个叶子节点。 一个叶子节点我们可以存放20条记录, 那边总记录就是 1170 * 20 = 23400
当B+ tree高度 = 3时,是不是就是 1170 * 1170 * 20 = 14040 * 1170 = 16426800
分页优化
- select * from student a,(select * from student order by id desc limit 1000000,10)b where a.id=b.id
- 适合自增主键select * from student where id>1000000 limit 10;
行锁必须有索引才能实现,否则会自动锁全表,那么就不是行锁了。
buffer pool
-
缓冲池(buffer pool)是一种降低磁盘访问的机制;
-
缓冲池通常以·页(page)·为单位缓存数据;
-
缓冲池的常见管理算法是LRU,InnoDB对普通LRU进行了优化:
-
mysql未优化的普通Lru有什么问题?
-
容易出现预读失效:预读:由于预读(Read-Ahead),提前把页放入了缓冲池,但最终MySQL并没有从页中读取数据,称为预读失效。
-
容易出现缓冲池污染:当某一个SQL语句,要批量扫描大量数据时,可能导致把缓冲池的所有页都替换出去,导致大量热数据被换出,MySQL性能急剧下降,这种情况叫缓冲池污染。
-
缓冲池分为老生代和新生代,入缓冲池的页,优先进入老生代,页被访问,才进入新生代,以解决预读失效的问题
-
增加“老年代停留时间窗口innodb_old_blocks_time”机制,解决缓冲池污染问题,只有满足“被访问”并且“在老生代停留时间”大于T,才会被放入新生代头部;最近访问一次的数据就不会把缓冲池中的页替换出去
其他优化
- innodb_buffer_pool_size 这个值越大,查询的速度就会越快。但是这个值太大会影响操作系统的性能。
- 优化表结构 冷热数据字段折分子表、增加冗余字段、避免text blob
主从同步延迟的原因 (慢查询或锁表)
一个服务器开放N个链接给客户端,大量的并发更新操作,但是从服务器中读取binlog 线程仅有一个,当某个sql在从服务器上执行的时间稍长或者由于某个sql要进行锁表就会导致主服务器上大量sql积压,未被同步到从服务器中。这就导致了主从不一致,也就是主从延迟。
解决方案: 主库负责更新操作、sql查询优化、业务层面
数据库连接数调整、主库参数syncbinlog = 1 ,innodbflushlogattrxcommit=1
雪花算法 snowflake算法64bit 全局唯一ID 生成策略
1.优点:
毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
可以根据自身业务特性分配bit位,非常灵活。
2.缺点:
依赖机器时钟,如果机器时钟回拨,会导致重复ID生成
在单机上是递增的,但是由于设计到分布式环境,每台机器上的时钟不可能完全同步,有时候会出现不是全局递增的情况
(此缺点可以认为无所谓,一般分布式ID只要求趋势递增,并不会严格要求递增,90%的需求都只要求趋势递增)
类美团 Leaf方案 本地缓存+MySQL
mysql怎么解决脏读
- 脏读在read commit级别中出现,rc级别是当前读,快照读解决了这个问题read view,约定只能看到比当前事务id小的数据,所以未提交到数据库的,跟之后提交的都查询不到。
mysql怎么解决不可重复读
- 读已提交级别每次查询会生成read view所以有重复读问题,rr级别只有第一次才生成read view所以避免了不可重复读
怎么解决的幻读(没有完全解决)
- 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读。只能看见比当前事务id小的数据
- 针对当前读(select … for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读。
- 事务A查询;事务B添加;事务A更新事务B插入的记录后,再执行A查询幻读就发生了
- 事务A查询;事务B添加;事务A查询后加for update 幻读发生了
如何减少主从延迟
-
降低多线程大事务并发的概率,优化业务逻辑
-
优化SQL,避免慢SQL, 减少批量操作 ,建议写脚本以update-sleep这样的形式完成。
-
提高从库机器的配置 ,减少主库写binlog和从库读binlog的效率差。
-
尽量采用 短的链路 ,也就是主库和从库服务器的距离尽量要短,提升端口带宽,减少binlog传输
的网络延时。 -
实时性要求的业务读强制走主库,从库只做灾备,备份
-
表里有很多数据怎么取出其中10条
-
explain https://www.cnblogs.com/yycc/p/7338894.html
-
主键生成策略
-
mysql https://blog.youkuaiyun.com/qq_32483145/article/details/80191323
-
SHOW PROCESSLIST显示哪些线程正在运行。show processlist;只列出前100条,如果想全列出请使用show full processlist;
-
show OPEN TABLES where In_use > 0;
-
什么是死锁?
死锁: 是指两个或两个以上的进程在执行过程中。因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等竺的进程称为死锁进程。
表级锁不会产生死锁.所以解决死锁主要还是针对于最常用的InnoDB。
死锁的关键在于:两个(或以上)的Session加锁的顺序不一致。
那么对应的解决死锁问题的关键就是:让不同的session加锁有次序。
死锁的解决办法?
1.查出的线程杀死 kill
SELECT trx_MySQL_thread_id FROM information_schema.INNODB_TRX;
2.设置锁的超时时间
Innodb 行锁的等待时间,单位秒。可在会话级别设置,RDS 实例该参数的默认值为 50(秒)。
生产环境不推荐使用过大的 innodb_lock_wait_timeout参数值
该参数支持在会话级别修改,方便应用在会话级别单独设置某些特殊操作的行锁等待超时时间,如下:
set innodb_lock_wait_timeout=1000; —设置当前会话 Innodb 行锁等待超时时间,单位秒。
容器
tomcat调优
框架相关
eureka
- 30/s 心跳client向server,30/s续约更新renew方法,90/s续约到期 ,
- 自我保护: 针对网络异常波动的安全保护措施,15min 超过85%的节点都没有正常心跳
-
spring boot负载策略 https://www.baidu.com/link?url=goATHgdXFocrFYoYOfY02qWwlxoU_sfp2DZw8aCpaTE7MfbtGej7Nz6jvSw5i6xYzMJJw_F-bItCr3w3rR2qMofPyLt0OQsRB1J320y92xi&wd=&eqid=c21003a2001b2c2e000000025d2b4e6c
-
ribbon和feign区别
1.启动类使用的注解不同,Ribbon用的是@RibbonClient,Feign用的是@EnableFeignClients 2.服务的指定位置不同,Ribbon是在@RibbonClient注解上声明,Feign则是在定义抽象方法的接口中使用@FeignClient声明 3.调用方式不同,Ribbon需要自己构建http请求,模拟http请求然后使用RestTemplate发送给其他服务,步骤相当繁琐 Feign则是在Ribbon的基础上进行了一次改进,采用接口的方式,将需要调用的其他服务的方法定义成抽象方法即可 不需要自己构建http请求。不过要注意的是抽象方法的注解、方法签名要和提供服务的方法完全一致
网络
tcp三次握手
第1次握手:客户端发送一个带有SYN(synchronize)标志的数据包给服务端;
第2次握手:服务端接收成功后,回传一个带有SYN/ACK标志的数据包传递确认信息,表示我收到了;
第3次握手:客户端再回传一个带有ACK标志的数据包,表示我知道了,握手结束。
其中:SYN标志位数置1,表示建立TCP连接;ACK标志表示验证字段。
四次挥手
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。客户端->服务端,服务端->客户端 要单独关闭
第1次挥手:客户端发送一个FIN,用来关闭客户端到服务端的数据传送,客户端进入FIN_WAIT_1状态;
第2次挥手:服务端收到FIN后,发送一个ACK给客户端,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),服务端进入CLOSE_WAIT状态;
第3次挥手:服务端发送一个FIN,用来关闭服务端到客户端的数据传送,服务端进入LAST_ACK状态;
第4次挥手:客户端收到FIN后,客户端t进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,服务端进入CLOSED状态,完成四次挥手。
其中:FIN标志位数置1,表示断开TCP连接。
算法
各种树二叉 红黑啥的
冒泡 二分啥的
分布式思想
集群选举 一致性哈希 paxos raft
热门问题
减库存redis + Lua
高并发高可用
幂等 或前置状态判断
-
前端按钮置灰
-
token
-
把”用户ID + 分隔符 + 商品ID“作为Redis key,并把”短时间所对应的秒数“设置为seconds,让它过期自动删除。
-
-
秒杀
- 账号风控
- 层层削减流量 redis + mq,扣库存redis+lua 服务降级、熔断、限流、
- 秒杀的特点就是这样高并发 时间极短、 瞬间用户量大。
- 资源静态化:按钮控制:置灰
- Nginx 并发也随便顶几万不是梦,负载均衡嘛,一台服务几百,在秒杀的时候多租点流量机。
- 服务单一职责:好处就是就算秒杀没抗住,也不会影响到其他的服务。(强行高可用)
- 恶意请求: 黑名单
- 链接暴露: 秒杀链接加盐 (可通过接口下单前获取)
- redis 集群 高可用
- 库存预热:提前加载到redis 活动结束跟数据库对库存同步
- 超卖 lua脚本 redission 分布式锁
- 限流降级熔断 sentinel,hystrix
- 削峰填谷:mq
红黑树 avl树
- 根结点和叶子节点(nil 空结点)为黑色,黑色节点左右都为红色
- 红黑树确保没有一条路 径会比其他路径长出俩倍,因而是接近平衡的。
- avl 树左右子树高度<=1 绝对平衡,总是旋转损失效率
http https
-
客户端发起Https请求,连接到服务器的443端口。
-
服务器必须要有一套数字证书(证书内容有公钥、证书颁发机构、失效日期等)。
-
服务器将自己的数字证书发送给客户端(公钥在证书里面,私钥由服务器持有)。
-
客户端收到数字证书之后,会验证证书的合法性。如果证书验证通过,就会生成一个随机的对称密钥,用证书的公钥加密。
-
客户端将公钥加密后的密钥发送到服务器。
-
服务器接收到客户端发来的密文密钥之后,用自己之前保留的私钥对其进行非对称解密,解密之后就得到客户端的密钥,然后用客户端密钥对返回数据进行对称加密,酱紫传输的数据都是密文啦。
-
服务器将加密后的密文返回到客户端。
-
客户端收到后,用自己的密钥对其进行对称解密,得到服务器返回的数据。
-
日常业务呢,数据传输加密这块的话,用https就可以啦,如果安全性要求较高的,比如登陆注册这些,需要传输密码的,密码就可以使用RSA等非对称加密算法,对密码加密。如果你的业务,安全性要求很高,你可以模拟https这个流程,对报文,再做一次加解密。
数按照相同的规则处理,再用相同的hash算法,和对应的密钥解密处理,以对比这个签名是否一致。
再举个例子,有些小伙伴是这么实现的,将所有非空参数(包含一个包AccessKey,唯一的开发者标识)按照升序,然后再拼接个SecretKey(这个仅作本地加密使用,不参与网络传输,它只是用作签名里面的),得到一个stringSignTemp的值,最后用MD5运算,得到sign。
服务端收到报文后,会校验,只有拥有合法的身份AccessKey和签名Sign正确,才放行。这样就解决了身份验证和参数篡改问题,如果请求参数被劫持,由于劫持者获取不到SecretKey(仅作本地加密使用,不参与网络传输),他就无法伪造合法的请求啦
04 HTTP 与 HTTPS 有哪些区别?
HTTP 是超文本传输协议,信息是明文传输,存在安全风险的问题。HTTPS 则解决 HTTP 不安全的缺陷,在 TCP 和 HTTP
网络层之间加入了 SSL/TLS 安全协议,使得报文能够加密传输。HTTP 连接建立相对简单, TCP 三次握手之后便可进行 HTTP 的报文传输。而 HTTPS 在 TCP 三次握手之后,还需进行
SSL/TLS 的握手过程,才可进入加密报文传输。HTTP 的端口号是 80,HTTPS 的端口号是 443。
HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的。
HTTPS 是如何解决上面的三个风险的?
混合加密的方式实现信息的机密性,解决了窃听的风险。aes+rsa aes加密信息,rsa加密aes秘钥
摘要算法的方式来实现完整性,它能够为数据生成独一无二的「指纹」,指纹用于校验数据的完整性,解决了篡改的风险。 类似签名
将服务器公钥放入到数字证书中,解决了冒充的风险。 官方CA机构申请证书
设计模式
装饰者模式
观察者模式
责任链模式
策略模式
接口安全
签名
1、线下分配appid和appsecret,针对不同的调用方分配不同的appid和appsecret
2、加入timestamp(时间戳),5分钟内数据有效
3、加入临时流水号 nonce(防止重复提交),至少为10位。针对查询接口,流水号只用于日志落地,便于后期日志核查。针对办理类接口需校验流水号在有效期内的唯一性,以避免重复请求。
4、加入签名字段signature,所有数据的签名信息。以上字段放在请求头中。
token spring security/jwt
浏览器输入url请求返回全过程
1.执行 DNS 域名解析;
2.封装 HTTP 请求数据包;
3.封装 TCP 请求数据包;
4.建立 TCP 连接(3 次握手);
5.参数从客户端传递到服务器端;
6.服务器端得到客户端参数之后,进行相应的业务处理,再将结果封装成 HTTP 包,返回给客户端;
7.服务器端和客户端的交互完成,断开 TCP 连接(4 次挥手);
8.浏览器通过自身执行引擎,渲染并展示最终结果给用户。
分布式锁
- redission
redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1);
如果存在就将可重入锁value +1
//加锁
if (redis.call('exists', KEYS[1]) == 0)
then redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil; end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1)
then redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil; end;
return redis.call('pttl', KEYS[1]);
//解锁
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0)
then return nil;end;
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0)
then redis.call('pexpire', KEYS[1], ARGV[2]); return 0;
else redis.call('del', KEYS[1]);
redis.call('publish', KEYS[2], ARGV[1]);
return 1; end;
return nil;
- 解锁首先判断KEYS[1]是否存在,存在将值减1,如果counter还大于0,就重新设置过期时间30000ms,否则就删除操作
可以看到删除过后还执行了一个publish命令,redis发布/订阅功能,通知订阅了这把锁的线程 - 看门狗30s(可设置)定时任务 每 30/3 一次,重新续时为30秒,