MQ
kafka的怎么保证不重复消费
1、kafka自带的消费机制
kafka有个offset的概念,当每个消息被写进去后,都有一个offset,代表他的序号,然后consumer消费该数据之后,隔一段时间,会把自己消费过的消息的offset提交一下,代表我已经消费过了。下次我要是重启,就会继续从上次消费到的offset来继续消费。
2、通过保证消息队列消费的幂等性来保证
数据要写库,你先根据主键查一下,如果数据有了,就别插入了,update一下好吧
写redis,那没问题了,反正每次都是set,天然幂等性
消息,我们可以建个表(专门存储消息消费记录)
生产者,发送消息前判断库中是否有记录(有记录说明已发送),没有记录,先入库,状态为待消费,然后发送消息并把主键id带上。
消费者,接收消息,通过主键ID查询记录表,判断消息状态是否已消费。若没消费过,则处理消息,处理完后,更新消息记录的状态为已消费。
Kafka如何保证消息的顺序性
消费者是单线程消费处理,而处理比较耗时的话,比如处理一条消息耗时几十 ms,那么 1 秒钟只能处理几十条消息,这吞吐量太低了。而多个线程并发跑的话,顺序可能就乱掉了。
解决方案:
一个 topic,一个 partition,一个 consumer,内部单线程消费,单线程吞吐量太低,一般不会用这个。
写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。
如何保证消息不丢失(rabbitMQ)
生产者保证
比如RabbitMQ,提供了confirm机制,也就是说消息由系统A发送到MQ,MQ接收到后会返回个ack告诉A,该消息收到了
这里有些人可能对这个ack会理解错误,以为是消费者收到消息后返回的结果,因为自己测试往往收到消息很快,这个MQ中转过程往往感受不到,这里特地提醒下,加深印象
MQ保证
RabbitMQ提供了一个持久化的机制,也就是接收到消息存放到磁盘,此时哪怕是宕机了,在恢复之后仍旧会继续消费
消费者保证
消费者B,同样有个ack机制来保证消费,当消费者收到MQ的消息后,默认会立即返回ack表示收到消息,但为了保证消息的正确消费,避免消费过程中宕机导致的消息丢失,我们可以进行手动的ack提交,也就是在完成消费后返回ack,这样就保证了消费者可靠
数据库
mysql高可用
1、mysql主从复制与读写分离
2、mysql+keepalived实现双主高可用
3、mysql分库分表
4、数据库中间件Mycat,sharingjdbc
mysql的索引
主键和唯一索引
1. 主键一定会创建唯一索引,但唯一索引不一定是主键
2. 主键不允许为空,唯一索引列允许为空
3. 一个表只能有一个主键,但可以有多个唯一索引(联合主键只能看成一个主键,因为是多个主键字段表示唯一行)
4. 主键可以被其他表引用为外键,唯一索引不行
5. 主键是一种约束,唯一索引是一种索引,是表的冗余数据结构,用来快速查找,两者有本质区别
索引类型:
Hash索引
优点
采用哈希表,对key值分散的等值查询速度较快
缺点
不支持范围查询,不支持多列索引的最左匹配规则,不支持索引排序,在大量重复的键值情况下效率较低,因为哈希碰撞问题
B+ Tree索引
优点
Hash索引的缺点就是它的优点
缺点
等值查询速度相对Hash索引会较慢点,一般都用B+索引
mysql性能优化
count(主键)往往是性能最优的
索引失效的情况
字符串不加单引号,会失效
使用的索引列不是复合索引列表中的第一部分,会失效
避免where子句中对字段进行null值判断,否则可能导致mysql放弃使用索引全表扫描
字段尽量加上默认值,比如0,''
尽量避免where子句中使用!=或<>操作符
避免where子句中使用or来连接条件(若or前的条件有索引,后面的列没有索引,那么涉及的索引都不会被用到),可以用union all来代替
避免在where条件中like以%打头
避免在where子句中对字段进行表达式操作
避免where子句中对=左边字段进行函数操作
开启mysql慢查询,配置慢查询时间,超过执行时间的语句将会记下来
需要事务处理的表使用innoDB,不需要的可以使用MyIsam,查询效率更高
group by后面的字段会默认进行排序,如果不想有这个损耗,可以用order by null来避免
重复执行相同的sql语句,传入参数中条件最好用外部传入,这样可以使用到mysql自己的缓存,比如where条件里包含日期字段=curdate(),这个curdate()最好外部传入,否则mysql每次都会执行一遍
SQL慢的原因
偶尔慢
数据库进行数据增删改操作会进行缓存,不会立即刷新到磁盘,当数据一直频繁增删改时,可能导致刷盘的频率加快,在刷新的时候会造成停顿延迟
遇到表锁或行锁,需要等待锁的释放,偶尔会慢
一直很慢
字段加了索引,但没有命中索引,有可能是多字段联合索引,a,b,c的顺序,但条件只用到b,这时可能索引不起作用
字段没加索引,数据量过大时造成全表查询,就慢了
java
JDK常用工具
jps
jps -lvm输出JVM运行进程
jstack
用来检测方法栈,比如检查内存溢出
1、top -Hp pid1查出最消耗CPU线程
2、printf "%x\n" pid2 对该线程进行16进制转换
3、jstack pid1 | grep pid2 查看可能出问题的栈信息
jmap
查看堆内存相关信息
jstat
查看gc相关信息
hashmap 是否线程安全 替代品
两个hashcode一样 equal等于?
equal的源码
参考文档:https://www.cnblogs.com/xudong-bupt/p/3960177.html
equal:
默认的equals方法,直接调用==,比较对象地址。
不同的子类,可以重写此方法,进行两个对象的equals的判断。
Object 中 直接使用==进行判断
String 中
(1)String类中的equals首先比较地址,如果是同一个对象的引用,可知对象相等,返回true。
(2)如果不是同一个对象,equals方法挨个比较两个字符串对象内的字符,只有完全相等才返回true,否则返回false。
hashcode:
hashCode是根类Obeject中的方法。
默认情况下,Object中的hashCode() 返回对象的32位jvm内存地址。也就是说如果对象不重写该方法,则返回相应对象的32为JVM内存地址。
list set map 区别
jvm java源码 spring源码
spring
IOC
依赖注入,本质是通过Spring帮你实例化对象并存储在容器中帮你管理Bean,当需要使用的时候进行对象注入
AOP
使用JDK动态代理或CGlib动态代理对需要进行切面处理的对象进行代理
ApplicationContext
是BeanFactory的子类,提供更多功能,比如AOP特性,载入多个上下文;容器启动时立即加载所有Bean
BeanFactory
负责Bean配置文档的读取,bean加载,实例化,维护Bean之间的依赖关系;采用延迟加载
springBean初始化过程
实例化BeanFactoryPostProcessor实现类
执行BeanFactoryPostProcessor的postProcessBeanFactory方法
实例化BeanPostProcessor实现类
实例化InstantiationAwareBeanPostProcessor的postProcessBeforeInstantiation方法执行Bean的构造器
执行InstantiationAwareBeanPostProcessor的postProcessPropertyValues方法为Bean注入属性
调用BeanNameAware的setBeanName方法
调用BeanFactoryAware的setBeanFactory方法
执行BeanPostProcessor的postProcessBeforeInitialization方法
调用InitializatingBean的afterPropertiesSet
调用bean中init-method指定的初始化方法
执行BeanPostProcessor的postProcessAfterInitialization方法
结束后,调用DiposibleBean的destory方法
调用bean的destory-method指定的方法
mybatis
缓存
一级缓存
mybatis的一级缓存是sqlSession级别的,它会创建一个私有的hashmap作为缓存容器
mybatis默认开启一级缓存
在同一个sqlSession中,执行两次相同的查询sql,第二次会从缓存读取;
在同一个sqlSession中,任何增删改都会导致缓存失效
当一个sqlSession结束后,缓存也会清除
二级缓存
mybatis二级缓存是mapper级别的,同一个mapper中,多个sqlSession共用同一个二级缓存区域
mybatis默认不开启二级缓存
在同一个mapper中,不同的sqlSession两次执行相同的sql语句,第二次之后也从缓存中读取
同一个mapper中,不同sqlSession执行任何增删改都会导致整个mapper二级缓存清除,所以对于存在大量增删改的数据表并不适合开启二级缓存
java8 stream
概念:
对一串数据流Stream的各方面处理
常用方法:
map:转换
filter:过滤
sorted:排序
foreach:遍历
Collectors
averagingInt 计算平均值
summarizingInt 统计数量,最大,最小,平均,总和
FlatMap:对象的多级处理,对象包含对象无线包含,流式处理
Reduce:将多个元素组合起来得到一个元素,多元素的复杂操作
Parallel:并行流计算,多线程运算
jvm
基本概念
JVM按区域划分可分为:堆,虚拟机栈,方法区,程序计数器,本地方法栈
程序计数器:每个线程当前执行的字节码指令的位置,所以每个线程都有自己的程序计数器,属线程私有的
虚拟机栈:用来存放局部变量,操作数栈,动态链接,方法出口等,更具体来讲,是因为线程每执行一个方法会创建一个栈帧压入栈,而栈帧中有前面说的这些东西,属线程私有的
堆:存放创建对象,线程共享
方法区:存放类的相关信息,常量,静态变量等,线程共享;jdk8以后用元空间(MetaSpace)取代,存储在物理内存(本地内存)
本地方法栈:和虚拟机栈类似,不过调用的是本地操作系统里面的底层类库,线程共享
垃圾回收算法
引用计数法:这个应该比较有名了,最简单但同时存在循环引用问题的算法,每个对象都包含一个引用计数器,有一个地方引用了就+1,引用失效-1,引用数为0表示可以垃圾回收了
可达性分析算法:目前流行的算法,通过GC Root对象作为起始点,从这些节点向下搜索,当一个对象到GC Root没有任何引用链相连时,则证明对象是不可用的
GC Root对象包括哪些?
1. 虚拟机栈中的引用对象
2. 方法区中的静态属性引用的对象
3. 方法区中的常量引用对象
4. 本地方法栈中JNI引用的对象
jvm细节
类何时被回收
1. 该类在堆中不存在实例对象
2. 该类的ClassLoader已被回收
3. 对该类的对象没有任何引用
方法中的对象何时被回收
当方法执行完成后,方法中定义的对象,需要等待垃圾回收才销毁
软引用和弱引用的区别
软引用在内存不够时才回收,内存足够不会回收;而弱引用,只能撑到下次垃圾回收之前就会被回收
新生代到老年代的条件
我们的一般印象是新生代GC到一定年龄后,对象才会进入老年代,但实际并不是只有这一种情况
当survivor区中相同年龄的对象之和超过survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到要求的最大年龄
JVM在什么情况下会加载一个类
简单的说就是在代码中用到这个类的时候
JVM类加载过程
1. 加载
内存中生成类的Class对象,作为方法区这个类的数据入口
2. 验证
确保Class文件字节流包含信息符合虚拟机要求
3. 准备
为类变量分配内存并设置初始值,这里的初始值指的基本类型的初始值,比如int类型初始值0
4. 解析
将常量池中的符号引用替换为直接引用的过程
5. 初始化
真正执行类中定义的Java程序代码,执行构造器方法
6. 使用
使用对象处理业务逻辑
7. 卸载
销毁对象类
类加载器
Bootstrap classloader启动类加载器,属于根加载器,加载jdk的JAVA_HOME\lib目录下类
Extension classloader扩展类加载器,加载JAVA_HOME\lib\ext扩展类
Application Classloader应用加载器,负责加载用户路径classpath上的类库
JVM双亲委派模型
当一个类加载器收到类加载任务,会先交给其父类加载器去完成,最终会把任务传到启动类加载器,只有当父类加载器无法完成加载任务是,才会交给其子类
保证了可信任类的优先级,比如用户自己建了一个java.lang.Object对象,该机制会保证加载系统认可的lib包下的对象,保证系统安全性
jvm调优
1、堆设置主要设置初始堆内存,最大堆内存,年轻代和年老代比例,年轻代中Eden区和Survivor区的比值
2、年轻代过小会导致Minor GC过于频繁,影响系统性能;年老代过小会导致内存溢出;所以要根据业务需求适当配比年轻代和老年代
3、垃圾收集器也可以根据实际情况进行适当的选择,现在常用的为CMS(追求最短时间停顿)和G1收集器(适用大容量内存(大于4G))
多线程
synchronized Lock 区别
synchronize是JVM内置关键字,lock是Java类
synchroinze锁可重入,不可中断,不公平,Lock可重入,可中断,可公平
synchronize可自动释放,Lock需要手动释放
Lock提供了Condition可控制notify哪个线程,synchroinze不能针对唤醒
总结:一般可以使用synchronize的地方,用它更方便;Lock使用更加灵活,在需要灵活中断获取锁,唤醒特定线程等场合可以用Lock
volatile关键字
1、保持内存可见性
所有线程都能看到共享内存的最新状态
2、防止指令重排序(有序性)
在单例模式中,用右侧的代码创建单例,仍可能造成问题,获得的是未初始化的对象,原因就是指令重排序,先分配了对象的内存地址,再初始化对象,还没初始化对象前,对象可能已经不为null了,这种情况就可以用volatile解决(建议看下前面的链接)
3、不能保证原子性
如何保证线程顺序
最基础的join
使用锁等待机制,获取同一把锁,设置标志位,后续的await
CountDownLatch计数器,当设置多个时,进行await,类似于设置了标志位
线程建立的几种方式
1、实现Runnable接口
创建单个线程的常用方式,实现接口即可
2、继承Thread
通过继承方式实现的线程,由于java的单一继承性,这种方式有局限性
3、Callable+FutureTask
一个线程执行有返回结果
4、Callable+ExecutorService+Future
多线程执行有返回结果的任务
线程池参数
corePoolSize
线程的核心线程数,当任务到来时,若创建的线程数量还未达到核心线程数,则会创建新的线程来处理
当任务到来时,若创建的线程数大于等于核心线程数且小于最大线程数,则只有当workQueue满是才创建新的线程去处理任务
当corePoolSize和maximumPoolSize相同,则创建的线程池大小固定,判断workQueue未满则加入workQueue中等待空闲线程去处理
当运行的线程数等于maximumPoolSize,且workQueue已经满了,则通过handler所指定的策略来处理任务
maximumPoolSize:最大线程数
workQueue:等待队列,当任务提交时,线程池所有线程正忙,会将该任务加入worker队列中等待
KeepAliveTime:线程池维护线程所允许的空闲时间,这个时间针对的是超过corePoolSize数量的线程,它们执行完线程不会立即销毁,直到超过KeepAliveTime所指定的时间
threadFactory:用来创建线程,默认创建的线程拥有相同的优先级且是非守护线程,同时设置线程的名称
handler
概述:该参数用来表示超过线程池执行任务限制的任务,我们所采取的策略,有四种实现策略
AbortPolicy:直接抛出异常,默认策略
CallerRunsPolicy:用调用者所在的线程来执行任务
DiscardOldesPolicy:丢弃阻塞队列中靠前的任务,并执行当前任务
DiscardPolicy:直接丢弃任务
线程池优点
降低资源消耗,重复利用已创建的线程降低线程创建和销毁造成的消耗
提高响应速度,任务到达是不需要等待线程创建就能立即执行
提高线程的可管理性,线程是稀缺资源不能无限制创建,通过线程池统一分配可以调优和监控
线程池的关闭
shutdown
一般情况下,我们使用shutdown是没什么问题的,当如果你存有永久循环处理线程,那就要注意了,shutdown是关不了这种永久while线程的
它的解释说明是:线程池拒接收新提交的任务,同时等待线程池里的任务执行完毕后关闭线程池
shutdownNow
它的解释是:线程池拒接收新提交的任务,同时立马关闭线程池,线程池里的任务不再执行
实际执行过程是,当执行shutdownNow方法,它会取出剩下所有没有执行的任务队列并返回,而线程池是在不断循环任务队列的;此时又有两种情况,线程池正取到的任务阻塞,那么会直接抛出异常,和线程中断抛异常一样的逻辑处理;如果线程在执行状态,则会处理完当前任务然后继续循环,判断到任务为null了退出
上面的我继续概括:执行该方法后,会删除所有未执行任务,正在执行的任务如果是阻塞的,则抛异常,否则置中断状态后,继续执行完成当前任务后退出;
相同
shutdown和shutdownNow都是异步执行的
不同
shutdownNow会强制置所有工作线程置中断状态,中断导致的后果需要通过自己代码判断isinterrupt处理;
而shutdown需要trylock尝试获取锁,获取不到则不会中断
结论
当使用shutdwonNow需要捕获处理可能抛出的中断异常;当使用shutdown需要保证线程任务没有永久阻塞的逻辑,否则线程关闭不了
锁的分类
死锁:多线程会遇到的问题,两个线程同时争抢同一把锁造成死循环
悲观锁:悲观锁认为同一个数据并发操作一定会发生修改,所以要对一个数据的并发操作加锁,悲观锁是比较重量级的
乐观锁:乐观锁认为同一个数据的并发操作是不会发生修改的,可以通过版本机制,当更新数据的时候会进行不断尝试修改
自旋锁:自旋锁是指尝试获取锁的线程不会立即阻塞,二是会采用循环的方式不断尝试获取锁;好处就是减少了线程上下文的切换,缺点是消耗CPU,实际使用还是要根据实际情况
可重入锁:可重入锁指在同一个线程外层方法获取锁后,内层方法加同一个锁会自动获取锁
偏向锁:偏向锁指的锁的一种状态,当一段同步代码一直被同一个线程所访问的时候,那么线程会自动获取锁,减少获取锁的消耗
轻量级锁:当偏向锁被另一个线程所访问时,偏向锁会升级为轻量级锁,也就是自旋锁的方式,不断尝试获取,期待另一个线程马上释放锁
重量级锁:当轻量级锁一直自旋下去到一定时候还没有获取到锁,锁会升级为重量级锁,进入阻塞状态
分布式
CAP理论
C(Consistency):一致性,表示所有节点上的数据必须保持同步
A(Availability):可用性,所有请求必定会得到响应,但不保证正确性
P(Partition tolerance):分区容错性,系统应该持续提供服务,即使系统内部有服务挂了,不会彻底死机
cap理论指出一个分布式系统不可能同时满足一致性、可用性和分区容错性,这三个要求只能同时满足其中两项
BASE理论
概述:BASE全称Basically Available(基本可用),Soft state(软状态)和Eventually consistent(最终一致性)
Basically Available(基本可用)
在分布式系统出现故障时,允许瞬时部分可用性
比如数据库分片,其中一片挂了,剩余仍可以使用
比如当大流量访问时,只允许部分用户访问,其它用户降级处理
Soft state(软状态)
表示数据存在中间状态,允许在不同节点的数据副本在同步过程中存在延迟
比如支付过程,待支付,支付中,支付成功,支付失败,那么支付中就是个中间状态,在成功之后同步状态存在一定延迟
Eventually consistent(最终一致性)
表示所有数据副本在一段时间的同步后最终达到一致的状态
一致性模型数据的一致性模型可以分成以下 3 类:
强一致性: 数据更新成功后,任意时刻所有副本中的数据都是一致的,一般采用同步方式实现
弱一致性:数据更新成功后,系统不承诺立即可以读到最新写入的值,也不承诺具体多久后可读到
最终一致性:弱一致性的一种形式,数据更新成功后,系统不承诺立即可以返回最新写入的值,但是保证最终会返回上一次更新操作的值。
分布式锁
作用:保证分布式环境下的顺序执行,分布式环境下的锁机制
Redis
实现方式:流行用setnx方法,设置成功则获取锁,redis可以设置过期时间;建议使用Redisson
缺点:第一是在setnx方法之后,设过期时间之前redis宕机仍然可能死锁,且过期时间不好定;第二redis通常都是主从部署,可以看出redis是AP模型,而分布式锁需要实现CP模型,明显是不可实现的,具体表现在,如果在主节点A获取到了锁,此时还没同步到从节点B,主节点宕机,那么B从节点会变成主节点,此时其他服务是又可以获取分布式锁的
zookeeper:
实现方式:zk通过添加同名节点成功者获取锁的方式实现,使用它的临时节点方式,可以在一个会话结束后自动释放节点,也就不存在需要定时释放的问题,哪怕程序异常断开了连接,节点没了会话维持也会自动失效
优点:zk可以创建顺序节点,使用zk的watch机制,可以实现公平锁
缺点:zk的强一致性使得在zk节点越多的情况下,获取分布式锁越慢,且在存在leader机器宕机的时候,zk的ZAB一致性协议会使zk集群暂时无法提供服务,直到选出leader,并恢复数据
数据库:通过表主键判断,插入成功获取锁,缺点明显,数据库IO是比较消耗性能的,不可重入,不可定时释放
全局ID生成方案
snowflow
优点
速度快,直接内存操作生成
不需要依赖第三方接口
实现简单
缺点
只能趋势递增(考虑如果是订单,可以通过订单id来推测订单量)
依赖机器时间,如果机器时间回拨可能导致生成id重复
Leaf美团点评分布式ID生成系统
Leaf-segment数据库方案:每次去数据库获取一个号段的值,用完再去获取
Leaf-snowflake: 解决时钟回拨问题
uid-generator百度:每次取回一批id自己用
session共享
1、使用nginx的ip_hash策略保证同一个用户始终访问同一个服务器
2、使用数据库存放session,可以保证session持久化,不过在微服务情况下,各个微服务有自己的数据库这就不好用了
3、cookie存储session,这样每次请求都会携带session信息,消耗资源,不推荐
4、redis存储session,可以使用spring-session框架,它重写了httpSession
分布式消息通信
MQ是我们常用到的消息中间件,用来传递消息,但为什么要用MQ,它又有什么问题?
优点:
1、异步处理
2、解耦
3、削峰
缺点:
系统可用性降低
多加一个中间件,带来的问题是如果MQ挂了怎么办,MQ挂了数据就没了,系统就不可运行了
系统的复杂度提升
引入MQ就需要考虑消息重复消费,消息丢失,消息的顺序问题等等,为解决这些问题可能又要引入新的机制
消息的一致性问题
RabbitMQ
四种通信模式
1. 链接包含最简洁的四幅图 2. 四种模式需要知道一个最基本的概念,一个队列对应了一个客户端或者说客户端中的一个消费者; 3. 不要多个消费者订阅一个队列,这样任何模式都只能消费一次
topic
主题订阅模式,消息经由交换机通过routing key绑定到队列,需要注意一条消息只能消费一次
fanout
广播模式,消息经由交换机直接发向和交换机绑定的所有队列,routing key无效
direct
直连模式,这种模式也可以说是种默认模式,当你连交换机都懒得创建的时候,这种模式使用的是默认交换机,它和fanout模式的区别除了交换机不同,就是当向该交换机发送一条消息时,与它绑定的队列只有一个能消费到
header
这种几乎用不到,它是通过请求信息中,请求头包含的信息进行消息区分
Kafka:高吞吐低延迟mq,适用用大数据量的流式传输,如日志传输,用户活动跟踪,运营指标等
分布式缓存
基本数据类型
string:常用kv结构,用来缓存数据
hash: 将信息凝聚在一个key中,可以进行统一的删除,缺点是设置过期时间只能在key上,不能对key对应的数据设置过期;优点是避免了命名冲突,减少内存消耗
set:无重复数据的集合,可用作抽奖,存放点赞的人,可以进行交集,并集,差集的计算;可以用来实现共同关注的人,可能认识的人等功能;甚至可以用来对账
zset:有序的无重复数据集合,可以用在时事热点信息的排序上
list:可以用作消息列表使用
常见问题
缓存穿透
概述:大量请求同一个数据,由于数据不存在,请求都访问数据库,导致数据库崩掉
解决办法
缓存不存在时,数据库返回null值也存入缓存,之后直接通过缓存返回null的处理办法
缓存雪崩
概述:大量缓存在同一时间失效,请求都打到数据库,导致数据库崩掉
解决办法
并发压力大通过加锁或队列,当缓存失效时,对某个key只允许一条线程访问,其它等待
缓存失效时间设置不同,可以在一个时间范围乘个随机数,尽量分布均匀
加二级缓存,二级缓存失效时间大于一级缓存,当一级缓存失效,二级缓存可以起到作用
如果能预计到某个时间点会有大量并发操作,可以设计手动reload缓存
并发压力大通过加锁或队列,当缓存失效时,对某个key只允许一条线程访问,其它等待
缓存击穿
概述:单个key访问流量很大,在它失效时,大量请求打到数据库
解决办法:类似缓存雪崩
缓存预热:系统启动时对热点数据进行缓存主动加载
分布式事务
事务
特点:
原子性(Atomicity)
事务是个不可分割的**最小执行单位**,执行的所有动作要不全部完成,要不都不起作用
一致性(Consistency)
事务前后保证**业务逻辑上的数据一致性**,比如A转账200给B,事务完成后B一定+200,A一定-200
隔离性(Isolation)
并发环境中,不同的事务操作相同的数据,每个事务都有自己的完整数据空间,**不会相互干扰**
持久性(Durability)
事务一旦被提交,会**永久保存到数据库**中,即使系统崩溃,重启后还能恢复到事务成功后的状态
并发事务问题:
脏读
丢失修改
不可重复读
幻读
两阶段提交(2pc,准备阶段、提交阶段)
准备阶段:协调者询问参与者事务是否执行成功,参与者发回事务执行结果
提交阶段:如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。
缺点:
1、同步阻塞 所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。
2、单点问题 协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响。特别是在阶段二发生故障,所有参与者会一直等待状态,无法完成其它操作。
3、数据不一致 在阶段二,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
4、太过保守 任意一个节点失败就会导致整个事务失败,没有完善的容错机制。
补偿事务(TCC)
核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:
1、Try 阶段主要是对业务系统做检测及资源预留
2、Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
3、Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
优点: 跟2PC比起来,实现以及流程相对简单了一些,但数据的一致性比2PC也要差一些
缺点: 缺点还是比较明显的,在2,3步中都有可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。
本地消息表(异步确保)
核心思想是:本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。
1、在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。
2、之后将本地消息表中的消息转发到 Kafka 等消息队列中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。
3、在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。
优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。
缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。
mq事务消息
有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。
技巧:
特别严格的场景,用的是TCC来保证强一致性;然后其他的一些场景基于了阿里的RocketMQ来实现了分布式事务
99%的分布式接口调用,不要做分布式事务,直接就是监控(发邮件、发短信)、记录日志(一旦出错,完整的日志)、事后快速的定位、排查和出解决方案、修复数据。每个月,每隔几个月,都会对少量的因为代码bug,导致出错的数据,进行人工的修复数据,自己临时动手写个程序,可能要补一些数据,可能要删除一些数据,可能要修改一些字段的值。权衡,要用分布式事务的时候,一定是有成本,代码会很复杂,开发很长时间,性能和吞吐量下跌,系统更加复杂更加脆弱反而更加容易出bug;好处,如果做好了,TCC、可靠消息最终一致性方案,一定可以100%保证你那快数据不会出错。
微服务(springcloud)
注册中心
服务网关
配置中心
负载均衡
服务熔断
监控
Eureka:注册中心,管理协调分布式集群信息,类似的有zookeeper,Nacos等
Zuul:服务网关
Fegin:RPC远程调用技术,使用的http协议,引入Feign后自带了Ribbon负载均衡的引入,默认轮询
Apollo:配置中心
Ribbon:负载均衡组件,主要有权重和轮询这两种模式
Hystrix:熔断降级限流组件
容错模式
超时: 主动超时
限流: 限制最大并发数
熔断: 错误数达到阈值时,类似保险丝熔断
隔离: 隔离不同的依赖调用
降级: 服务降级
隔离模式
线程池模式
信号量模式
设计模式
设计原则
单一职责原则(SRP)
定义一个类,应该只有一个引起它变化的原因,该原因就是职责
开闭原则(OCP)
对扩展开放,对修改封闭
里氏替换原则(LSP)
任何基类可以出现的地方,其子类一定可以出现
接口隔离原则(ISP)
使用多个隔离的接口,比使用单个接口好,可以降低类间的耦合性
依赖倒置原则(DIP)
要求调用者和被调用者都是依赖抽象,两者没有直接的关联和接触,一方变动不会影响另一方,强调了抽象的重要性。
一句话就是依赖于抽象而不依赖于具体
迪米特原则(最少知道原则)
一个实体类尽量少的和其它实体产生依赖,使模块功能独立
合成/聚合复用(CARP)
类的继承导致的耦合性也会比较高,比如父类接口增加一个方法,会导致所有子类编译出错;
但如果只是通过组合聚合,引用类的方法,就可以降低风险,同时实现复用
简单工厂模式
适用场景
- 工厂类负责创建的对象比较少。
- 客户只知道传入工厂类的参数,对于如何创建对象(逻辑)不关心。
- 由于简单工厂很容易违反高内聚责任分配原则,因此一般只在很简单的情况下应用。
优点
- 工厂类是整个模式的关键。包含了必要的逻辑判断,根据外界给定的信息,决定究竟应该创建哪个具体类的对象。
- 通过使用工厂类,外界可以从直接创建具体产品对象的尴尬局面摆脱出来,仅仅需要负责“消费”对象就可以了。而不必管这些对象究竟如何创建及如何组织的。明确了各自的职责和权利,有利于整个软件体系结构的优化。
缺点
- 由于工厂类集中了所有实例的创建逻辑,违反了高内聚责任分配原则,将全部创建逻辑集中到了一个工厂类中,它所能创建的类只能是事先考虑到的,如果需要添加新的类,则就需要改变工厂类了。
- 当系统中的具体产品类不断增多时候,可能会出现要求工厂类根据不同条件创建不同实例的需求。这种对条件的判断和对具体产品类型的判断交错在一起,很难避免模块功能的蔓延,对系统的维护和扩展非常不利。
- 这些缺点在工厂方法模式中得到了一定的克服。
策略模式
适用场景
- 多个类有不同的表现形式,每种表现形式可以独立成单独的算法。
- 需要再不同情况下使用不同的算法,以后算法可能还会增加。
- 对用户隐藏算法逻辑。
优点
- 每个算法单独封装,减少了算法和算法调用者的耦合。
- 合理使用继承有助于提取出算法中的公共部分。
- 简化了单元测试。
缺点
- 策略模式只适用于客户端知道所有的算法或行为的情况。
- 策略模式造成很多的策略类,每个具体策略类都会产生一个新类。不过可以使用享元模式来减少对象的数量。
装饰模式
适用场景
- 需要扩展一个类的功能,或给一个类添加附加职责。
- 需要动态的给一个对象添加功能,这些功能可以再动态的撤销。
- 需要增加由一些基本功能的排列组合而产生的非常大量的功能,从而使继承关系变的不现实。
- 当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。
优点
- Decorator模式与继承关系的目的都是要扩展对象的功能,但是Decorator可以提供比继承更多的灵活性。
- 通过使用不同的具体装饰类以及这些装饰类的排列组合,设计师可以创造出很多不同行为的组合。
缺点
- 这种比继承更加灵活机动的特性,也同时意味着更加多的复杂性。
- 装饰模式会导致设计中出现许多小类,如果过度使用,会使程序变得很复杂。
- 装饰模式是针对抽象组件(Component)类型编程。但是,如果你要针对具体组件编程时,就应该重新思考你的应用架构,以及装饰者是否合适。当然也可以改变Component接口,增加新的公开的行为,实现“半透明”的装饰者模式。在实际项目中要做出最佳选择。
代理模式
适用场景
- 远程代理,为一个对象在不同的地址空间提供局部代表,这样就可以隐藏一个对象存在于不同地址空间的事实。
- 虚拟代理,是根据需要创建开销很大的对象。通过它来存放实例化需要很长时间的真是对象。
- 安全代理,用来控制真实对象访问时的权限。
- 智能指引,是指当调用真是的对象时,代理处理另外的一些事情。
优点
- 职责清晰,真实的角色就是实现实际的业务逻辑,不用关心其他非本职责的事务,通过后期的代理完成一件完成事务,附带的结果就是编程简洁清晰。
- 代理对象可以在客户端和目标对象之间起到中介的作用,这样起到了中介的作用和保护了目标对象的作用。
- 高扩展性
缺点
- 在客户端和目标对象增加一个代理对象,会造成请求处理速度变慢。
- 增加了系统的复杂度。
工厂方法模式
适用场景
- 工厂方法模式是new一个对象的替代品,所以在所有需要生成对象的地方都可以使用,但是需要慎重地考虑是否要增加一个工厂类进行管理,增加代码的复杂度。
- 需要灵活的、可扩展的框架时,可以考虑采用工厂方法模式。
- 工厂方法模式可以用在异构项目中,例如通过WebService与一个非Java的项目交互,虽然WebService号称是可以做到异构系统的同构化,但是在实际的开发中,还是会碰到很多问题,如类型问题、WSDL文件的支持问题,等等,从WSDL中产生的对象都认为是一个产品,然后由一个具体的工厂类进行管理,减少与外围系统的耦合。
- 可以使用在测试驱动开发的框架下,例如,测试一个类A,就需要把与类A有关联关系的类B也同时产生出来,我们可以使用工厂方法模式把类B虚拟出来,避免类A与类B的耦合。目前由于JMock和EasyMock的诞生,该使用场景已经弱化了,读者可以在遇到此种情况时直接考虑使用JMock或EasyMock。
优点
- 良好的封装性,代码结构清晰,减少模块间的耦合。
- 工厂方法模式的扩展性非常优秀。
- 屏蔽产品类。
- 工厂方法模式是典型的解耦框架。
缺点
- 使用者必须知道相应工厂的存在。
- 每次增加一个产品时,都需要增加一个具体类和对象实现工厂,是的系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。
原型模式
适用场景
- 某些结构复杂的对象的创建工作;由于需求的变化,这些对象经常面临着剧烈的变化,但是他们却拥有比较稳定一致的接口。
- 一般在初始化的信息不发生变化的情况下,克隆是最好的方法。
优点
- 隐藏了对象创建的细节。
- 提高了性能。
- 不用重新初始化,动态获得对象运行时的状态。
缺点
- 适用性不是很广。
- 每一个类必须配备一个克隆方法。
- 配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。
模板方法模式
适用场景
- 适用于子类中有重复的代码,可以把重复代码提取出来,放到父类中。
优点
- 提高代码复用性。
- 帮助子类摆脱重复的不变行为。
缺点
- 考虑不全面统一出现问题。
架构设计
高可用
避免单点故障
负载均衡技术
failover快速失败
选址DNS
硬件负载:F5
软件负载: lvs、nginx、HAProxy
去中心化的软件负载
热备(Linux HA)
多机房(同城灾备、异地灾备)
应用高可用
故障监控,自动预警
CPU、内存监控
链路监控
日志监控
应用容错设计、自我保护
服务降级、限流、快速失败等
数据量
数据分片
读写分离
可伸缩设计
垂直伸缩: 比如业务垂直拆分成多个微服务,优先考虑
水平伸缩:比如数据库单表数据量过大,根据业务条件水平拆表到多个表
提升硬件性能
增加服务器
提升硬件性能
CDN: