synchronized
基于JVM的对象监视器和操作系统的互斥锁,每个对象都关联一个对象监视器,线程视图进入synchronized
代码块或方法时,会请求锁定当前对象的监视器;监视器锁又依赖于底层操作系统的 Mutex Lock(互斥锁)来实现,指令层面是通过
monitorenter
和monitorexit
实现。
Synchronized
通过对象内部的**监视器锁(monitor)**实现,监视器锁又依赖于底层操作系统的 Mutex Lock(互斥锁)来实现。而操作系统实现线程之间的切换需要从用户态转换到核心态,成本非常高。
synchronized
代码块或方法的权限。同一时间,只允许一个线程持有特定对象的监视器。synchronized
代码块或方法时,它会请求锁定当前对象对应的监视器。monitorenter
:尝试获取对象监视器,如果成功则继续执行同步代码,否则线程会被阻塞直到监视器可用。monitorexit
:同步代码块或方法执行完毕后,释放对象监视器。synchronized
支持可重入,已持有某个对象监视器的线程,再次请求该监视器仍能成功,从而避免死锁。任务提交到线程池后,先判断当前当前线程数是否小于corePoolSize,是则创建线程来执行任务,否则将任务放入队列,如果队列满了,则判断当前线程数是否小于最大线程数,是则创建线程执行任务,否则调用拒绝策略。
七大参数:核心线程数、最大线程数、线程存活时间、单位、工作队列、线程工厂、拒绝策略(线程耗尽,队列满了)
1、直接抛异常,默认,拒绝执行异常
2、调用者线程去执行
3、丢弃最老的任务
4、丢弃最新的任务
5、自定义
RejectedExecutionException
异常。切面、通知、连接点、代理、织入
Spring AOP是通过代理模式来实现的。以下是Spring AOP实现的基本步骤和原理:
java.lang.reflect.Proxy
类来创建代理对象。以下是一个简单的Spring AOP实现示例:
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Executing: " + joinPoint.getSignature().getName());
}
}
在这个例子中:
LoggingAspect
类是一个切面,它包含一个前置通知。@Before
注解表示这是一个前置通知,将在方法执行前被调用。"execution(* com.example.service.*.*(..))"
是一个切入点表达式,它匹配com.example.service
包下所有类的所有方法。要启用AOP并应用这个切面,你需要在Spring配置中添加如下内容:
<aop:config>
<aop:aspect ref="loggingAspect">
<aop:before method="logBefore" pointcut="execution(* com.example.service.*.*(..))"/>
aop:aspect>
aop:config>
<bean id="loggingAspect" class="com.example.aspect.LoggingAspect"/>
或者在Java配置中:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
@Bean
public LoggingAspect loggingAspect() {
return new LoggingAspect();
}
}
通过以上步骤,当你调用com.example.service
包下的某个方法时,Spring AOP会通过代理对象拦截这个调用,并在方法执行前调用logBefore
方法。这就是Spring AOP的基本实现原理。
JDK动态代理:基于Java反射机制实现,只能对实现了接口的类生成代理;
CGLIB动态代理:使用ASM库通过字节码操作技术在运行时生成目标类的子类。
动态代理在Java中有两种主要的实现方式:JDK动态代理和CGLIB动态代理。
java.lang.reflect.Proxy
类和java.lang.reflect.InvocationHandler
接口来创建代理对象。当通过代理对象调用方法时,实际的调用会被转发到InvocationHandler
的invoke()
方法中,在这个方法里可以添加额外的逻辑,如权限检查、日志记录等。final
修饰的类或方法。区别:
在实际使用中,根据具体的需求和场景选择合适的动态代理实现方式。如果目标类已经实现了接口,且不需要代理final方法,可以选择JDK动态代理;如果目标类没有实现接口或者需要代理final方法,那么应该选择CGLIB动态代理。在Spring框架中,AOP模块默认优先使用JDK动态代理,当目标类没有实现接口时,会自动切换到CGLIB动态代理。
Producer:消息生产者,就是向 kafka broker 发消息的客户端。
Consumer:消息消费者,向 kafka broker 取消息的客户端。
Topic:可理解为一个队列,一个 Topic 又分为一个或多个分区,
Consumer Group:这是 kafka 用来实现一个 topic 消息的广播(发给所有的 consumer)和单播(发给任意一个 consumer)的手段。一个 topic 可以有多个 Consumer Group。
Broker:一台 kafka 服务器就是一个 broker。一个集群由多个 broker 组成。一个 broker 可以容纳多个 topic。
Partition:为实现扩展性,一个 topic 可分布到多个 broker上,每个 partition 是一个有序的队列。partition 中的每条消息都会被分配一个有序的id(offset)。将消息发给 consumer,kafka 只保证按一个 partition 中的消息的顺序,不保证一个 topic 的整体(多个 partition 间)的顺序。
Offset:kafka 的存储文件都是按照 offset.kafka 来命名,用 offset 做名字的好处是方便查找。
分区:kafka将topic分为多个partition,每个partition内严格按照顺序排序,就是说同一partition,先发布的消息offset小于后发布的;
键控分区:生产者通过键控分区的方式决定消息发到哪个partition;
消费者消费顺序:同一消费者消费同一分区消息是按offset顺序消费的;
持久化存储:将所有发布到其上的消息都持久化存储在磁盘上。
副本机制:每个topic下的每个分区都有多个副本。其中一个副本被选为主副本(Leader),负责读写请求;其他副本是追随者副本(Follower)。当主副本出现故障时,Kafka会自动从追随者副本中选举新的主副本继续服务,从而提供容错能力。
ISR集合:ISR集合包含了与主副本保持同步的所有追随者副本。只有当一个消息被成功写入ISR中的所有副本后,生产者才会收到确认。这样可以保证即使有节点故障,数据也不会丢失。
确认应答:生产者设置acks参数,控制需要多少副本确认接受,才认为写入成功,
acks=all
意味着所有ISR中的副本都要确认接收,这是最高的可靠级别。事务模式:生产者可启用事务模式,确保一系列消息要么全部成功提交,要么全部回滚,避免部分消息丢失或重复。
消费者位移管理:消费者在消费消息后提交自己的消费位移(offset),以此记录已消费消息的位置。如果消费者在消费完消息并提交位移之前发生故障,那么在恢复后它可以从上次提交的位移处继续消费,从而避免重复消费或者漏消费。
重试和幂等性:生产者可通过配置实现重试逻辑,结合幂等性功能,在网络异常或其他问题导致消息投递失败时,能安全地重新尝试发送消息而不会导致重复。
Kafka高性能
批处理:可批量发送消息,而不是逐一发送,减少网络I/O次数,提高了带宽利用率。
零拷贝:数据传输时采用操作系统层面的零拷贝技术,减少CPU将数据从内核空间拷贝到用户空间的开销。
磁盘顺序写:消息存储基于磁盘,以追加的方式写入文件,而非随机写入,磁头不需寻道,只需向同一方向移动。
高效索引结构:对消息采用稀疏索引结构,从而快速定位消息。
多线程与异步处理:生产者和消费者都支持异步收发消息。
分区与分布式架构:水平扩展,随着硬件增加而线性提升性能;
消息压缩:使用GZIP算法,减少网络传输数据量。
Eureka、Ribbon、Hystrix、Zuul、Feign、
Spring Cloud是一套基于Spring Boot实现的微服务架构解决方案,它包含了一系列为构建分布式系统和服务治理而设计的组件和工具集。以下是一些核心组件及其功能:
随着Spring Cloud的演进,一些基于Netflix OSS的组件逐渐被替换或不再推荐使用,比如Netflix Eureka已被Spring Cloud自带的服务发现组件Spring Cloud Consul或Spring Cloud Zookeeper取代,而Spring Cloud LoadBalancer替代了Ribbon的部分功能。同时,Spring Cloud也引入了新的组件如Spring Cloud Gateway、Spring Cloud Kubernetes等以适应云原生时代的需求。
未提交读:最低隔离级别,事务未提交前,就可被其他事务读取。
提交读:一个事务提交后才能被其他事务读取到。
可重复读:默认级别,保证多次读取同一个数据时,其值都和事务开始时候的内容是一致,禁止读取到别的事务未提交的数据。
序列化:代价最高最可靠的隔离级别。
脏读 :表示一个事务能够读取另一个事务中还未提交的数据。比如,某个事务尝试插入记录 A,此时该事务还未提交,然后另一个事务尝试读取到了记录A。
不可重复读 :是指在一个事务内,多次读同一数据。
幻读 :指同一个事务内多次查询返回的结果集不一样。比如第一次查询有 n 条记录,第二次查询却有 n+1 条记录,好像产生了幻觉。
不可重复读和幻读的区别:不可重复读的重点是修改;两次读取的值不一样。幻读的重点在于增删;两次读出来的记录数不一样。从控制角度来看,不可重复读只需要锁住满足条件的记录,幻读要锁住满足条件及其相近的记录。
EXPLAIN语句用于分析SQL执行计划
type
:ALL(全表扫描)、index(全索引扫描)、range(索引范围扫描)、ref(非唯一性索引扫描,通过某个列的值引用)
possible_keys
:可能用到的索引列表。
key
:实际使用的索引,如果没有使用索引则显示NULL。
key_len
:使用的索引长度。
filtered
:在经过此表之后,按照条件过滤后的行的比例。
rows
:根据统计信息估算出需要读取的行数。rows<1000,是在可接受的范围内的其他字段还包括
extra
,包含一些额外的信息,如Using index
(覆盖索引),Using filesort
(外部排序),Using temporary
(使用临时表)等。
filesort
:如果一个查询包含ORDER BY
子句且没有合适的索引来满足排序需求,或者虽然有索引但执行计划决定了使用全表扫描而非索引访问方法,MySQL会将符合条件的记录读取到内存中的临时空间(如sort buffer)或者外部磁盘文件上进行排序。
数据类型:
- PgSQL支持丰富的数据类型,包括JSONB、数组、范围类型、窗口函数、全文搜索、地理空间索引等功能。
- MySQL的数据类型相对简单,但同样支持JSON字段类型、地理位置索引等。
事务中DDL语句:
PgSQL在事务处理方面更加严谨
缓存同一时间大面积失效
- 过期时间设置随机
- 热点数据均匀分布在不同的缓存数据库中
- 热点数据永不过期
- hystrix限流&降级
大量请求的key不存在
- 参数校验
- 缓存无效key:如果缓存和数据库都查不到某个key 的数据就写一个到redis 中去并设置过期时间。
- 布隆过滤器
某key失效时大量该Key请求过来
- 热点数据永不失效。
- 增加互斥锁。
1、读写串行化(吞吐量太低)
2、先删缓存再更新数据库
- 删缓存和更新数据库的间隙,别的线程查库回写
- 延时双删解决(先删缓存,再更新数据库,一秒后再删缓存;异步二删)
- 如果二删失败,也会出现不一致
3、先更新数据库、再删缓存
- 更新数据库前有线程查了旧值,删缓存后将旧值回写(读远快于写,所以基本不会出现该问题)
- 非要解决的话就使用异步延时双删
4、如何解决删缓存失败的情况
- 将要删的key放入MQ,让缓存自己消费消息删key,失败则重试直到成功
- 使用MySQL的canal订阅binlog日志,提取需要删除的key,发送给MQ,缓存自己消费消息删key
加锁机制:若客户端面对的是redis cluster集群,会根据hash节点选择一台机器,然后发送一段lua脚本,保证原子性,脚本中KEYS[1]表示加锁的key,ARGV[1]表示锁生存时间,ARGV[2]表示加锁客户端ID。再来一个客户端想要加锁时,同样发送一段lua脚本,先判断锁key是否已存在,存在的话再判断ARGV[2]是否包含该客户端ID,不包含的话客户端会收到该锁key的剩余生存时间,然后进入while循环,不断尝试加锁。
可重入性支持: Redisson支持可重入的分布式锁,Redisson中包含一个计数器,每次成功获取锁时递增计数器,每次释放锁时递减计数器,当计数器降为0时,删除锁。
锁的自动续期: 为了避免由于网络延迟、GC暂停等原因导致锁在使用过程中过期,Redisson提供了锁的自动续期功能。当客户端持有锁时,Redisson会在后台定期为锁续期,确保锁在使用期间不会因为超时而被其他客户端获取。
RedLock算法: Redisson还支持RedLock算法,在RedLock算法中,Redisson会在多个独立的Redis实例上尝试获取锁,只有在大多数实例上成功获取锁时才认为获取锁成功。这种机制提高了分布式锁的可用性和容错性。
IO多路复用是一种高效的I/O处理技术,其原理主要是通过一种机制让单个线程能够同时监控多个文件描述符(FD,File Descriptor)的I/O状态,从而实现高效、并发的网络通信。
poll和epoll都是Linux系统中的I/O多路复用技术,用于监控多个文件描述符(通常是网络套接字)的事件状态,如读就绪、写就绪等。
poll
系统调用允许程序同时监控多个文件描述符的状态变化。poll
时,需要先创建一个包含待监控文件描述符的数组,并为每个描述符指定感兴趣的事件类型。然后调用poll
函数,它会阻塞直到有至少一个描述符的事件发生或超时。poll
返回后,需要遍历整个数组来检查哪些描述符的事件已经发生。epoll
是Linux内核在2.6版本中引入的一种更高效的I/O多路复用机制。epoll
时,首先创建一个epoll
实例,然后通过epoll_ctl
函数将待监控的文件描述符添加到这个实例中,并指定感兴趣的事件类型。epoll_wait
函数会阻塞直到有至少一个描述符的事件发生、超时或者被显式唤醒。epoll_wait
返回后,它会提供一个事件列表,这个列表只包含了那些真正发生了事件的文件描述符,因此无需像poll
那样遍历整个描述符集合。关于epoll是同步还是异步的问题:
epoll_wait
时会阻塞等待事件发生。epoll_wait
返回后,应用程序可以立即处理已就绪的文件描述符,而不需要等待I/O操作完成。同时,未就绪的描述符可以继续被epoll监控,等待下次事件发生。总结来说,epoll本身是一个同步I/O模型,但可以通过与非阻塞I/O的结合,在应用程序层面实现类似于异步I/O的处理方式。
poll 和 epoll 是Linux系统中的两种I/O多路复用技术,允许单进程同时监控多个文件描述符(如网络套接字)的读写事件状态,从而在大量并发连接中有效地管理I/O操作。以下是它们各自的工作流程:
先创建一个包含所有待监控文件描述符的数组,并为每个描述符指明感兴趣的事件类型;
调用poll后,会阻塞,直到至少一个描述符发生事件或超时;
poll返回后,遍历整个数组来查看哪些描述符有事件发生。
初始化: 创建一个pollfd
结构体数组,每个元素表示要监控的一个文件描述符及其关注的事件类型(可读、可写等)。
调用poll函数: 调用poll()
函数,传入这个结构体数组和等待事件发生的超时时间。例如:
struct pollfd fds[FD_SETSIZE];
int nfds = ...; // 数组中有效的文件描述符数量
int timeout = ...; // 超时时间(毫秒)
int ready_fds = poll(fds, nfds, timeout);
内核处理:
pollfd
结构体数组中对应元素的revents
字段。轮询结果:
poll()
返回值为准备就绪的文件描述符数量,如果超时则返回0,若发生错误则返回负数。pollfd
数组中的revents
字段来判断哪些文件描述符上有事件发生,并进行相应的处理。先创建epoll实例,再通过epoll_ctl方法传入待监控文件描述符;
调用epoll_wait,阻塞直到至少有一个描述符的事件发生、超时或显式唤醒;
epoll_wait返回后,提供了一个事件列表,该列表只包含发生了事件的描述符。
创建epoll实例: 首先调用epoll_create()
或者epoll_create1()
创建一个epoll实例,返回一个epoll句柄。
添加/修改监控事件: 对于需要监控的文件描述符,调用epoll_ctl()
函数将其添加到epoll实例中,并指定感兴趣的事件类型(EPOLLIN、EPOLLOUT等):
int epoll_fd = epoll_create1(0); // 创建epoll实例
struct epoll_event event;
event.events = EPOLLIN | EPOLLET; // 设置事件类型
event.data.fd = socket_fd; // 关联的文件描述符
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fd, &event); // 添加到epoll实例
等待事件: 调用epoll_wait()
函数,传入epoll句柄以及用于存放事件的缓冲区和超时时间,等待事件发生:
struct epoll_event events[MAX_EVENTS];
int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, timeout);
内核处理:
处理就绪事件:
epoll_wait()
返回值是准备就绪的事件数量。events
数组,根据其中的data.fd
找到就绪的文件描述符,并依据events[i].events
字段确定发生了何种类型的事件,然后进行相应处理。epoll相比poll的一大改进在于其高效的事件通知机制,特别是在大量并发连接的情况下,避免了不必要的线性扫描,极大地提升了性能。
Redis淘汰策略(LRU最近最少使用,关键是看数据最后一次被使用到发生替换的时间长短,时间越长,数据就会被删除;LFU是淘汰一段时间内,使用次数最少的页面。)
- 内存满时,再执行写入,直接报错
- 从已设TTL的键中挑选LRU的键进行删除
- 所有键中挑选LRU的键进行删除
- 从已设TTL的键中挑选LFU的键进行删除
- 所有键中挑选LFU的键进行删除
- 随机删除一个设置了TTL的键
- 删除生存时间(TTL)最小的键
Redis淘汰策略是指在内存使用达到最大限制(由maxmemory
配置决定)时,为保证服务的持续运行而采取的一种删除数据的方法。当Redis数据库的内存占用超过预设的最大值时,需要通过选择合适的淘汰策略来移除部分键值对以释放内存空间。
以下是Redis支持的多种淘汰策略:
每种策略都有其适用场景和优缺点,根据应用需求的不同,可以选择适合的淘汰策略以优化缓存命中率、减少数据丢失或平衡系统负载等。在实际应用中,通常建议为Redis实例设置合理的maxmemory
限制,并结合业务特点选择合适的淘汰策略。
The end.