前言
同学们好,这里是佩恩的博客,最近抽空面试了一下Java高级开发的岗位,总体来说面试题比前几年要深入很多,奉劝各位能苟则苟,但一定不要忘记学习技术,以免突然的大礼包让你手足无措,下面就直接进入正题吧。
面试题汇总
1.线程池参数及作用
2.任务产生是50/s,持续不断产生,处理时间为30s,如何设定线程池参数
3.JVM参数在Docker部署和主机部署有什么区别
4.Dubbo服务如何实现双注册中心注册
5.MySQL性能优化
6.Nacos在线更新配置实现流程
7.JVM如何调优
8.如果一直产生FullGC,如何定位并处理问题
9.Spring如何解决循环依赖
10.Redis数据类型有哪些?Hash类型如何实现的?
11.Spring事务传播机制详解
12.雪花算法产生的ID会重复吗?
1.线程池属性
在IDEA搜索ThreadPoolExecutor,Alt+7 或者 Ctrl+F12,查看方法及属性,可以看到属性相当的多
以下是线程池核心参数
ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
corePoolSize:核心线程数
- 核心线程在空闲状态下,也不会被销毁
- 当线程池线程数小于corePoolSize时会创建
maximumPoolSize:最大线程数
- 当线程池中的线程数达到 maximumPoolSize 时,线程池不再创建新线程,任务会被放入任务队列等待执行。
- 如果任务队列已满且线程数达到 maximumPoolSize,新任务会被拒绝,并由 RejectedExecutionHandler 处理。
keepAliveTime:存活时间
- 非核心线程的空闲存活时间,比如设置60秒,非核心线程在空闲60秒后会被销毁,避免占用过多资源
unit:时间单位
- 这是一个枚举类,包括纳秒、微秒、毫秒、秒、分、时、天
- 建议TimeUnit.SECONDS
workQueue:工作队列或称为任务队列
- 用于存储等待执行的任务
常见的任务队列实现
- ArrayBlockingQueue:基于数组的有界阻塞队列,固定容量,先进先出
- LinkedBlockingQueue:基于链表的可选容量阻塞队列,容量默认为Integer.MAX_VALUE
- SynchronousQueue:不存储任务的阻塞队列,每个任务必须立即被线程处理
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列
选择合适的任务队列对线程池的性能和行为有重要影响
threadFactory:线程工厂
- 用于创建线程
- 可以自定义
handler:拒绝策略处理器
常见的拒绝策略
- AbortPolicy:默认策略,直接抛出 RejectedExecutionException
- CallerRunsPolicy:由调用线程执行任务,如主线程
- DiscardPolicy:直接丢弃任务,不报异常
- DiscardOldestPolicy:丢弃队列中最老的任务,然后尝试提交新任务
2.任务产生是50/s,持续不断产生,处理时间为30s,如何设定线程池参数
当任务产生速率为 50/s(每秒产生 50 个任务),且每个任务的处理时间为 30 秒时,线程池的配置需要确保任务能够被及时处理,同时避免资源过度使用。以下是详细的分析和建议:
1. 关键参数分析
(1)任务产生速率
- 每秒产生 50 个任务。
- 每小时产生 50×3600=180,000 个任务。
(2)任务处理时间
- 每个任务的处理时间为 30 秒。
- 每个线程每小时可以处理 3600/30=120 个任务。
(3)系统处理能力
- 每小时需要处理的任务总数为 180,000 个。
- 每小时每个线程可以处理 120 个任务。
- 因此,需要的线程数为180000/120=1,500 个线程
2. 线程池参数配置
(1)corePoolSize
- 建议值:500
- 说明:核心线程数是线程池中始终存在的线程数量。设置一个合理的初始值,确保有足够的线程立即处理任务。500 个核心线程可以处理大部分任务,同时避免过多线程占用资源。
(2)maximumPoolSize
- 建议值:1500
- 说明:最大线程数是线程池中允许的最大线程数量。设置为 1500 个线程,以应对突发的任务高峰。
(3)keepAliveTime
- 建议值:60 秒
- 说明:非核心线程的空闲存活时间。设置为 60 秒,确保非核心线程在空闲 60 秒后被销毁,避免过多线程占用资源。
(4)unit
- 建议值:TimeUnit.SECONDS
(5)workQueue
- 建议值:LinkedBlockingQueue 或 ArrayBlockingQueue
- 容量:建议设置为 10,000 或更高。
- 说明:任务队列用于存储等待执行的任务。由于任务产生速率较高,任务队列需要有足够的容量来缓冲任务。
(6)threadFactory
- 建议值:自定义线程工厂。
- 说明:自定义线程工厂可以为线程设置合适的名称和优先级,便于调试和监控。
(7)handler
- 建议值:CallerRunsPolicy
- 说明:当任务队列已满且线程数达到 maximumPoolSize 时,使用 CallerRunsPolicy 由调用线程执行任务,避免任务被拒绝。
3. 示例代码
以下是一个配置线程池的示例代码:
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
500, // corePoolSize
1500, // maximumPoolSize
60, // keepAliveTime
TimeUnit.SECONDS, // unit
new LinkedBlockingQueue<>(10000), // workQueue
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "CustomThread-" + Thread.activeCount());
}
}, // threadFactory
new ThreadPoolExecutor.CallerRunsPolicy() // handler
);
// 提交任务
for (int i = 0; i < 50; i++) {
int taskNumber = i;
executor.execute(() -> {
System.out.println("Executing task " + taskNumber + " by " + Thread.currentThread().getName());
try {
Thread.sleep(30000); // 模拟任务处理时间 30 秒
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
executor.shutdown();
}
}
4. 注意事项
(1)资源限制
- 内存:确保服务器有足够的物理内存来支持大量的线程。每个线程默认占用 1MB 的堆栈空间,1500 个线程可能需要 1.5GB 的内存。
- CPU:确保服务器有足够的 CPU 核心来支持高并发处理。如果 CPU 核心数不足,线程可能会频繁上下文切换,导致性能下降。
(2)任务队列容量
- 队列大小:任务队列的容量需要足够大,以避免任务被拒绝。如果队列已满,任务会被拒绝执行,可以使用 CallerRunsPolicy 由调用线程执行任务。
(3)拒绝策略
- 选择合适的拒绝策略:使用 CallerRunsPolicy 可以避免任务被拒绝,但可能会导致调用线程阻塞。如果任务产生速率过高,可以考虑使用其他拒绝策略(如 DiscardPolicy)。
(4)优化任务处理
- 并行处理:如果可能,优化任务的处理逻辑,减少每个任务的处理时间。例如,通过并行处理或优化算法来提高效率。
- 批量处理:将多个任务批量处理,减少任务提交的频率。
(5)监控和调整
- 监控工具:使用监控工具(如 JMX、VisualVM)实时监控线程池的状态,动态调整参数。
- 日志记录:记录任务处理的详细日志,便于分析和排查问题。
5. 总结
通过合理配置线程池参数,可以有效应对高任务产生速率和长任务处理时间的场景,确保系统能够高效运行。以下是一些关键点:
- 核心线程数:设置合理的初始值,确保有足够的线程立即处理任务。
- 最大线程数:设置为计算出的所需线程数,以应对突发的任务高峰。
- 任务队列:设置足够大的任务队列,避免任务被拒绝。
- 拒绝策略:选择合适的拒绝策略,确保系统的稳定性。
- 资源监控:实时监控系统的资源使用情况,动态调整参数。
通过这些措施,可以确保线程池在高负载下稳定高效地运行
3.JVM参数在Docker部署和主机部署有什么区别
(1) 命令有所不同
如堆大小限制
- 主机部署使用-Xms(初始堆大小)、-Xmx(最大堆大小) 来限制JVM访问主机的物理内存大小
- 容器使用 --memory 限制容器内存使用
(2) 内存管理机制
- 主机部署JVM 默认直接读取宿主机物理内存,可能导致容器内存超限触发 OOM
- 容器部署使 JVM 识别容器内存限制,可以避免内存溢出。
(3) CPU资源限制
-XX:ParallelGCThreads(并行垃圾回收线程数)、-XX:ConcGCThreads(并发垃圾回收线程数)
- 主机部署限制CPU资源使用,如:-XX:ParallelGCThreads=8 -XX:ConcGCThreads=4
- 容器部署通过 --cpus 参数限制容器的 CPU 使用,如:docker run --cpus=4 my-java-app
(4) 参数传递
- 主机部署通过启动脚本或命令行直接传递 JVM 参数
- 容器部署通过环境变化量或Dockerfile、docker-compose.yml等文件
(5) 日志
- 主机部署的日志路径一般由 logback.xml 文件或启动文件指定
- 容器部署的日志存放于Docker文件夹内
4.Dubbo服务如何实现双注册中心注册
原文地址:blog.csdn.net/qq_43012298…
下面我们以 Zookeeper 和 Nacos 为案例进行Springboot项目做双注册中心注册
主要有以下几个步骤:引入依赖、编写配置文件、定义接口、启动服务
1.引入依赖,pom文件引入 Zookeeper 和 Nacos 以及Springboot等依赖
2.配置注册中心:在 application.yml 文件中配置多个注册中心
3.定义服务接口和实现:通过 @DubboService 注解指定注册中心
服务接口:
服务实现:
4.启动服务提供者和消费者:编写启动类,启动Springboot项目
5.MySQL性能优化
与MySQL性能相关的方面主要有:建表、sql、表分割、硬件
建表相关:表字段类型设置、引擎、索引、冗余列、派生列
sql:是否使用索引、执行语句调优
表分割:单表数据量过大,达到性能瓶颈,需要分表,读写分离,数据库集群等
硬件:增大mysql占用内存、增加更快的硬盘
详情请看我的掘金博客:juejin.cn/post/749396…
6.Nacos在线更新配置实现流程
主要实现步骤:引入依赖、填写配置、编写配置类
1.引入依赖:Nacos 和 Springboot
2.编写配置文件:在 application.yml 中填写
3.使用@ConfigurationProperties 或 @Value 注解来绑定配置值,并通过 @RefreshScope 来支持动态刷新
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
@RefreshScope
@Component
public class AppConfig {
@Value("${app.name}")
private String appName;
@Value("${app.version}")
private String appVersion;
// Getters and Setters
}
4.可以自行编写测试类来测试是否能动态获取参数值,并通过 spring.cloud.nacos.config.refresh-time 来调整轮询频率
7.JVM如何调优
7.1 JVM 调优能达到什么样的效果?
提升性能、减少内存泄漏风险、合理设置堆内存和元数据区大小以避免 OOM,降低GC频率,改善用户体验
7.2 JVM 哪些区域可以调优?
堆内存、垃圾回收、新生代、老年代、线程等
(1) 设置堆内存大小
- -Xms:初始堆大小
- -Xmx:最大堆大小
建议:将 -Xms 和 -Xmx 设置成相同的值,避免堆大小动态扩展带来的性能开销,根据应用的内存需求和服务器内存大小,合理分配堆内存,对于内存密集型应用,可以设置堆内存大小为服务器内存的50%~70%
(2) 垃圾回收
调优领域:内存、锁竞争、cpu占用、io
最快的GC是不发生GC,当然这不可能发生,可以尽量减少,如果频繁发生 Full GC,考虑以下几个问题:
- 数据是不是太多?
-
- resultSet = statement.executeQuery(“select * from 大表 limit n”)
- 数据表示是否太臃肿
-
- 对象图
- 对象大小 16 Integer 24 int 4
- 是否存在内存泄漏
-
- static Map map ...
- 软引用
- 弱引用
- 第三方缓存实现
(3) 新生代调优
新生代的特点
- 所有的 new 操作的内存分配非常廉价
-
- TLAB thread-local allocation buffer
- 死亡对象的回收代价是零
- 大部分对象用过即死
- Minor GC 的时间远远低于 Full GC
新生代理想大小:
- 新生代能容纳所有【并发量*(请求-响应)】的数据
- 幸存区大到能保留【当前活跃对象+需要晋升对象】
- 晋升阈值配置得当,让长时间存活的对象尽快晋升
-XX:MaxTenuringThreshold=threshold
-XX:+PrintTenuringDistrubution
(4) 老年代调优
以CMS为例
- CMS 的老年代内存越大越好
- 先尝试不做调优,如果没有 Full GC 那么已经...,否则先尝试调优新生代
- 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4~1/3
-
- -XX:CMSInitiatingOccupancyFraction=percent
8.生产环境一直产生FullGC,如何定位并处理问题
(1) 开启 GC 日志,在 JVM 启动参数中添加以下选项
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCDateStamps
-Xloggc:/path/to/gc.log
这些参数会将 GC 的详细信息、时间戳记录到指定的日志文件中
(2) 分析 GC 日志
使用 GCViewer 或 GCEasy 分析 GC 日志,重点关注以下内容
- Full GC 的频率:频繁的 Full GC 表明可能存在内存泄漏或内存不足
- Full GC 的持续时间:长时间的 Full GC 表明可能需要优化垃圾回收器或调整堆内存大小
- 堆内存使用情况:观察堆内存的使用趋势,是否存在持续增长的情况
(3) 检查堆内存配置
- 堆内存大小
-
- 检查 -Xms 和 -Xmx 参数,确保堆内存大小合理,如果堆内存过小,可能导致频繁的 Full GC
- 如果堆内存过大,可能会导致 GC 时间过长
- 新生代和老年代比例:
-
- 检查 -Xmn、 -XX:NewRatio 和 -XX:SurvivorRatio 参数,确保新生代和老年代的大小合理
- 如果新生代过小,可能导致对象频繁晋升到老年代,增加 Full GC 的频率
(4) 检查内存泄漏
- 检查代码中的内存泄漏点(重点)
-
- 资源未关闭的情况(如文件流、线程、数据库连接等)
- 是否有静态集合(如List、Map)不断增长,导致内存泄漏
- 第三方依赖漏洞(如Json无限转化,使用@JsonIgnore解决)
- 使用内存分析工具
-
- VisualVM 或 MAT(Memory Analyzer Tool)拍摄堆转储(Heap Dump),分析内存使用情况
(5) 监控应用性能
- 实时监控工具:
-
- JConsole 或 VisualVM 实时监控
- Prometheus 和 Grafna 搭建监控系统,并设置告警规则
9.Spring如何解决循环依赖
(1) 什么是循环依赖?
循环依赖其实就是循环引用,也就是两个或者两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。如下图:
注意,这里不是函数的循环调用,是对象的相互依赖关系。循环调用其实就是一个死循环,除非有终结条件。
Spring中循环依赖场景有:
(1)构造器的循环依赖
(2)field属性的循环依赖
其中,构造器的循环依赖问题无法解决,只能拋出BeanCurrentlyInCreationException异常,在解决属性循环依赖时,spring采用的是提前暴露对象的方法。
(2) Spring怎么解决循环依赖
Spring的循环依赖的理论依据基于Java的引用传递,当获得对象的引用时,对象的属性是可以延后设置的。(但是构造器必须是在获取引用之前)
Spring的单例对象的初始化主要分为三步:
(1)createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象
(2)populateBean:填充属性,这一步主要是多bean的依赖属性进行填充
(3)initializeBean:调用spring xml中的init 方法。
从上面单例bean的初始化可以知道:循环依赖主要发生在第一、二步,也就是构造器循环依赖和field循环依赖。那么我们要解决循环引用也应该从初始化过程着手,对于单例来说,在Spring容器整个生命周期内,有且只有一个对象,所以很容易想到这个对象应该存在Cache中,Spring为了解决单例的循环依赖问题,使用了三级缓存。
这三级缓存分别指:
- singletonFactories : 单例对象工厂的cache
- earlySingletonObjects :提前暴光的单例对象的Cache
- singletonObjects:单例对象的cache
在创建bean的时候,首先想到的是从cache中获取这个单例的bean,这个缓存就是singletonObjects。如果获取不到,并且对象正在创建中,就再从二级缓存earlySingletonObjects中获取。如果还是获取不到且允许singletonFactories通过getObject()获取,就从三级缓存singletonFactory.getObject()(三级缓存)获取,如果获取到了则:从singletonFactories中移除,并放入earlySingletonObjects中。其实也就是从三级缓存移动到了二级缓存。
从上面三级缓存的分析,我们可以知道,Spring解决循环依赖的诀窍就在于singletonFactories这个三级cache。这个cache的类型是ObjectFactory。这里就是解决循环依赖的关键,发生在createBeanInstance之后,也就是说单例对象此时已经被创建出来(调用了构造器)。这个对象已经被生产出来了,虽然还不完美(还没有进行初始化的第二步和第三步),但是已经能被人认出来了(根据对象引用能定位到堆中的对象),所以Spring此时将这个对象提前曝光出来让大家认识,让大家使用。
这样做有什么好处呢?让我们来分析一下“A的某个field或者setter依赖了B的实例对象,同时B的某个field或者setter依赖了A的实例对象”这种循环依赖的情况。A首先完成了初始化的第一步,并且将自己提前曝光到singletonFactories中,此时进行初始化的第二步,发现自己依赖对象B,此时就尝试去get(B),发现B还没有被create,所以走create流程,B在初始化第一步的时候发现自己依赖了对象A,于是尝试get(A),尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过ObjectFactory.getObject拿到A对象(虽然A还没有初始化完全,但是总比没有好呀),B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中。此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,进去了一级缓存singletonObjects中,而且更加幸运的是,由于B拿到了A的对象引用,所以B现在hold住的A对象完成了初始化。
知道了这个原理时候,肯定就知道为啥Spring不能解决“A的构造方法中依赖了B的实例对象,同时B的构造方法中依赖了A的实例对象”这类问题了!因为加入singletonFactories三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决。
总结
不要使用基于构造函数的依赖注入,可以通过以下方式解决:
- 在字段上使用@Autowired注解,让Spring决定在合适的时机注入
- 用基于setter方法的依赖注入
10.Redis数据类型有哪些?Hash类型如何实现的?
Redis的原有数据类型有 String、List、Set、ZSet、Hash,新版本新增了Bitmap、HyperLogLog、Geo、Stream类型
1.String
Redis中的String类型可以存储字符串、整数和浮点数,底层实现一般为SDS(简单动态字符串) ,基于此结构可以实现对字符串的整个部分和一部分进行操作,也可以实现浮点数或整数的自增和自减操作。此外Redis中的String类型是Redis中最基本的数据类型,其他类型都是基于字符串构建的
常见操作有SET key value、GET key、 INCR/DECR
String类型可一般用于缓存、计数器和分布式锁
2.List
Redis中的List类型可以理解为一种有序的字符串集集合,List的底层实现可以为:
1.双向链表(数据量大时使用)
2.压缩列表(数据量小时使用,节约内存空间。在新版本中被废弃,使用ListPack替代)
基于上述两种数据结构,可以实现List的按索引访问,或对队头和队尾弹出或删除等操作
List类型可以被用于消息队列,但是基于List的消息队列有两个问题:
1.生产者需要自行实现全局唯一ID
2.不能以消费者形式消费数据
3.Set
Redis中的Set类型可以理解为是一种无序的字符串集集合,且Set类型的key是唯一的,内部不可重复,Set类型的底层实现可以为:
1.哈希表(数据量大时使用,可以实现较快的删改查)
2.整数列表IntSet(数据量小时且存储为整数使用,节省内存)
Set类型可以用作数据去重(存储用户唯一ID)和关系聚合计算(共同求交集,推荐求并集等)。
4.Hash
Redis的Hash类型是一种键值对集合,用于存储结构化数据,Hash类型的底层实现可以为:
1.哈希表(字段较多时使用,支持快速的插入和查找)
2.压缩列表(字段较少时,值较小时使用,节约内存。在新版本被废弃,使用ListPack替代)
Hash类型一般用于存储结构化数据(对象),存储用户信息:如用户名、密码、邮箱等,很适合存储JavaBean
5.ZSet
Redis中的ZSet是一种存储有序集合的数据类型,特殊在于每个集合元素关联一个分数,按分数从小到大排序。ZSet类型的底层实现可以为:
1.跳表(用于存储有序集合,支持范围的快速查找和排序操作)
2.哈希表(将集合与分数映射,便于快速查找)
ZSet类型一般用于排行榜,或者延迟队列(用分数来存储时间戳,按照分数取任务)
6.Bitmap
Redis后续更新的位图Bitmap类型本质上是字符串类型的扩展,但可以对字符串进行位操作。Bitmap类型底层实现同为SDS,但以位(bit)为单位进行操作,即为:Bitmap将一个字符串视为一串连续的二进制位(bit) ,可以对这些二进制位进行操作,从而实现高效的状态存储和查询。每一位(bit)都可以用0或1表示不同的状态
应用场景有状态管理、记录用户在线、离线状态、用户签到
7.HyperLogLog
Redis中的HyperLogLog是一种近似基数统计的数据结构,适合大规模数据的去重统计。
底层主要通过概率算法实现(存在误差,在0.81%以内),且使用固定内存(12kb)存储消息
应用场景主要为网站UV计数或大数据去重计数
11.Spring事务传播机制详解
(1)事务的基本原理
Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行:
- 获取连接 Connection con = DriverManager.getConnection()
- 开启事务con.setAutoCommit(true/false);
- 执行CRUD
- 提交事务/回滚事务 con.commit() / con.rollback();
- 关闭连接 conn.close();
使用Spring的事务管理功能后,我们可以不再写步骤 2 和 4 的代码,而是由Spirng 自动完成。 那么Spring是如何在我们书写的 CRUD 之前和之后开启事务和关闭事务的呢?解决这个问题,也就可以从整体上理解Spring的事务管理实现原理了。下面简单地介绍下,注解方式为例子
- 配置文件开启注解驱动,在相关的类和方法上通过注解@Transactional标识。
- spring 在启动的时候会去解析生成相关的bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。
- 真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。
(2)Spring事务的传播属性
所谓spring事务的传播属性,就是定义在存在多个事务同时存在的时候,spring应该如何处理这些事务的行为。这些属性在TransactionDefinition中定义,具体常量的解释见下表:
| 常量名称 | 常量解释 |
|---|---|
| PROPAGATION_REQUIRED | 支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择,也是 Spring 默认的事务的传播。 |
| PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。新建的事务将和被挂起的事务没有任何关系,是两个独立的事务,外层事务失败回滚之后,不能回滚内层事务执行的结果,内层事务失败抛出异常,外层事务捕获,也可以不处理回滚操作 |
| PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
| PROPAGATION_MANDATORY | 支持当前事务,如果当前没有事务,就抛出异常。 |
| PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
| PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
| PROPAGATION_NESTED | 如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事务,则按REQUIRED属性执行。它使用了一个单独的事务,这个事务拥有多个可以回滚的保存点。内部事务的回滚不会对外部事务造成影响。它只对DataSourceTransactionManager事务管理器起效。 |
(3)数据库事务隔离级别
| 隔离级别 | 隔离级别的值 | 导致的问题 |
|---|---|---|
| Read-Uncommitted | 0 | 导致脏读 |
| Read-Committed | 1 | 避免脏读,允许不可重复读和幻读 |
| Repeatable-Read | 2 | 避免脏读,不可重复读,允许幻读 |
| Serializable | 3 | 串行化读,事务只能一个一个执行,避免了脏读、不可重复读、幻读。执行效率慢,使用时慎重 |
脏读:一事务对数据进行了增删改,但未提交,另一事务可以读取到未提交的数据。如果第一个事务这时候回滚了,那么第二个事务就读到了脏数据。
不可重复读:一个事务中发生了两次读操作,第一次读操作和第二次操作之间,另外一个事务对数据进行了修改,这时候两次读取的数据是不一致的。
幻读:第一个事务对一定范围的数据进行批量修改,第二个事务在这个范围增加一条数据,这时候第一个事务就会丢失对新增数据的修改。
总结:
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。
大多数的数据库默认隔离级别为 Read Commited,比如 SqlServer、Oracle
少数数据库默认隔离级别为:Repeatable Read 比如: MySQL InnoDB
(4)Spring中的隔离级别
| 常量 | 解释 |
|---|---|
| ISOLATION_DEFAULT | 这是个 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔离级别。另外四个与 JDBC 的隔离级别相对应。 |
| ISOLATION_READ_UNCOMMITTED | 这是事务最低的隔离级别,它充许另外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读。 |
| ISOLATION_READ_COMMITTED | 保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。 |
| ISOLATION_REPEATABLE_READ | 这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。 |
| ISOLATION_SERIALIZABLE | 这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。 |
(5)事务的嵌套
通过上面的理论知识的铺垫,我们大致知道了数据库事务和spring事务的一些属性和特点,接下来我们通过分析一些嵌套事务的场景,来深入理解spring事务传播的机制。
假设外层事务 Service A 的 Method A() 调用 内层Service B 的 Method B()
PROPAGATION_REQUIRED(spring 默认)
如果ServiceB.methodB() 的事务级别定义为 PROPAGATION_REQUIRED,那么执行 ServiceA.methodA() 的时候spring已经起了事务,这时调用 ServiceB.methodB(),ServiceB.methodB() 看到自己已经运行在 ServiceA.methodA() 的事务内部,就不再起新的事务。
假如 ServiceB.methodB() 运行的时候发现自己没有在事务中,他就会为自己分配一个事务。
这样,在 ServiceA.methodA() 或者在 ServiceB.methodB() 内的任何地方出现异常,事务都会被回滚。
PROPAGATION_REQUIRES_NEW
比如我们设计 ServiceA.methodA() 的事务级别为 PROPAGATION_REQUIRED,ServiceB.methodB() 的事务级别为 PROPAGATION_REQUIRES_NEW。
那么当执行到 ServiceB.methodB() 的时候,ServiceA.methodA() 所在的事务就会挂起,ServiceB.methodB() 会起一个新的事务,等待 ServiceB.methodB() 的事务完成以后,它才继续执行。
他与 PROPAGATION_REQUIRED 的事务区别在于事务的回滚程度了。因为 ServiceB.methodB() 是新起一个事务,那么就是存在两个不同的事务。如果 ServiceB.methodB() 已经提交,那么 ServiceA.methodA() 失败回滚,ServiceB.methodB() 是不会回滚的。如果 ServiceB.methodB() 失败回滚,如果他抛出的异常被 ServiceA.methodA() 捕获,ServiceA.methodA() 事务仍然可能提交(主要看B抛出的异常是不是A会回滚的异常)。
PROPAGATION_SUPPORTS
假设ServiceB.methodB() 的事务级别为 PROPAGATION_SUPPORTS,那么当执行到ServiceB.methodB()时,如果发现ServiceA.methodA()已经开启了一个事务,则加入当前的事务,如果发现ServiceA.methodA()没有开启事务,则自己也不开启事务。这种时候,内部方法的事务性完全依赖于最外层的事务。
PROPAGATION_NESTED
现在的情况就变得比较复杂了, ServiceB.methodB() 的事务属性被配置为 PROPAGATION_NESTED, 此时两者之间又将如何协作呢? ServiceB.methodB() 如果 rollback, 那么内部事务(即 ServiceB.methodB()) 将回滚到它执行前的 SavePoint 而外部事务(即 ServiceA.methodA()) 可以有以下两种处理方式:
a、捕获异常,执行异常分支逻辑
void methodA() {
try {
ServiceB.methodB();
} catch (SomeException) {
// 执行其他业务, 如 ServiceC.methodC();
}
}
这种方式也是嵌套事务最有价值的地方, 它起到了分支执行的效果, 如果 ServiceB.methodB() 失败, 那么执行 ServiceC.methodC(), 而 ServiceB.methodB 已经回滚到它执行之前的 SavePoint, 所以不会产生脏数据(相当于此方法从未执行过), 这种特性可以用在某些特殊的业务中, 而 PROPAGATION_REQUIRED 和 PROPAGATION_REQUIRES_NEW 都没有办法做到这一点。
b、 外部事务回滚/提交 代码不做任何修改, 那么如果内部事务(ServiceB.methodB()) rollback, 那么首先 ServiceB.methodB() 回滚到它执行之前的 SavePoint(在任何情况下都会如此), 外部事务(即 ServiceA.methodA()) 将根据具体的配置决定自己是 commit 还是 rollback
另外三种事务传播属性基本用不到,在此不做分析。
(6)总结
对于项目中需要使用到事务的地方,我建议开发者还是使用spring的TransactionCallback接口来实现事务,不要盲目使用spring事务注解,如果一定要使用注解,那么一定要对spring事务的传播机制和隔离级别有个详细的了解,否则很可能发生意想不到的效果。
12.雪花算法产生的ID会重复吗?
雪花算法的ID结构
主要由以下几部分组成:
- 时间戳(41 位) :记录自 2022-01-01 00:00:00 UTC 以来的毫秒数。
- 机器 ID(10 位) :用于区分不同的机器实例。
- 序列号(12 位) :在同一毫秒内生成的 ID 的序列号
可能导致ID重复的情况
1.时间回拨
- 问题:回拨的幅度超过1毫秒,可能产生重复
- 解决方案:
-
- 时间补偿机制:检测到时间回拨,暂停ID生成,直到系统时间恢复正常
- 使用本地时间戳:使用本地时间戳(如System.nanoTime())代替系统时间
2.机器ID冲突
- 问题:多个机器使用了同个机器ID,可能会导致雪花算法ID重复
- 解决方案:确保机器ID唯一
3.序列号溢出
- 问题:在同一毫秒内,如果生成的ID数量超过了4096个,序列号部分会溢出,导致ID重复
- 解决方案:
-
- 限制生成速率:用限流器来限制生成速率,如 Guava 的 RateLimiter
- 增加序列号位数:如业务需要更高的生成速率,可以适当增加序列号的位数,但会减少时间戳或机器ID的位数,雪花ID总位数不变