反射的概述
Java反射的原理:java类的执行需要经历以下过程,
编译:.java文件编译后生成.class字节码文件
加载:类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例
连接:细分三步
验证:格式(class文件规范) 语义(final类是否有子类) 操作
准备:静态变量赋初值和内存空间,final修饰的内存空间直接赋原值,此处不是用户指定的初值。
解析:符号引用转化为直接引用,分配地址
初始化:有父类先初始化父类,然后初始化自己;将static修饰代码执行一遍,如果是静态变量,则用用户指定值覆盖原有初值;如果是代码块,则执行一遍操作。
Java的反射就是利用上面第二步加载到jvm中的.class文件来进行操作的。.class文件中包含java类的所有信息,当你不知道某个类具体信息时,可以使用反射获取class,然后进行各种操作。
Java反射就是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;并且能改变它的属性。总结说:反射就是把java类中的各种成分映射成一个个的Java对象,并且可以进行操作。
线程的创建方式?你喜欢哪种?为什么
方式一:继承Thread类
方式二:实现Runnable接口
方式三:实现Callable接口 --JDK5.0新增
@PathVariable @RequestParam 和 @RequestBody 的区别
@RequestParam
1.用于将请求参数区数据映射到功能处理方法的参数上,接受的参数是来自requestHeader中,即请求头,通常用于GET请求,也可以用于post,delete等请求。
2.用来处理Content-Type: 为 application/x-www-form-urlencoded编码的内容。
3.@RequestParam 有三个属性:
(1)value:请求参数名(必须配置)
(2)required:是否必需,默认 true,即请求中必须包含该参数,如果没有包含,将会抛出异常(可选配置)
(3)defaultValue:默认值,如果设置了该值,required 将自动设为 false,无论你是否配置了required,配置了什么值,都是 false(可选配置)
@RequestBody
1.@RequestBody 映射请求到方法体上,接受的参数是来自requestBody中,即请求体。
2.用来处理非Content-Type: 为 application/x-www-form-urlencoded编码的内容。例如:application/json、application/xml等类型的数据。
3.@RequestBody注解常用于接收json格式的数据,并将其转换成对应的数据类型。
@PathVariable
1.@PathVariable 可以将 URL 中占位符参数绑定到控制器处理方法的入参中,获取请求路径中的变量作为参数
- 带占位符的 URL 是 Spring3.0 新增的功能
为什么HashMap线程不安全?(jdk7版本)
HashMap会进行resize操作,在resize操作的时候会造成线程不安全。下面将列举可能出现线程不安全的地方。
1、put的时候导致的多线程数据不一致。 这个问题比较好想象,比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。
下面的代码是resize的核心内容:
这是jdk7的实现方式,jdk8不是这样的。
// 这个方法的功能是将原来的记录重新计算在新桶的位置,然后迁移过去。
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
为什么HashMap是不安全的?(jdk8版本)
根据上面JDK1.7出现的问题,在JDK1.8中已经得到了很好的解决,如果你去阅读1.8的源码会发现找不到transfer函数,因为JDK1.8直接在resize函数中完成了数据迁移。另外说一句,JDK1.8在进行元素插入时使用的是尾插法。
为什么说JDK1.8会出现数据覆盖的情况喃,我们来看一下下面这段JDK1.8中的put操作代码:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) // 如果没有hash碰撞则直接插入元素
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
其中第六行代码是判断是否出现hash碰撞,假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。
总结: HashMap的线程不安全主要体现在下面两个方面:
1.在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。
2.在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。
@Autowired
@Autowired由spring框架定义,用于描述类中属性或相关方法(例如构造方法)。Spring框架在项目运行时假如发现由他管理的Bean对象中有使用@Autowired注解描述的属性或方法,可以按照指定规则为属性赋值(DI)。其基本规则是:首先要检测容器中是否有与属性或方法参数类型相匹配的对象,假如有并且只有一个则直接注入。其次,假如检测到有多个,还会按照@Autowired描述的属性或方法参数名查找是否有名字匹配的对象,有则直接注入,没有则抛出异常。最后,假如我们有明确要求,必须要注入类型为指定类型,名字为指定名字的对象还可以使用@Qualifier注解对其属性或参数进行描述(此注解必须配合@Autowired注解使用)。
单点登录业务实现原理
实现步骤:
1.用户输入用户名和密码之后点击登录按钮开始进行登录操作.
2.JT-WEB向JT-SSO发送请求,完成数据校验
3.当JT-SSO获取数据信息之后,完成用户的校验,如果校验通过则将用户信息转化为json.并且动态生成UUID.将数据保存到redis中. 并且返回值uuid.
如果校验不存在时,直接返回"不存在"即可.
4.JT-SSO将数据返回给JT-WEB服务器.
5.如果登录成功,则将用户UUID保存到客户端的cookie中.
自动配置原理
@SpringBootApplication是@Configuration、@EnableAutoConfiguration和@ComponentScan的组合
@Configuration表示该类是Java配置类。
@ComponentScan开启自动扫描符合条件的bean(添加了@Controller、@Service等注解)。
@EnableAutoConfiguration会根据类路径中的jar依赖为项目进行自动配置,比如添加了spring-boot-starter-web依赖,会自动添加Tomcat和Spring MVC的依赖,然后Spring Boot会对Tomcat和Spring MVC进行自动配置(spring.factories EnableAutoConfiguration配置了WebMvcAutoConfiguration)。
EnableAutoConfiguration主要@AutoConfigurationPackage,@Import(EnableAutoConfigurationImportSelector.class)这两个注解组成的。
@AutoConfigurationPackage用于将启动类所在的包里面的所有组件注册到spring容器。
@Import 将EnableAutoConfigurationImportSelector注入到spring容器中,EnableAutoConfigurationImportSelector通过SpringFactoriesLoader从类路径下去读取META-INF/spring.factories文件信息,此文件中有一个key为org.springframework.boot.autoconfigure.EnableAutoConfiguration,定义了一组需要自动配置的bean。
根据xxxAutoConfiguration上的@ConditionalOnClass等条件判断是否加载,符合条件才会将相应的组件被加载到spring容器。
Spring事务中的传播行为
事务传播行为是为了解决业务层方法之间互相调用的事务问题。
正确的事务传播行为可能得值如下
TransactionDefinition.PROPAGATION_REQUIRED
使用最多的一个事务传播行为,@Transactional注解默认使用这个传播行为。如果当前存在事务,则加入该事物,如果没有事务则创建新的事物。- TransactionDefinition.PROPAGATION_REQUIRES_NEW
创建一个新的事物,如果当前存在事物,则把当前事务挂起。开启的事务互相独立,互不干扰。
- TransactionDefinition.PROPAGATION_NESTED
如果当前存在事务,则创建一个事物作为当前事务的嵌套事务运行。没有则创建一个新的事物。 - TransactionDefinition.PROPAGATION_MANDATORY
如果当前存在事务,则加入该事物,如果当前没有事务,则抛出异常。
若是错误的配置以下3种事务传播行为,事务不会发生回滚:
- TransactionDefinition.PROPAGATION_SUPPORTS 如果当前存在事务,则加入该事物,如果当前没有事务,则以非事务的方式继续运行。
- TransactionDefinition.PROPAGATION_NOT_SUPPORTED 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- TransactionDefinition.PROPAGATION_NEVER 以非事务方式运行,如果当前存在事务,则抛出异常。
索引是什么?有什么作用以及优缺点?以及常见的几种索引
1、索引是对数据库表中一或多个列的值进行排序的结构,是帮助MySQL高效获取数据的数据结构
2、索引就是加快检索表中数据的方法。数据库的索引类似于书籍的索引。在书籍中,索引允许用户不必翻阅完整个书就能迅速地找到所需要的信息。在数据库中,索引也允许数据库程序迅速地找到表中的数据,而不必扫描整个数据库。
MySQL数据库几个基本的索引类型:普通索引、唯一索引、主键索引、全文索引
1、索引加快数据库的检索速度
2、索引降低了插入、删除、修改等维护任务的速度
3、唯一索引可以确保每一行数据的唯一性
4、通过使用索引,可以在查询的过程中使用优化隐藏器,提高系统的性能、索引需要占物理和数据空间
主键索引
: 数据列不允许重复,不允许为NULL,一个表只能有一个主键。唯一索引
: 数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。普通索引
: 基本的索引类型,没有唯一性的限制,允许为NULL值。全文索引
: 是目前搜索引擎使用的一种关键技术。
MySQL索引使用的数据结构主要有BTree索引 和 哈希索引 。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。
MySQL的BTree索引使用的是B树中的B+Tree,但对于主要的两种存储引擎的实现方式是不同的。
- MyISAM: B+Tree叶节点的data域存放的是数据记录的地址。在索引检索的时候,首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”。
- InnoDB: 其数据文件本身就是索引文件。相比MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按B+Tree组织的一个索引结构,树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。这被称为“聚簇索引(或聚集索引)”。而其余的索引都作为辅助索引,辅助索引的data域存储相应记录主键的值而不是地址,这也是和MyISAM不同的地方。在根据主索引搜索时,直接找到key所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。 因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。
MyISAM 和 InnoDB 的区别
- 是否支持行级锁:MyISAM 只有表级锁,而InnoDB支持行级锁和表级锁,默认为行级锁。
- 是否支持事物和崩溃后的安全恢复:MyISAM强调的是性能,,每次查询具有原子性,其执行速度比 InnoDB 类型更快,但是不提供事务支持。但是InnoDB提供事物支持,外部键等高级数据库问题。
- 是否支持外键:MyISAM 不支持,而 InnoDB支持
- 是否支持MVCC:仅InnoDB支持。应对高并发事务, MVCC比单纯的加锁更高效;MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作;MVCC可以使用 乐观(optimistic)锁 和 悲观(pessimistic)锁来实现;各数据库中MVCC实现并不统一。
B树和B+树两者有何异同
- B树的所有节点既存放键(key)也存放数据(data),而B+树只有叶子节点存放key和data,其它节点只存放key。
- B树的叶子节点都是独立的,B+树的叶子节点有一条引用链指向与它相邻的叶子节点。
为什么建议InnoDB表必须建主键,并且推荐使用整型的自增主键?
InnoDB索引文件是根据主键来进行维护的,如果没有建立主键,会默认查找字段建立唯一索引来维护索引文件,如果没有字段唯一则会维护类似ROWID建立索引文件。会降低MySQL性能。
相比于字符串,整型比较大小速度快,存储的空间也少。如果不使用自增,可能导致节点分裂,需要重新梳理B+树,增加维护时间。
并发事务带来的问题
- 脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
- 丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
- 不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
- 幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
不可重复读和幻读区别:
不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者删除比如多次读取一条记录发现记录增多或减少了。
数据库的隔离级别
READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读、不可重复读。
READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是还会导致幻读、和不可重复读。
REPEATABLE-READ(可重复读): 对同一字段的多次读取结果是一致的,除非数据是被本身事务所修改,可以阻止脏读和不可重复读,但是会导致幻读。
SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别,所有的事物依次逐个执行,这样事物之间就可以互相不产生干扰,该级别可以阻止脏读、幻读和不可重复读。
MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)。我们可以通过 SELECT @@tx_isolation; 命令来查看,MySQL 8.0 该命令改为 SELECT @@transaction_isolation;
InnoDB储存引擎在 REPEATABLE-READ(可重复读) 事务隔离级别下使用的是 Next-Key Lock 锁算法,因此可以完全避免幻读这与其他数据库系统(如 SQL Server) 是不同的。所以说InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 已经可以完全保证事务的隔离性要求,即达到了 SQL标准的 SERIALIZABLE(可串行化) 隔离级别。因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 READ-COMMITTED(读取提交内容) ,但是你要知道的是InnoDB 存储引擎默认使用 REPEAaTABLE-READ(可重读) 并不会有任何性能损失。
InnoDB 存储引擎在 分布式事务 的情况下一般会用到 SERIALIZABLE(可串行化) 隔离级别。
表级锁和行级锁的区别
表级锁和行级锁对比:
- 表级锁: MySQL中锁定 粒度最大 的一种锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM和 InnoDB引擎都支持表级锁。
- 行级锁: MySQL中锁定 粒度最小 的一种锁,只针对当前操作的行进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。
InnoDB存储引擎的锁的算法有三种:
- Record lock:单个行记录上的锁
- Gap lock:间隙锁,锁定一个范围,不包括记录本身
- Next-key lock:record+gap 锁定一个范围,包含记录本身
MySQL 是怎么弄避免的幻读的?
- 在快照读情况下,MySQL通过
mvcc
来避免幻读。快照读,读取的是快照中的数据,不需要进行加锁。 - 在当前读情况下,MySQL通过
next-key
来避免幻读(加行锁和间隙锁来实现的)。所谓当前读就是,读取的是最新版本的数据,并且对读取的记录加锁,阻塞其他事务同时改动相同记录,避免出现安全问题。
MVCC是指数据库中一条数据有多个版本同时存在,在某个事务对其进行具体操作的时候,是需要查看这一条记录的隐藏列事务版本的id,比对事务id并根据事务的隔离级别从而去判断是哪个版本的数据。
MVCC 实现原理如下:
MVCC 的实现依赖于版本链,版本链是通过表的三个隐藏字段实现。
- DB_TRX_ID:当前事务id,通过事务id的大小判断事务的时间顺序。
- DB_ROLL_PTR:回滚指针,指向当前行记录的上一个版本,通过这个指针将数据的多个版本连接在一起构成undo log版本链。
- DB_ROW_ID:主键,如果数据表没有主键,InnoDB会自动生成主键。
使用事务进行更新记录的时候,就会生成版本链,执行过程如下:
- 用排它锁锁住该行;
- 将该行原本的值拷贝到undo log,作为旧版本回滚;
- 修改当前行的值,生成一个新的版本,更新事务id,使回滚指针指向旧版本的记录,这样就形成一条版本链。
垂直分区与水平分区
根据数据库里面数据表的相关性进行拆分。 例如,用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。
简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。
- 垂直拆分的优点: 可以使得列数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。
- 垂直拆分的缺点: 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂;
水平分区
保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。 水平拆分可以支撑非常大的数据量。
水平拆分是指数据表行的拆分,表的行数超过200万行时,就会变慢,这时可以把一张的表的数据拆成多张表来存放。举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。
水平拆分能够 支持非常大的数据量存储,应用端改造也少,但 分片事务难以解决 ,跨节点Join性能较差,逻辑复杂。《Java工程师修炼之道》的作者推荐 尽量不要对数据进行分片,因为拆分会带来逻辑、部署、运维的各种复杂度 ,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。如果实在要分片,尽量选择客户端分片架构,这样可以减少一次和中间件的网络I/O。
BufferPool缓存机制
数据库的增删改查操作都是直接操作buffer pool,从Buffer Pool缓冲池中修改数据,数据库会先将修改的数据写入redo日志,然后通过异步IO线程随机写入磁盘文件。其中先记录redo日志后写入磁盘,为了保证数据库的性能。redo日志为顺序写入,磁盘文件为随机写入。顺序写入比随机写入效率高。
一条 SQL 语句的执行流程
Redis 常见数据结构以及使用场景
string
- 介绍 :string 数据结构是简单的 key-value 类型。虽然 Redis 是用 C 语言写的,但是 Redis 并没有使用 C 的字符串表示,而是自己构建了一种 简单动态字符串(simple dynamic string,SDS)。相比于 C 的原生字符串,Redis 的 SDS 不光可以保存文本数据还可以保存二进制数据,并且获取字符串长度复杂度为 O(1)(C 字符串为 O(N)),除此之外,Redis 的 SDS API 是安全的,不会造成缓冲区溢出。
- 常用命令: set,get,strlen,exists,dect,incr,setex 等等。
- 应用场景 :一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等。
list
- 介绍 :list 即是 链表。链表是一种非常常见的数据结构,特点是易于数据元素的插入和删除并且且可以灵活调整链表长度,但是链表的随机访问困难。许多高级编程语言都内置了链表的实现比如 Java 中的 LinkedList,但是 C 语言并没有实现链表,所以 Redis 实现了自己的链表数据结构。Redis 的 list 的实现为一个 双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
- 常用命令: rpush,lpop,lpush,rpop,lrange、llen 等。
- 应用场景: 发布与订阅或者说消息队列、慢查询。
hash
- 介绍 :hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。不过,Redis 的 hash 做了更多优化。另外,hash 是一个 string 类型的 field 和 value 的映射表,特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等。
- 常用命令: hset,hmset,hexists,hget,hgetall,hkeys,hvals 等。
- 应用场景: 系统中对象数据的存储。
set
- 介绍 : set 类似于 Java 中的 HashSet 。Redis 中的 set 类型是一种无序集合,集合中的元素没有先后顺序。当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。比如:你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。
- 常用命令: sadd,spop,smembers,sismember,scard,sinterstore,sunion 等。
- 应用场景: 需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景
sorted set
- 介绍: 和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中 HashMap 和 TreeSet 的结合体。
- 常用命令: zadd,zcard,zscore,zrange,zrevrange,zrem 等。
- 应用场景: 需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息。
什么是跳表?
将有序链表通过建立冗余索引改造成支持“折半查找”算法,可以进行快速的插入、删除、查找操作。(类似B+树)
Redis 单线程模型详解
Redis 基于 Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler)。文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据 套接字目前执行的任务来为套接字关联不同的事件处理器。
当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关 闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。
文件事件处理器(file event handler)主要是包含 4 个部分:
- 多个 socket(客户端连接)
- IO 多路复用程序(支持多个客户端连接的关键)
- 文件事件分派器(将 socket 关联到相应的事件处理器)
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
Redis 缓存重建
大V直播带货导致冷门数据在某一时刻被大量访问,导致大量请求打到数据库,导致数据库崩溃。
解决思路:查询缓存不存在时加锁让资源同步请求数据库,查询到数据时再同步到缓存。
解决方法:
- 通过synchronzied双重校验锁住查询数据库以及缓存代码,但是会导致不同冷门数据的查询被同时锁住,并且在分布式环境下,会导致所有机器重建缓存。
- 通过redis分布式锁的方式实现,通过redisson根据数据ID获取锁对象然后加锁,本质上是通过redis setnx命令实现的。
Redis 超百万/千万请求
微博超大V发布热点数据导致超百万/千万请求同时访问微博,redis承受不住宕机。
解决思路:设置多级缓存,设置JVM级别的缓存,查询redis前查询JVM级别的缓存来分担redis压力。
为什么redis这么快
- Redis是完全基于内存的数据库
- 处理网络请求使用的是单线程,避免了不必要的上下文切换和锁的竞争维护。
- 使用了I/O多路复用模型。
完全基于内存
为什么要用完全呢。因为像mysql这样的传统关系型数据库是存储在硬盘的,那么硬盘的性能和瓶颈将会影响到数据库。
单线程
需要注意的是,这里的单线程指的是,Redis处理网络请求的时候只有一个线程
,而不是整个Redis服务是单线程的。
I/O多路复用模型
- 传统多进程并发模型: 每监听到一个Socket连接就会分配一个线程处理
- 多路复用模型: 单个线程,通过记录跟踪每一个Socket连接的I/O的状态来同时管理多个I/O流。
这里的I/O指的是网络I/O
,多路指的是多个网络连接
,复用指的是复用一个线程
。
结合Redis:
- 在Redis中的
I/O多路复用程序
会监听多个客户端连接的Socket - 每当有客户端通过Socket流向Redis发送请求进行操作时,I/O多路复用程序会将其放入一个
队列
中。 - 同时I/O多路复用程序会
同步
、有序
、每次传送一个任务给处理器处理。 - I/O多路复用程序会在上一个请求处理完毕后再继续分派下一个任务。(同步)
Redis事务实现
- 事务开始
MULTI命令的执行,标识着一个事务的开始。MULTI命令会将客户端状态的flags属性中打开REDIS_MULTI标识来完成的。 - 命令入队
当客户端切换到事务状态之后,服务器会根据这个客户端发送来的命令来执行不同的操作。如果客户端发送的命令为MULTI、EXEC、WATCH、DISCARD中的一个,则事务开始执行,否则将命令放入一个事务队列中,客户端返回QUEUED命令。 - 客户端会检查命令格式是否正确,如果不正确,服务器会在客户端状态的flags属性关闭REDIS_MULTI标识,并返回错误信息给客户端。
- 事务执行
客户端发送EXEC命令,服务器执行EXEC命令逻辑。 - 如果客户端的flags属性不包含REDIS_MULTI标识,或者包含REDIS_DIRTY_CAS或者REDIS_DIRTY_EXEC标识,那么就直接取消事务的执行。
- 客户端处于事务状态。会执行队列中所有命令,最后返回结果。
redis实际上是支持事务回滚的,只不过这种回滚是发生在指令组队阶段,因为这些指令是可以预知的。
对于运行时出错,redis是不支持回滚的,因为情况是未知的,所以为了保证redis简单快速,所以设计者并未将运行时出错的事务回滚。
- WATCH 命令是一个乐观锁,可以为监控一个或多个值,一旦其中一个值被修改,之后的事务就不会执行。健康一直持续到EXEC命令。
- MULTI命令用于开启一个事务。客户端可以继续向服务器发送任意多条命令,这些命令不会立即执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
- EXEC用于执行事务块内的命令。返回事务队列中所有命令的返回值,按命令执行的先后顺序排列。当操作被打断,返回空值nil。
- DISCARD用于清空事务队列,并放弃执行事务,并且客户端从事务状态中退出。
- UNWATCH命令可以取消WATCH对所有key的监控。
Redis有哪些部署方案?
- 单机版:单机部署,单机redis能够承载的 QPS 大概就在上万到几万不等。这种部署方式很少使用。存在的问题:1、内存容量有限 2、处理能力有限 3、无法高可用。
- 主从模式:一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的读请求全部走从节点。这样也可以很轻松实现水平扩容,支撑读高并发。master 节点挂掉后,需要手动指定新的 master,可用性不高,基本不用。
- 哨兵模式:主从复制存在不能自动故障转移、达不到高可用的问题。哨兵模式解决了这些问题。通过哨兵机制可以自动切换主从节点。master 节点挂掉后,哨兵进程会主动选举新的 master,可用性高,但是每个节点存储的数据是一样的,浪费内存空间。数据量不是很多,集群规模不是很大,需要自动容错容灾的时候使用。
- Redis cluster:服务端分片技术,3.0版本开始正式提供。Redis Cluster并没有使用一致性hash,而是采用slot(槽)的概念,一共分成16384个槽。将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行。主要是针对海量数据+高并发+高可用的场景,如果是海量数据,如果你的数据量很大,那么建议就用Redis cluster,所有主节点的容量总和就是Redis cluster可缓存的数据容量。
主从复制的原理?
- 当启动一个从节点,会发送一个PSYNC命令给主节点。
- 如果是从节点初次连接到主节点,那么会触发一次全量复制。此时主节点会启动一个后台线程,开始生成一份RDB快照文件。
- 同时还会将从客户端client新收到的所有写命令缓存在内存中。RDB文件生成完毕后,主节点会将RDB文件发送给从节点,从节点会先将RDB文件写入本地磁盘,然后再从本地磁盘加载到内存中。
- 接着主节点会将内存中缓存的写命令发送到从节点,从节点同步这些数据。
如何解决缓存数据库不一致
- 先更新数据库,后删除缓存。理论上来说还是可能会出现数据不一致的问题,不过概率非常小,因为缓存的写入速度是比数据库的写入速度快很多。
- 延时双删。先淘汰缓存,再写数据库,休眠一秒,再次淘汰缓存。
def update_data(key, obj):
del_cache(key) # 删除 redis 缓存数据。
update_db(obj) # 更新数据库数据。
logic_sleep(_time) # 当前逻辑延时执行。
del_cache(key) # 删除 redis 缓存数据。
数据工作的大致流程:
- 服务节点删除 redis 主库数据。
- 服务节点修改 mysql 主库数据。
- 服务节点使得当前业务处理 等待一段时间,等 redis 和 mysql 主从节点数据同步成功。
- 服务节点从 redis 主库删除数据。
- 当前或其它服务节点读取 redis 从库数据,发现 redis 从库没有数据,从 mysql 从库读取数据,并写入 redis 主库。
如何对Redis进行优化
- 缩短键值对的存储长度。禁用bigkey。
- 使用lazy free(延迟删除)特性。惰性删除,使用子线程进行删除,避免主线程阻塞。
- 设置键值的过期时间。
- 禁用耗时长的查询命令。禁止使用keys命令,会进行遍历。避免查询所有成员,可以进行分页。尽量在客户端进行计算。
- 使用slowlog优化耗时命令。
- 避免大量数据同时失效。
- 限制Redis内存大小。
- 检查数据持久化策略。
- 使用分布式架构来增加写速度。
怎么判断对象已经死亡?
引用计数法
给对象中添加一个计数器,每当一个地方引用他,则计数器加1,当引用失效,计数器减1,计数器为0的对象就是不可能在被使用的。
这个方法实现简单,效率高,但是目前主流并没有选择这个算法来管理内存,主要原因是无法解决对象之间相互循环引用的问题。
可达性算法
通过一系列"GC Roots"对象作为起点,开始向下搜索节点所走过的路称为引用链,当一个对象到"GC Roots"没有任何引用链相连,则证明该对象是不可达的。
可作为GC Roots的对象包括下面几种
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
引用
强引用
我们常常new出来的对象都是强引用,只要强引用存在垃圾回收器永远不会回收,哪怕内存不足。
软引用
使用SoftReference修饰的对象被称为软引用,如果内存足够则垃圾回收器不会回收,如果内存不足就会回收这类对象的引用。
弱引用
使用WeakReference修饰的对象被称为弱引用,只要发生垃圾回收,不管当前内存是否足够,都会回收它的内存。不过由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
虚引用
使用PhantomReference修饰的对象被称为虚引用,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么就和没有任何引用一样,在任何时候都会被垃圾回收。唯一的作用就是用队列接受对象即将死亡的通知。
如何判断一个类是一个无用的类
- 该类的所有实例都已经被回收,堆中不存在该类的任何实列
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class 对象没在任何地方被引用,无法在任何地方通过反射访问该类的方法
虚拟机可以对满足上诉3个条件的无用类进行回收,并不是和对象一样不使用了就会必然被回收
类加载器
JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader:
BootstrapClassLoader(启动类加载器)
最顶层的加载类,由C++实现,负责加载 %JAVA_HOME%/lib
目录下的jar包和类或者或被 -Xbootclasspath
参数指定的路径中的所有类。
ExtensionClassLoader(扩展类加载器)
主要负责加载目录 %JRE_HOME%/lib/ext
目录下的jar包和类,或被 java.ext.dirs
系统变量所指定的路径下的jar包。
AppClassLoader(应用程序类加载器)
面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。
双亲委派模型
系统中的ClassLoder在协同工作的时候会默认使用双亲委派机制。在类加载的时候,系统会先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把请求委派给该父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。
类加载器之间的"父子"关系不是通过继承来体现的,是由"优先级"来决定的。
如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是如果想打破双亲委派机制模型则需要重写loadClass() 方法。
使用线程池的好处
- 降低资源消耗。通过重复利用已经创建的线程降低创建线程和销毁线程带来的消耗。
- 提高效应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
- 提高线程的可管理性。如果无限制的创建线程,不仅会消耗系统资源,还会降低系统的稳定性,使用线程可以进行统一的分配,调优和监控。
Executor框架
Executor框架是在JDK1.5之引进的,通过 Executor 来启动线程比使用 Thread 的 start 方法更好,除了更易管理和效率更好之外,最关键的一点是:有助于防止 this 逃逸。
this 逃逸是指在构造函数返回之前其他线程就持有该对象的引用. 调用尚未构造完全的对象的方法可能引发令人疑惑的错误。
Executor框架不仅包括了线程池的管理,还提供了线程工厂,队列以及拒绝策略。
ThreadPoolExecutor
ThreadPoolExcutor 是 Executor 框架最核心的类
ThreadPoolExecutor最核心的三个参数
- corePoolSize:核心线程数定义了最小可以同时运行的线程数量。
- maximumPoolSize:当队列中的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
- workQueue:当新的任务来的时候会判断当前运行的线程数量是否达到核心线程数量,如果达到,则会被存放到队列当中。
ThreadPoolExecutor 其他常见参数:
- keepAliveTime: 当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime 才会被回收销毁;
- unit: keepAliveTime 参数的时间单位。
- threadFactory: executor 创建新线程的时候会用到。
- handler:饱和策略。
ThreadPoolExecutor 饱和策略定义:
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor 定义一些策略:
ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用 execute 方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
ThreadPoolExecutor.DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求。
Executors 返回线程池对象的弊端如下:
- FixedThreadPool 和 SingleThreadExecutor: 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
- CachedThreadPool 和 ScheduledThreadPool: 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
创建线程池的几种方式
方式一:通过 ThreadPoolExecutor 构造函数实现(推荐)
方式二:通过 Executor 的框架的工具类 Executors 来创建三种类型的 ThreadPoolExecutor:
- FixedThreadPool
- SingleThreadExecutor
- CachedThreadPool
FixedThreadPool
FixedThreadPool 被称为可重用固定线程数量的线程池。
- 如果当前运行的线程数小于 corePoolSize, 如果再来新任务的话,就创建新的线程来执行任务;
- 当前运行的线程数等于 corePoolSize 后, 如果再来新任务的话,会将任务加入 LinkedBlockingQueue;
- 线程池中的线程执行完 手头的任务后,会在循环中反复从 LinkedBlockingQueue 中获取任务来执行;
为什么不推荐使用 FixedThreadPool ?
FixedThreadPool 使用无界队列 LinkedBlockingQueue(队列的容量为 Intger.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响 :
- 当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize;
- 由于使用无界队列时 maximumPoolSize 将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建 FixedThreadPool 的源码可以看出创建的 FixedThreadPool 的 corePoolSize 和 maximumPoolSize 被设置为同一个值。
- 由于 1 和 2,使用无界队列时 keepAliveTime 将是一个无效参数;
- 运行中的 FixedThreadPool(未执行 shutdown() 或 shutdownNow())不会拒绝任务,在任务比较多的时候会导致 OOM(内存溢出)。
SingleThreadExecutor
SingleThreadExecutor 是一个只有一个线程的线程池。
- 如果当前运行的线程数少于 corePoolSize,则创建一个新的线程执行任务;
- 当前线程池中有一个运行的线程后,将任务加入 LinkedBlockingQueue
- 线程执行完当前的任务后,会在循环中反复从 LinkedBlockingQueue 中获取任务来执行;
为什么不推荐使用 SingleThreadExecutor?
SingleThreadExecutor 使用无界队列 LinkedBlockingQueue 作为线程池的工作队列(队列的容量为 Intger.MAX_VALUE)。SingleThreadExecutor 使用无界队列作为线程池的工作队列会对线程池带来的影响与 FixedThreadPool 相同。说简单点就是可能会导致 OOM,
CachedThreadPool
CachedThreadPool 是一个会根据需要创建新线程数量的线程池。
- 首先执行 SynchronousQueue.offer(Runnable task) 提交任务到任务队列。如果当前 maximumPool 中有闲线程正在执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行 offer 操作与空闲线程执行的 poll 操作配对成功,主线程把任务交给空闲线程执行,execute() 方法执行完成,否则执行下面的步骤 2;
- 当初始 maximumPool 为空,或者 maximumPool 中没有空闲线程时,将没有线程执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤 1 将失败,此时 CachedThreadPool 会创建新线程执行任务,execute 方法执行完成;
为什么不推荐使用 CachedThreadPool?
CachedThreadPool 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor 主要用来在给定的延迟后运行任务,或者定期执行任务。
运行时数据区域
线程私有的:
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享的:
- 堆
- 方法区
- 直接内存
程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
Java 虚拟机栈
与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。
Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。 (实际上,Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。)
局部变量表主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
Java 虚拟机栈会出现两种错误:StackOverFlowError 和 OutOfMemoryError。
- StackOverFlowError: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
- OutOfMemoryError: 若 Java 虚拟机栈中没有空闲内存,并且垃圾回收器也无法提供更多内存的话。就会抛出 OutOfMemoryError 错误。
Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。
Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。
Java 方法有两种返回方式:
- return 语句。
- 抛出异常。
不管哪种返回方式都会导致栈帧被弹出。
本地方法栈
和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误。
堆
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
Java世界中“几乎”所有的对象都在堆中分配,但是,随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从jdk 1.7开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
在 JDK 7 版本及JDK 7 版本之前,堆内存被通常被分为下面三部分:
- 新生代内存(Young Generation)
- 老生代(Old Generation)
- 永生代(Permanent Generation)
JDK 8 版本之后方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。
分代模型
年轻代:主要存放新创建的对象,分为三个区域 伊甸园、存活区 (from,to)。
- 伊甸园:新建的对象
- from:从to向from复制,一个对象在存活区被来回复制15次,会进入老年代。
- to:伊甸园存活的对象复制到to。
垃圾回收算法: 复制算法。为了解决标记清楚算法的内存碎片问题,就产生了复制算法。复制算法将内存分为大小相等的两半,每次只使用其中一半。垃圾回收时。将当前这一块的存活对象全部拷贝到另一半,然后当前这一半内存就可以直接清楚。这种算法没有内存碎片,但是他的问题就在于浪费时间。而且,它的效率跟存活对象的个数有关。
老年代:存放存活时间长的对象
垃圾回收算法:
- 标记清楚。标记阶段,把垃圾内存标记出来。清楚阶段,直接将垃圾内存回收。这种算法比较简单,但是会产生大量的内存碎片。
- 标记整理/标记压缩算法。为了解决复制算法的缺陷,就提出了标记压缩算法,这种算法在标记阶段跟标记清楚算法是一样的,但是在完成标记之后,不是直接清理垃圾内存,而是将存活对象往一端移动,然后将边界以外的所有内存直接清除。
永久代 (元数据):主要存放类对象、静态对象
垃圾回收器:
- java1.8 默认垃圾回收器 Parallerl Scavenge + Parallel Old
- CMS 并发标记清除 ,参数多,调优复杂
- G1 Garbage First,只需要设置一个 STW 目标时间,其他参数可以自动调整,G1 颠覆了传统分代模型
CMS 和 G1 降低了 STW(Stop The World) 时间。G1 被用来替代 CMS
JVM为什么要设置STW?
如果没有STW,会产生浮动垃圾(即标记完是存活对象,未产生STW,线程随之结束,可能对象已经变成了垃圾),回收性能差、效率低。
如果出现分析过程中对象引用还在不断变化,则分析结果的准确性无法保证。
方法区
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
方法区和永久代的关系
《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。
为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?
- 整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
- 元空间里面存放的是类的元数据,这样加载多少类的元数据就不由
MaxPermSize
控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。 - 在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。
乐观锁的两种实现方式
乐观锁一般会使用版本号机制和cas算法实现
版本号机制
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
cas算法
即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数
- 需要读写的内存值 V
- 进行比较的值 A
- 拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
乐观锁的缺点
1.ABA问题
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。
JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
2.循环时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
3.只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了 AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用 AtomicReference 类把多个共享变量合并成一个共享变量来操作。
Synchroized的偏向锁、轻量级锁、重量级锁
- 偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到了。
- 轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程。
- 如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞。
- 自旋锁:自旋锁就是线程在获取锁的过程中,不会阻塞线程,也就无法唤醒线程,阻塞和唤醒这两个步骤都是要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量。
Synchronized和ReentrantLock的区别
- synchronized是一个关键字,ReentrantLock是一个类。
- synchronized会自动的加锁与释放锁,ReentrantLock需要手动加锁与释放锁。
- synchronized的底层是JVM层面的锁,ReentrantLock是API层面的锁。
- synchronized是非公平锁,ReentrantLock可以选择公平锁和非公平锁。
- synchronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过state标识来标识锁的状态。
- synchronized底层有一个锁升级的过程。
CAS与 synchronized 的使用情况
简单来说CAS适用于写比较少的情况下,synchronized 适用于写比较多的情况下。
并发编程的三个重要特性
- 原子性 : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。synchronized 可以保证代码片段的原子性。
- 可见性 :当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile 关键字可以保证共享变量的可见性。
- 有序性 :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化。
Volatile 缓存可见性实现原理
底层实现主要是通过汇编lock前缀指令,他会锁定这块内存区域的缓存并回写到主内存。
Intel 64架构软件开发者手册对lock指令的解释:
- 会将当前处理器缓存行的数据立即写回到系统内存。
- 这个写回内存的操作会引起其他CPU里缓存了该内存地址的数据无效(MESI协议)。
- 提供内存屏障功能,使lock前后指令不能重排序。
as-if-seria语义
不管怎么重排序,(单线程)程序的执行结果不能被改变。编译器和处理器不会对存在数据依赖关系的操作做重排序。
CopyOnWriteArrayList
CopyOnWriteArrayList是java.util.concurrent包提供的方法,它实现了读操作无锁,写操作则通过操作底层数组的新副本来实现,是一种读写分离的并发策略。体现的是最终一致性,利用空间换时间。
CopyOnWriteArrayList并发容器用于绝大部分访问都是读,且只是偶尔写的并发场景。
shiro 认证和验证流程
认证流程
- 调用SecurityUtils.getSubject()方法创建Subject对象。
- 将用户信息封装为UsernamePasswordToken对象并调用login()方法将用户信息提交给SecurityManager对象
- SecurityManager将认证操作交给Authenticator进行身份验证
- Authenticator将用户信息委托给realm
- realm对象访问数据库获取用户信息并封装返回
- Authenticatcor对用户信息进行校验
验证流程
- 系统调用subject相关方法将用户信息(例如isPermitted)递交给SecurityManager
- SecurityManager将权限检测操作委托给Authorize对象
- Authorize对象将用户信息委托给realm
- realm访问数据库获取用户权限信息封装并返回
- Authorize对用户信息授权信息进行校验
工厂设计模式
Spring使用工厂模式可以通过 BeanFactory 或 ApplicationContext 创建 bean 对象。
- BeanFactory:延迟注入,相比于 ApplicationContext 来说会占用更少的内存,程序启动速度更快。
- ApplicationContext:容器启动时,不管你有没有用到,一次性创建所有 bean。Beanfactory 仅提供了最基本的依赖注入支持, ApplicationContext 扩展了 BeanFactory ,除了有 BeanFactory 的功能还有额外更多功能,所以一般开发人员使用 ApplicationContext 会更多。
单例设计模式
在系统中,有一些对象只需要一个。比如:线程池,缓存,日志对象。这一类对象只能有一个实例,如果创建多个实例就会导致一些问题。如程序异常,资源使用过量。
好处:
- 对于频发使用的对象,可以省略创建对象花费的时间。
- 由于new 操作的次数减少,所以对系统内存的使用频率也会降低。
Spring中 bean 的默认作用域就是 singleton 的
- prototype:每次请求就会创建一个新的 bean 实例。
- request:每一次HTTP请求都会产生一个新的 bean ,该 bean 仅在当前HTTP request内有效。
- session:每一次HTTP请求都会产生一个新的 bean ,该 bean 仅在当前HTTP session内有效。
- global-session:全局 session 作用域
代理设计模式
AOP 能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可扩展性和可维护性。
Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象,而没有实现接口的对象,就只能使用 Cglib,SpringAOP会使用 Cglib 生成一个被代理对象的子类来作为代理。
Spring 怎么解决循环依赖的问题
存在两种Bean方式的注入,构造器注入和属性注入。对于构造器注入的循环依赖,Spring处理不了,会直接抛出BeanCurrentlylnCreationException
异常。对于单例模式下属性注入的循环依赖,是通过三级缓存来处理循环依赖的。非单例对象的循环依赖,则无法处理。
singletonObjects
:单例池,完成了实例化的单例对象map。earlySingletonObjects
:spring的二级缓存,完成实例化未初始化的单例对象map。singletonFactories
:spring的三级缓存,完成了初始化的单例对象map。
假如A依赖了B的实例对象,同时B也依赖了A的实例对象
- 实例化A,此时还未完成属性的填充,为一个半成品,将A的普通对象和beanDefinition一起生成lamda表达式存放在singletonFactories中。
- 对A进行属性注入,发现需要注入B对象,但是一级、二级、三级缓存均为发现对象B。
- 实例化B,此时还未完成属性的填充,为一个半成品,将B的普通对象和beanDefinition一起生成lamda表达式存放在singletonFactories中。
- 对B进行属性注入,发现需要注入A对象,此时在一级、二级未发现对象A,但是在三级缓存中发现了对象A的信息,如果A需要进行AOP,则通过lamda表达式生成代理对象,不需要则获得普通对象。并将对象A放入二级缓存中,同时删除三级缓存中的对象A。
- 将对象A注入到对象B中。
- 对象B完成属性填充,执行初始化方法,并放入到一级缓存,同时删除二级缓存中的对象B。
- 对象A得到对象B,将对象B注入到对象A中。
- 对象A完成属性填充,执行初始化方法,并放入到一级缓存,同时删除二级缓存中的对象A。
MyBatis 中#{}和${}的区别是什么?
- 占位符 ${} 是Properties 文件中的变量占位符,它可以用于标签属性值和sql内部,属于静态文本替换。
- 占位符 #{} 是sql的参数占位符,MyBatis 会将sql中的#{}替换为?号,在sql执行前会使用 PreparedStatement 的参数设置方法,按序给sql的?号占位符设置参数。
CAP & BASE理论
当发生网络分区的时候,如果我们要继续服务,那么强一致性和可用性只能 2 选 1。也就是说当网络分区之后 P 是前提,决定了 P 之后才有 C 和 A 的选择。也就是说分区容错性(Partition tolerance)我们是必须要实现的。简而言之就是:CAP 理论中分区容错性 P 是一定要满足的,在此基础上,只能满足可用性 A 或者一致性 C。
- 一致性(Consistency) : 所有节点访问同一份最新的数据副本
- 可用性(Availability): 非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。
- 分区容错性(Partition Tolerance) : 分布式系统出现网络分区的时候,仍然能够对外提供服务。
BASE 是 Basically Available(基本可用) 、Soft-state(软状态) 和 Eventually Consistent(最终一致性) 三个短语的缩写。即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。BASE理论本质上是对CAP的延伸和补充,是对CAP中AP方案的一个补充。
- 基本可用:指分布式系统在出现不可预知故障的时候,允许损失部分可用性(允许响应时间上和系统功能上的损伤)。
- 软状态:软状态指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行同步过程中存在延时。
- 最终一致性:最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
实现最终一致性的具体方法
- 读时修复 : 在读取数据时,检测数据的不一致,进行修复。比如 Cassandra 的 Read Repair 实现,具体来说,在向 Cassandra 系统查询数据的时候,如果检测到不同节点的副本数据不一致,系统就自动修复数据。
- 写时修复 : 在写入数据,检测数据的不一致时,进行修复。比如 Cassandra 的 Hinted Handoff 实现。具体来说,Cassandra 集群的节点之间远程写数据的时候,如果写失败 就将数据缓存下来,然后定时重传,修复数据的不一致性。
- 异步修复 : 这个是最常用的方式,通过定时对账检测副本数据的一致性,并修复。
比较推荐写时修复,这种方式对性能消耗比较低。
Zookeeper 介绍
ZooKeeper 为我们提供了高可用、高性能、稳定的分布式数据一致性解决方案,通常被用于实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。
另外,ZooKeeper 将数据保存在内存中,性能是非常棒的。 在“读”多于“写”的应用程序中尤其地高性能,因为“写”会导致所有的服务器间同步状态。(“读”多于“写”是协调服务的典型场景)。
- ZooKeeper 本身就是一个分布式程序(只要半数以上节点存活,ZooKeeper 就能正常服务)。
- 为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么 ZooKeeper 本身仍然是可用的。
- ZooKeeper 将数据保存在内存中,这也就保证了 高吞吐量和低延迟(但是内存限制了能够存储的容量不太大,此限制也是保持 znode 中存储的数据量较小的进一步原因)。
- ZooKeeper 是高性能的。 在“读”多于“写”的应用程序中尤其地明显,因为“写”会导致所有的服务器间同步状态。(“读”多于“写”是协调服务的典型场景。)
- ZooKeeper 有临时节点的概念。 当创建临时节点的客户端会话一直保持活动,瞬时节点就一直存在。而当会话终结时,瞬时节点被删除。持久节点是指一旦这个 znode 被创建了,除非主动进行 znode 的移除操作,否则这个 znode 将一直保存在 ZooKeeper 上。
- ZooKeeper 底层其实只提供了两个功能:① 管理(存储、读取)用户程序提交的数据;② 为用户程序提供数据节点监听服务。
Zookeeper的数据模型和节点类型
Zookeeper数据模型采用层次化的多叉树形结构。每个节点可以拥有Nge子节点,最上层的根节点以"/"表示。每个数据节点被称为znode,它是ZooKeeper中数据的最小单元。并且,每个znode都是一个唯一的路径标识。Zookeeper是用来协调服务的,而不是用来存储业务数据的,所有Zookeeper限制每个节点的数据大小最大为1M。
znode通常分为4大类:
- 持久节点:一旦创建就一直存在,除非手动删除。
- 临时节点:临时节点的生命周期是与客户端回话绑定的,会话消失则节点消失。并且,临时节点只能做叶子节点,不能创建子节点。
- 持久顺序节点:除了具备持久节点的特性,还具备顺序性。
- 临时顺序节点:除了具备临时节点的特性,还具备顺序性。
ZAB协议
ZAB协议是为分布式协调服务ZooKeeper专门设计的一种支持崩溃恢复的原子广播协议。ZooKeeper主要依赖ZAB协议来实现分布式数据一致性。ZAB协议包括两种基本的模式,分别是
- 崩溃恢复:当整个服务框架在启动过程中,或当Leader服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB协议就会进入恢复模式并选举出新的Leader服务器。当选举产生新的Leader服务器,同时集群中过半数的机器完成同Leader服务器的状态同步之后,ZAB协议就会退出恢复模式。状态同步就是数据同步,用来保证集群中过半机器能够和Leader服务器的数据状态保持一致。
- 消息广播:当集群中过半的Follower服务器完成和Leader服务器的状态同步,那么整个服务框架就可以进入消息广播模式。数据副本的传递策略就是采用消息广播模式。
Zookeeper的Watcher机制
Watcher实现由三个部分组成:
- Zookeeper服务端
- Zookeeper客户端
- 客户端的ZKWatchManager对象
客户端首先将Watcher注册到服务端,同时将Watcher对象保存到客户端的Watch管理器中。当Zookeeper服务端监听的数据状态发送变化时,服务端会主动通知客户端,接着客户端的Watch管理器会触发相关Watcher来回调相应处理逻辑,从而完成整体的数据发布、订阅流程。
Zookeeper的选举模式
初始化选举:假设我们集群中有3台机器,那也就意味着我们需要两台以上同意(超过半数)。比如这个时候我们启动了 server1 ,它会首先 投票给自己 ,投票内容为服务器的 myid 和 ZXID ,因为初始化所以 ZXID 都为0,此时 server1 发出的投票为 (1,0)。但此时 server1 的投票仅为1,所以不能作为 Leader ,此时还在选举阶段所以整个集群处于 Looking 状态。接着 server2 启动了,它首先也会将投票选给自己(2,0),并将投票信息广播出去(server1也会,只是它那时没有其他的服务器了),server1 在收到 server2 的投票信息后会将投票信息与自己的作比较。首先它会比较 ZXID ,ZXID 大的优先为 Leader,如果相同则比较 myid,myid 大的优先作为 Leader。所以此时server1 发现 server2 更适合做 Leader,它就会将自己的投票信息更改为(2,0)然后再广播出去,之后server2 收到之后发现和自己的一样无需做更改,并且自己的 投票已经超过半数 ,则 确定 server2 为 Leader,server1 也会将自己服务器设置为 Following 变为 Follower。整个服务器就从 Looking 变为了正常状态。当 server3 启动发现集群没有处于 Looking 状态时,它会直接以 Follower 的身份加入集群。
崩溃选举:假设 server1 给自己投票为(1,99),然后广播给其他 server,server3 首先也会给自己投票(3,95),然后也广播给其他 server。server1 和 server3 此时会收到彼此的投票信息,和一开始选举一样,他们也会比较自己的投票和收到的投票(zxid 大的优先,如果相同那么就 myid 大的优先)。这个时候 server1 收到了 server3 的投票发现没自己的合适故不变,server3 收到 server1 的投票结果后发现比自己的合适于是更改投票为(1,99)然后广播出去,最后 server1 收到了发现自己的投票已经超过半数就把自己设为 Leader,server3 也随之变为 Follower。
Zookeeper的数据同步原理
Leader服务器根据peerLastZxid、minCommittedLog、maxCommittedLog的值决定数据同步类型。
- peerLastZxid:Learner服务器(Follower或observer)最后处理的zxid。
- minCommittedLog:Leader服务器proposal缓存队列committedLog中的最小的zxid。
- maxCommittedLog:Leader服务器proposal缓存队列committedLog中的最大的zxid。
- 差异化同步(DIFF同步):MinCommitedLog < peerLastZxid < MaxCommitedLog
- 回滚与差异化同步(TRUNC+DIFF):在实际运行当中, 主节点leader可能会出现故障, 比如重启, 经过一段时间之后, 数据是如何同步呢?当Leader将事务提交到本地事务日志中后,正准备将proposal发送给其他的Follower进行投票时突然宕机,这个时候Zookeeper集群会选取出新的Leader对外服务,并且可能提交了几个事务,经过一段时间之后,当旧的Leader再次上线,新Leader发现旧Leader身上有自己所没有的事务,就需要回滚抹去旧的Leader上自己没有的事务,再让旧的Leader同步完自己新提交的事务,这个就是TRUNC+DIFF的应用场景。
- 全量同步(SNAP同步):如果follower 的 peerLastZxid 小于 leader 的 minCommittedLog 或者 leader 节点上不存在提案缓存队列时,将采用 SNAP 全量同步方式。
Zookeeper实现分布式锁
线程先创建一接一个的顺序临时节点,找出最小的序列号,获取到锁,不是则通过watch监听上个节点的变化,线程业务执行完成之后,依次下一个节点进行操作。
脑裂问题
所谓脑裂就是在多机热备的高可用系统中,假设两个机房之间出现了通信故障,那么处于两个机房中的节点就会各自选举出master节点,当网络恢复时,就不知道该听哪个master节点的了。
过半机制:Zookeeper采用的防止脑裂问题的方法,通过这个机制,可以确保就算发生了网络故障,也只会有一个master节点选出。
一致性问题
设计一个分布式系统必定会遇到一个问题—— 因为分区容忍性(partition tolerance)的存在,就必定要求我们需要在系统可用性(availability)和数据一致性(consistency)中做出权衡 。这就是著名的 CAP 定理。
而上述前者就是 Eureka 的处理方式,它保证了AP(可用性),后者就是 ZooKeeper 的处理方式,它保证了CP(数据一致性)。
- 一致性(Consistence) : 所有节点访问同一份最新的数据副本
- 可用性(Availability): 非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。
- 分区容错性(Partition tolerance) : 分布式系统出现网络分区的时候,仍然能够对外提供服务。
分布式系统如何保证接口幂等性?
- Token接口方案
- 服务端需要提供一个token获取接口(该 Token 可以是一个序列号,也可以是一个分布式 ID 或者 UUID 串,反正要保证唯一性),客户端调用接口获取 Token,这时候服务端会生成一个 Token 串。
- 然后将该串存入 Redis 数据库中,以该 Token 作为 Redis 的键(注意设置过期时间)。
- 将 Token 返回到客户端,客户端拿到后应存到表单隐藏域中。
- 端在执行提交表单时,把 Token 存入到 Headers 中,执行业务请求带上该 Headers。
- 端接收到请求后从 Headers 中拿到 Token,然后根据 Token 到 Redis 中查找该 key 是否存在。
- 端根据 Redis 中是否存该 key 进行判断,如果存在就将该 key 删除,然后正常执行业务逻辑。如果不存在就抛异常,返回重复提交的错误信息。
- 上下游接口服务。对于这种情况我们可以让上游系统每次调用接口时都传输一个唯一的key。
我们下游系统接收到数据以后,先不要直接操作数据库,而是先拿着这个key,通过setNX命令设置到Redis并设置一定的过期时间
分布式Session解决方案
- 客户端存储:直接将session数据存储到浏览器的cookie中,浏览器发起请求时,通过cookie将session数据发送给客户端。但是,由于cookie不安全,且每次http请求,都会携带存储在cookie中的完整用户信息,会增大网络传输开销,并且cookie有存储大小限制。所以基本上不会使用这种方式。
- Hash一致性:修改nginx的负载均衡配置,设置为ip-hash策略,将客户端与服务器进行绑定,让来自同一ip的请求,全都转发到同一台服务器。但是一台服务器挂掉之后,该服务器的session信息会全部丢失。并且进行水平扩展时,会重新对客户端ip进行hash操作,部分ip会被重新映射服务器。
- 分布式redis:将集群下所有服务器的session都存储到redis集群中。直接使用Spring封装的Spring Session,数据保存在redis中,无缝接入,无安全隐患。唯一的缺点是,服务器需要与Redis做一次网络交互,多了点网络开销。
分布式事务解决方案
强一致性
XA规范:分布式事务规范,定义了分布式事务模型。四个角色,事务管理器(协调组TM)、资源管理器(参与者RM)、应用程序AP,通信资源管理器CRM。
2PC方案
2PC方案分为两阶段:
第一阶段,事务管理器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交。
第二阶段,事务管理器要求每个数据库提交数据,或者回滚数据。
缺点:
- 单点问题:事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导致数据库无法使用。
- 同步阻塞:在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源。
- 数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,比如在第二阶段中,假设协调者发出了事务commit的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。
弱一致性
TCC方案
- Try阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留。
- Confirm阶段:这个阶段指的是各个服务中执行实际操作。
- Cancel:如果任何一个服务的业务方案执行出错,那就将已经执行成功的业务逻辑回滚。
缺点:事务回滚实际上是严重依赖与你自己写代码来回滚,造成补偿代码巨大。
MQ事务
- 发送方向 MQ 服务端(MQ Server)发送 half 消息。
- MQ Server 将消息持久化成功之后,向发送方 ack 确认消息已经发送成功。
- 发送方开始执行本地事务逻辑。
- 发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)。
- MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;
- MQ Server 收到 rollback 状态则删除半消息,订阅方将不会接受该消息。
异常情况
- MQ Server 对该消息发起消息回查。
- 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
- 发送方根据检查得到的本地事务的最终状态再次提交二次确认。
- MQ Server基于 commit/rollback 对消息进行投递或者删除。
优点
- 消息数据独立存储 ,降低业务系统与消息系统之间的耦合。
- 吞吐量大于使用本地消息表方案。
缺点
- 一次消息发送需要两次网络请求(half 消息 + commit/rollback 消息) 。
- 业务处理服务需要实现消息状态回查接口。
Spring Cloud
- 一个中心:eureka
- 两个基本点:ribbon、hystrix
- 三个工具:feign、zuul、config
- 四个监控:hystrix dashboard、turbine、sleuth、zipkin
消息服务
- rabbitmq:六种模式
- rocketmq:延时消息、顺序消息、事物消息 (可靠消息)
lucene solr
- 全文检索引擎服务器
- 索引:倒排索引
- solr 和 elasticsearch
docker + k8s
- 开发运维一体化
- docker:容器化技术
- k8s:持续部署,自动部署工具
分布式事务
- seata at 全自动事务方案,对业务无侵入,80%业务场景
- seata tcc 对业务有侵入,效率更高
- rocketmq 可靠消息最终一致性 (最终确保)
为什么要用 Dubbo?
- 负载均衡——同一个服务部署在不同的机器时该调用那一台机器上的服务。
- 服务调用链路生成——随着系统的发展,服务越来越多,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。Dubbo 可以为我们解决服务之间互相是如何调用的。
- 服务访问压力以及时长统计、资源调度和治理——基于访问压力实时管理集群容量,提高集群利用率。
- 服务降级——某个服务挂掉之后调用备用服务。
什么是分布式以及为什么要使用分布式?
分布式或者说 SOA 分布式重要的就是面向服务,说简单的分布式就是我们把整个系统拆分成不同的服务然后将这些服务放在不同的服务器上减轻单体服务的压力提高并发量和性能。比如电商系统可以简单地拆分成订单系统、商品系统、登录系统等等,拆分之后的每个服务可以部署在不同的机器上,如果某一个服务的访问量比较大的话也可以将这个服务同时部署在多台机器上。
从开发角度来讲单体应用的代码都集中在一起,而分布式系统的代码根据业务被拆分。所以,每个团队可以负责一个服务的开发,这样提升了开发效率。另外,代码根据业务拆分之后更加便于维护和扩展。
另外,我觉得将系统拆分成分布式之后不光便于系统扩展和维护,更能提高整个系统的性能。你想一想嘛?把整个系统拆分成不同的服务/系统,然后每个服务/系统 单独部署在一台服务器上,是不是很大程度上提高了系统性能呢?
分布式缓存
分布式缓存主要解决的是单机缓存的容量受服务器的限制并且无法保存通用的信息。因为,本地缓存只在当前服务里有效,比如如果你部署了两个相同的服务,他们两者之间的缓存数据是无法共同的。
分布式服务限流方案
- 计数器:算法的实现思路就是从第一个请求进入开始计时,在1s内,每进入一个请求计数器加一,如果累加的数字达到设置的最大值,那么后续的请求全部拒绝,直到1s结束,计数器恢复为0,重新计数。Java内部可以通过原子类计数器AtomicInteger来实现。
- 漏桶算法:通过漏桶算法进行限流,因为处理速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,如果桶满了,那么新进来的请求就丢弃。可以准备一个队列,用来保存请求,另外通过线程池从队列中获取请求执行。弊端是无法应对短时间的突发流量。
- 令牌桶算法:系统维护一个令牌(token)桶,以一个恒定的速度往桶里放入令牌,如果有请求想要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则该请求将被拒绝服务。令牌桶算法通过控制桶的容量、发放令牌的速率,来达到对请求的限制。
- Redis集群限流:请求到达时,就向redis服务器发送一个incr命令,比如需要限制某个用户访问接口的次数,只需要拼接用户id和接口名生成redis的key,每次该用户访问此接口时,只需要对这个key执行incr命令,在这个key上带上过期时间,就可以实现指定时间的访问评率。
分布式ID解决方案
Snowflake(雪花算法)
Snowflake是Twitter开源的分布式ID生成算法。Snowflake由64bit的二进制数字组成,这64bit的二进制被分成了几部分,每一部分存储的数据都有特定的含义:
- 第0位:符号位(标识正负),始终为0,不用管。
- 第1~41位:一共41位,表示时间戳,单位是毫秒,可以支撑2^41毫秒(约69年)
- 第42~52位:一共10位,一般前5位表示机房ID,后5位表示机器ID
- 第53位~64位:一共12位,用来表示序列号。序列号自增。
优点:生成速度比较快、生成的ID有序递增、比较灵活(可以对Snowflake算法进行简单的改造比如加入业务ID)
缺点:需要解决重复ID问题(依赖时间,当机器时间不对的情况下,可能导致产生重复ID)
Leaf(美团)
Leaf是美团开源的一个分布式ID解决方案。Leaf提供了号段模式和Snowflake这两种模式来生成分布式ID。并且,它支持双号段,还解决了雪花ID的系统时钟回拨问题。需要依赖于Zookeeper。Leaf对原有的号段模式进行改进,增加双号段模式,避免获取DB在获取号段的时候阻塞请求获取ID的进程。在号段未使用完之前,主动提前获取下一个号段。
Dubbo集群容错策略
- Failover Cluster:失败自动切换,dubbo的默认容错方案,当调用失败时自动切换到其他可用节点。通常用于读操作,但重试可能带来更大延迟,可通过 retries="2" 来设置重试次数。
- Failback Cluster:失败自动恢复,调用失败后台记录日志和调用信息,定时任务每隔5秒重试。通常用于消息通知操作。
- Failfast Cluster:快速失败,只会调用一次,失败后立刻抛出异常。通常用于非幂等性操作,比如新增记录。
- Failsafe Cluster:失败安全,调用出现异常,记录日志不抛出。通常用于审计日志等操作。
- Forking Cluster:并行调用多个服务提供者,通过线程池创建多个线程,并发调用多个provider,只要一个成功就返回结果。通常用于时效性较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。
- Broadcast Cluster:广播模式,逐个调用每个provider,如果其中一台报错,再循环调用结束后,抛出异常。
Dubbo 提供的负载均衡策略
Random LoadBalance(默认,基于权重的随机负载均衡机制)
- 随机,按权重设置随机概率。
- 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
RoundRobin LoadBalance(不推荐,基于权重的轮询负载均衡机制)
- 轮循,按公约后的权重设置轮循比率。
- 存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。
LeastActive LoadBalance
- 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。
- 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
ConsistentHash LoadBalance
- 一致性 Hash,相同参数的请求总是发到同一提供者。(如果你需要的不是随机负载均衡,是要一类请求都到一个节点,那就走这个一致性hash策略。)
- 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
dubbo的健壮性表现
- 监控中心宕掉不影响使用,只是丢失部分采样数据
- 数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
- 注册中心对等集群,任意一台宕掉后,将自动切换到另一台
- 注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯
- 服务提供者无状态,任意一台宕掉后,不影响使用
- 服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复
数据库同步
数据库冷备份
冷备份发生在数据库已经正常关闭的情况下,当正常关闭时会提供给我们一个完整的数据库。冷备份是将关键性文件拷贝到另外位置的一种说法。对于备份数据库信息而言,冷备份是最快和最安全的方法。
冷备份的优点:
1.是非常快速的备份方法(只需拷贝文件)
2.低度维护,高度安全。
冷备份的缺点:
1.效率低
2.由于冷备份是定期备份的,所以有可能丢失数据。
3.在实施备份的全过程中,数据库必须要作备份而不能作其它工作。
数据库热备份
热备份是在数据库运行的情况下,备份数据库操作的sql语句,当数据库发生问题时,可以重新执行一遍备份的sql语句。
热备份的优点:
1.备份时数据库仍可使用。
2.可在表空间或数据文件级备份,备份时间短。
热备份的缺点:
1.难以维护,不得以失败告终。
备份原理:
1.当数据库修改时,会将修改的信息,写入二进制日志文件中(二进制文件默认是关闭的).
2.当二进制日志文件中有数据时,数据库从库会通过IO线程读取二进制文件信息.
3.IO线程将读到的数据写到中继日志中.
4.Sql线程将中继日志中的文件 写到从数据库中,最终实现数据库主从同步.
微服务架构设计
- 当服务提供者启动时,将自己的信息注册到注册中心。
- 注册中心接收到用户的请求,更新服务列表信息。
- 当消费者启动时,先连接注册中心,再获取服务列表信息。
- 注册中心将自己的服务列表信息同步到客户端。
- 消费者收到服务列表信息,保存到本地,方便下次调用。
- 当消费者收到用户请求时,会根据负载均衡操作,选择一个服务提供者,以IP:PORT进行 RPC 远程调用。
- 当服务提供者宕机时,注册中心会根据心跳检测机制检查,如果宕机,则更新服务列表信息,并且全网广播通知所有的消费者更新服务列表信息。
什么是Spring Cloud
SpringCloud 就是微服务系统架构的一站式解决方案,提供了一套简易的编程模型,使我们能够 SpringBoot 的基础上轻松的实现微服务项目的构建。
Eureka
Eureka 是基于 REST(代表性状态转移)的服务,主要在 AWS 云中用于定位服务,以实现负载均衡和中间层服务器的故障转移。我们称此服务为 Eureka 服务器。Eureka 还带有一个基于 Java 的客户端组件 Eureka Client,它使与服务的交互变得更加容易。客户端还具有一个内置的负载平衡器,可以执行基本的循环负载平衡。在 Netflix,更复杂的负载均衡器将 Eureka 包装起来,以基于流量,资源使用,错误条件等多种因素提供加权负载均衡,以提供出色的弹性。
- 注册
服务提供者启动时,向 eureka 一次次的反复注册,直到注册成功为止 - 拉取注册表
服务发现者每30秒拉取一次注册表(刷新注册表) - 心跳检测
服务提供者每30秒内发送一次心跳数据,eureka 连续三次没收到服务的心跳,则判断服务宕机,会剔除掉这个服务 - 自我保护模式
如果由于网络不稳定或中断,15分钟内,85%以上服务器出现心跳异常,就会自动进入保护模式。
在保护模式下所有服务都不会被剔除。
网络恢复后,可以自动退出保护模式,恢复正常。
一级缓存和二级缓存
当服务拉取注册表信息时,并不是直接从Eureka的服务注册表中获取。Eureka做了二级缓存,第一级叫ReadOnly缓存,二级叫做ReadWrite缓存。
客户端会直接从ReadOnly缓存中读取注册表信息。当服务在进行注册的时候,先往服务注册表中写入注册信息,服务注册表更新了,立马会同步一份数据到ReadWrite缓存中。定时任务会去检查ReadWrite是否跟ReadOnly不一致,不一致就把数据同步到ReadOnly中。
eureka 和 zookeeper 的区别
eureka:
- 强调AP(可用性)
- 集群结构:对等结构
zookeeper:
- 强调CP(一致性)
- 集群结构:主从结构
Ribbon
RestTemplate
springboot 提供的远程调用工具
类似于 HttpClient,可以发送 http 请求,并处理响应。RestTemplate简化了Rest API调用,只需要使用它的一个方法,就可以完成请求、响应、Json转换
方法:
- getForObject(url, 转换的类型.class, 提交的参数)
- postForObject(url, 协议体数据, 转换的类型.class)
RestTemplate 和 Dubbo 远程调用的区别:
RestTemplate:
- http调用
- 效率低
Dubbo:
- RPC调用,Java的序列化
- 效率高
Ribbon 负载均衡和重试
Ribbon 对 RestTemplate 做了封装,增强了 RestTemplate 的功能,是一个客户端/进程内负载均衡器,运行在消费者端。
- 获得地址表
- 轮询一个服务的主机地址列表
- RestTemplate负责执行最终调用
Nginx 和 Ribbon 的对比
提到 负载均衡 就不得不提到大名鼎鼎的 Nignx 了,而和 Ribbon 不同的是,它是一种集中式的负载均衡器。
何为集中式呢?简单理解就是 将所有请求都集中起来,然后再进行负载均衡
Nginx 是接收了所有的请求进行负载均衡的,而对于 Ribbon 来说它是在消费者端进行的负载均衡。如下图。
请注意 Request 的位置,在 Nginx 中请求是先进入负载均衡器,而在 Ribbon 中是先在客户端进行负载均衡才进行请求的。
Ribbon 的几种负载均衡算法
- RoundRobinRule:轮询策略。Ribbon 默认采用的策略。若经过一轮轮询没有找到可用的 provider,其最多轮询 10 轮。若最终还没有找到,则返回 null。
- RandomRule: 随机策略,从所有可用的 provider 中随机选择一个。
- RetryRule: 重试策略。先按照 RoundRobinRule 策略获取 provider,若获取失败,则在指定的时限内重试。默认的时限为 500 毫秒。
Ribbon 重试
一种容错机制,当调用远程服务失败,可以自动重试调用
Hystrix
Hystrix 就是一个能进行 熔断 和 降级 的库,通过使用它能提高整个系统的弹性。
熔断
- 当访问量过大,出现大量失败,可以做过热保护,断开远程服务不再调用
- 限流
- 防止故障传播、雪崩效应
降级
- 调用远程服务失败(宕机、500错、超时),可以降级执行当前服务中的一段代码,向客户端返回结果
- 快速失败
降级是为了更好的用户体验,当一个方法调用异常时,通过执行另一种代码逻辑来给用户友好的回复。这也就对应着 Hystrix 的 后备处理 模式。
所谓 熔断 就是服务雪崩的一种有效解决方案。当指定时间窗内的请求失败率达到设定阈值时,系统将通过 断路器 直接将此请求链路断开。
hystrix dashboard 断路器仪表盘
Open Feign
OpenFeign 也是运行在消费者端的,使用 Ribbon 进行负载均衡,所以 OpenFeign 直接内置了 Ribbon。
集成工具
- 远程调用:声明式客户端
- ribbon 负载均衡和重试
- hystrix 降级和熔断
Feign默认不启用Hystrix,不推荐启用Hystrix
Zuul
微服务网关,简单来讲网关是系统唯一对外的入口,介于客户端与服务器端之间,用于对请求进行鉴权、限流、 路由、监控等功能。路由与过滤是Zuul的两大核心功能,路由功能负责将外部请求转发到具体的服务实例上去,是实现统一访问入口的基础,过滤功能负责对请求进行额外的处理,是请求校验过滤及服务聚合的基础。
过滤器类型
- pre:在请求被路由到目标服务前执行,比如权限校验、打印日志等功能;
- routing:在请求被路由到目标服务时执行,这是使用Apache HttpClient或Netflix Ribbon构建和发送原始Http请求的地方;
- post:在请求被路由到目标服务后执行,比如给目标服务的响应添加头信息,收集统计数据的功能;
- error:请求在其他阶段发送错误时执行。
API 网关
- 微服务系统统一的调用入口
- 统一的权限校验
- 集成ribbon
- 集成hystrix
默认启用了 ribbon 的负载均衡,默认不启用重试,zuul不推荐启用重试,Zuul默认启用了hystrix
Zuul 和 Feign 的区别
zuul
- 部署在所有微服务项目之前
- 网关一般是一个独立的服务,与业务无关
- 不推荐启用重试
会造成后台服务压力翻倍
重试尽量不部署在最前面,越往后越好
feign
- 部署在微服务内部,服务和服务之间调用
- 不推荐启用 hystrix
一般在最前面进行降级和熔断,类似于电箱断路器,只在入户位置部署,
不在微服务内部部署hystrix,否则会引起混乱
Turbine
聚合 Hystrix 监控数据,连接多台服务器,抓取日志数据,进行聚合,交给仪表盘在同一个监控界面进行展现
Config
Spring Cloud Config 为分布式系统中的外部化配置提供服务器和客户端支持。使用 Config 服务器,可以在中心位置管理所有环境中应用程序的外部属性。
Spring Cloud Config 就是能将各个 应用/系统/模块 的配置文件存放到 统一的地方然后进行管理
Bus
用于将服务和服务实例与分布式消息系统链接在一起的事件总线。在集群中传播状态更改很有用(例如配置更改事件)。
可以简单理解为 Spring Cloud Bus 的作用就是管理和广播分布式系统中的消息,也就是消息引擎系统中的广播模式。当然作为 消息总线 的 Spring Cloud Bus 可以做很多事而不仅仅是客户端的配置刷新功能。
而拥有了 Spring Cloud Bus 之后,我们只需要创建一个简单的请求,并且加上 @ResfreshScope 注解就能进行配置的动态修改了,下面我画了张图供你理解。
Gateway
Gateway采用异步非阻塞IO模型,支持异步通信。内部实现了限流和负载均衡。其中限流算法采用的是令牌桶算法。
Nacos核心架构图
Sentinel
Sentinel是一款面向分布式服务架构的轻量级流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统自适应保护等多个维度来保障服务的稳定性,核心思想是:根据对于资源配置的规则来为资源执行相应的流控/降级/系统保护策略。
Sentinel的限流算法: 滑动窗口算法。指某个应用限流控制1分钟最多允许600次访问,采用滑动窗口算法是将每1分钟拆分为6个等份时间段,每个时间段为10秒,6个时间段为1组,这一组区域就是滑动窗口。每产生1个访问在对应时间段的计数器加1,当滑动窗口所有时间段的计数器总和超过600,后面新的访问将被限流拒绝。同时每过10秒。滑动窗口向右移动。前面的过期时间段计数器作废。
滑动窗口算法的理念是将整段时间均分后独立计算再汇总统计。
Seata
Seata是一款开源的分布式事务解决方案,致力于提供高新能和简单易用的分布式事务服务。Seata提供了AT、TCC、SAGA和XA事务模式。AT模式是阿里首推的模式。
AT模式是一种改进后的两阶段提交。
- 一阶段:业务数据和回滚日志在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:分布式事务操作成功,则事务协调者通知资源管理器异步删除undolog。
优点:
- 应用层基于sql解析实现了自动补偿,从而最大程度的降低业务侵入性。
- 将分布式事务中事务协调者独立部署,负责事务的注册、回滚。
- 通过全局锁实现了写隔离和读隔离。
为什么要使用消息队列?
- 通过异步处理提高系统性能(减少响应所需时间)
- 削峰/限流:先将短时间高并发产生的事务消息存储在消息队列中,然后后端服务再慢慢根据自己的能力去消费这些消息,这样就避免直接吧后端服务打垮掉。
- 降低系统耦合性:生产者发送消息到消息队列中,消费者处理消息,直接去消息队列中取消息即可不需要和其他系统有耦合,提高了系统的扩展性。
使用消息队列会带来哪些问题?
- 系统可用性降低:引入消息队列后,如果消息队列挂掉,会影响到系统的可用性。
- 系统复杂性增加:加入MQ之后,需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性。
- 一致性问题:异步确实可以提高系统的响应速度。如果消息的的真正消费者没有正确消费消息,这样就会导致数据不一致的情况。
RabbitMQ怎么保证消息不丢失?
- 确保消息到MQ:confirm机制,属于异步方式,消息发送完之后不需要阻塞等待。当消息达到指定的队列之后,mq将会主动回传一个ack,代表消息入队成功。
- 确保队列和消息持久化:开启交换机、队列与消息的持久化,三者缺一不可。消息刷盘后,再批量异步回传ack。
- 确保消息从队列中传到消费端:开启手动ack模式,当消息正确处理完成后,再通知mq。消费端处理消息异常后,回传nack,这样mq会把这条消息投递到另外一个消费端。
如何避免消息重复消费
在消息生成时,MQ内部针对每条生产者发送的消息生成一个唯一id,作为去重和幂等的依据,避免重复的消息进入队列。
在消息消费时,要求消息体中也要有全局唯一id作为去重和幂等的依据,避免同一条消息被重复消费。
MQ中大量数据积压,该如何解决?
如果是线上突发问题,要临时扩容,增加消费端的数量。扩容消费者的实例同时,必须同步扩容主题Topic的分区数量,确保消费者的实例数和分区数相等。如果消费者的实例数超过了分区数,由于分区是单线程消费,所以扩容就没效果。
后续通过监控,日志等手段分析,优化消费端的业务处理逻辑。可以判断消息的发送时间和当前时间,如果超过了2分钟,则直接将数据转为Json写入Redis,查询可以先查询数据库,没有找到再查询缓存,同时需要定时任务去处理Redis的数据写入数据库。
MQ中的消息过期失效了怎么办?
如果消息在Queue中积压超过一定的时间就会被RabbitMQ给清理掉,这个数据就没了。大量的数据就会直接被搞丢。
我们可以采取一个方案,就是批量重导。大量数据积压时,直接写到数据库,然后过了高峰期再重新灌入MQ中。
事务消息如何实现?
- 生产者产生消息,发送一条半事务消息到MQ服务器
- MQ收到消息后,将消息持久化,此时消息为待发送状态
- MQ服务器返回ACK确认到生产者
- 生产者执行本地事务
- 本地事务执行成功,commit执行结果到MQ服务器,如果失败,发送rollback
- 如果正常commit,MQ服务器更新消息状态为可发送,如果是rollback,则删除消息
- 如果消息状态更新为可发送,则MQ服务器push消息到消费者,消费者消费完则返回ACK
- 如果MQ服务器长时间没有收到生产者的commit或rollback,它会反查生产者,根据查询的结果执行最终状态。
事务的实现主要是对信道的设置,主要的方法有三个:
channel.txSelect()声明启动事务模式
channel.txCmmint()提交事务
channel.txRollback()回滚事务
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。