面试真题**
1.ThreadLocal什么情况内存泄漏?
ThreadLocal 是 Java 中用于创建线程局部变量的类。每个线程都拥有自己独立的变量副本,互不干扰。虽然 ThreadLocal 可以方便地实现线程安全,但不正确的使用方式可能会导致内存泄漏。以下是 ThreadLocal 引起内存泄漏的几种常见情况:
1. ThreadLocal 没有手动删除
ThreadLocal 变量与线程生命周期一致,如果在线程运行过程中没有手动删除 ThreadLocal 变量,会导致内存泄漏。具体来说,当线程使用完 ThreadLocal 变量后,应该调用 remove()
方法来清理资源。
ThreadLocal<MyObject> threadLocal = new ThreadLocal<>();
try {
threadLocal.set(new MyObject());
// 使用 threadLocal 变量
} finally {
threadLocal.remove();
}
2. 使用线程池
线程池中的线程是被重复利用的,如果某个线程池线程使用了 ThreadLocal 变量但没有清理,那么这个 ThreadLocal 变量会一直存在于线程的生命周期内,直到线程被销毁。
// 假设线程池中有一个线程池
ExecutorService executor = Executors.newFixedThreadPool(10);
ThreadLocal<MyObject> threadLocal = new ThreadLocal<>();
executor.submit(() -> {
threadLocal.set(new MyObject());
try {
// 使用 threadLocal 变量
} finally {
threadLocal.remove();
}
});
3. 静态 ThreadLocal 变量
如果 ThreadLocal 变量被声明为静态变量,会导致其生命周期与整个应用程序一致,即使线程已经结束,ThreadLocal 变量仍然存在,导致内存泄漏。
public class MyClass {
private static final ThreadLocal<MyObject> threadLocal = new ThreadLocal<>();
public void doSomething() {
threadLocal.set(new MyObject());
try {
// 使用 threadLocal 变量
} finally {
threadLocal.remove();
}
}
}
4. ThreadLocal 的引用未被清理
ThreadLocal 变量的键是一个弱引用,而值是一个强引用。如果 ThreadLocal 变量的引用未被清理,即使线程结束,值也不会被垃圾回收,从而导致内存泄漏。
ThreadLocal<MyObject> threadLocal = new ThreadLocal<>();
void someMethod() {
threadLocal.set(new MyObject());
// 如果没有调用 remove() 方法,MyObject 将不会被垃圾回收
}
解决方案
-
尽量使用 ThreadLocal 的子类,如 InheritableThreadLocal
- InheritableThreadLocal 允许线程的子线程继承父线程的 ThreadLocal 变量,适用于需要跨线程传递上下文的场景。
-
在 finally 块中调用 remove() 方法
- 确保 ThreadLocal 变量使用后能够及时清理,避免内存泄漏。
ThreadLocal<MyObject> threadLocal = new ThreadLocal<>();
try {
threadLocal.set(new MyObject());
// 使用 threadLocal 变量
} finally {
threadLocal.remove();
}
- 注意线程池的使用
- 在线程池中使用 ThreadLocal 变量时,更要小心内存泄漏问题,确保线程结束后能够及时清理 ThreadLocal 变量。
总结
ThreadLocal 在正确使用时,可以方便地解决线程安全问题,但如果不注意清理,会导致内存泄漏。在使用 ThreadLocal 时,尤其是在长生命周期的线程(如线程池)中使用时,务必在使用完后调用 remove()
方法清理,确保不会导致内存泄漏。
2.Spring AOP实现原理?
Spring AOP(Aspect-Oriented Programming,面向切面编程) 是 Spring 框架中的一个重要模块,它允许程序员将横切关注点(如日志记录、事务管理、安全检查等)分离出来,减少代码重复,提高模块化程度。Spring AOP 主要依赖于代理模式来实现动态横切关注点的插入。下面是 Spring AOP 的实现原理的详细解析:
1. AOP 基本概念
- 切面(Aspect):横切关注点的模块化,比如日志记录、事务管理。切面可以看作是实现这些横切关注点的代码。
- 连接点(Join Point):程序执行过程中的特定点,比如方法调用、异常抛出等。Spring AOP 仅支持方法级别的连接点。
- 通知(Advice):在切面的具体实现,定义了在连接点上执行的具体动作。通知分为前置通知、后置通知、返回通知、异常通知和环绕通知。
- 切点(Pointcut):匹配连接点的表达式,定义了哪些连接点会被切面拦截。
- 引入(Introduction):在不修改代码的情况下,动态地为类添加一些方法或字段。
- 织入(Weaving):将切面应用到目标对象,创建一个代理对象的过程。织入可以在编译时、类加载时和运行时进行。Spring AOP 采用的是运行时织入。
2. Spring AOP 实现方式
Spring AOP 主要通过两种方式实现:JDK 动态代理和 CGLIB 动态代理。
- JDK 动态代理:基于 Java 的接口机制。适用于代理实现了接口的类。
- CGLIB 动态代理:基于继承机制。适用于代理没有实现接口的类。CGLIB 通过生成目标类的子类来创建代理对象。
3. Spring AOP 的运行原理
- 定义切面:使用 @Aspect 注解定义切面,并在切面中定义切点和通知。
@Aspect
public class LoggingAspect {
@Pointcut("execution(* com.example.service.*.*(..))")
private void forServicePackage() {}
@Before("forServicePackage()")
public void beforeAdvice() {
System.out.println("Executing @Before advice on method");
}
@After("forServicePackage()")
public void afterAdvice() {
System.out.println("Executing @After advice on method");
}
}
- 配置切面:将切面类声明为 Spring 管理的 bean。
<bean id="loggingAspect" class="com.example.aspect.LoggingAspect" />
<aop:aspectj-autoproxy />
- 生成代理对象:Spring AOP 自动为匹配切点表达式的目标对象生成代理对象。
- 如果目标对象实现了接口,Spring AOP 使用 JDK 动态代理。
- 如果目标对象没有实现接口,Spring AOP 使用 CGLIB 动态代理。
- 代理对象调用:当代理对象的方法被调用时,AOP 框架会根据切点表达式判断是否需要执行切面中的通知。
- 如果方法匹配切点表达式,执行对应的通知逻辑(如前置通知、后置通知)。
- 如果方法不匹配切点表达式,直接执行目标方法。
4. Spring AOP 代理的选择
Spring AOP 默认优先使用 JDK 动态代理。如果目标对象没有实现任何接口,Spring AOP 则使用 CGLIB 动态代理。可以通过以下配置强制使用 CGLIB 代理:
<aop:aspectj-autoproxy proxy-target-class="true" />
5. 执行流程
- Spring 容器启动时,扫描 @Aspect 注解,解析切点和通知。
- 将切点和通知信息缓存,生成代理类。
- 代理类在方法调用前后,根据通知类型执行相应的逻辑(如日志记录、事务管理)。
- 如果使用环绕通知,代理类可以控制目标方法的执行顺序。
总结
Spring AOP 的核心在于通过代理模式(JDK 动态代理和 CGLIB 动态代理)实现对目标对象的增强。它利用 @Aspect 注解和 AOP 配置文件来定义切点和通知,通过切面将横切关注点模块化,从而实现松耦合和增强代码的可维护性。了解 Spring AOP 的原理,有助于更好地设计和调试基于 AOP 的应用程序。
3.单例模式的好处?
单例模式(Singleton Pattern)是一种创建型设计模式,确保一个类只有一个实例,并提供一个全局访问点。单例模式在许多应用程序中被广泛使用,尤其是在需要控制资源访问、管理全局状态或确保单个实例的场景中。以下是单例模式的主要好处:
1. 控制实例数量
单例模式确保一个类只有一个实例存在。这样可以避免因创建多个实例而导致的资源浪费或不一致问题。
2. 全局访问点
单例模式提供一个全局访问点,使得程序中的不同部分能够轻松访问该实例。这对于管理全局状态或配置非常有用。
3. 延迟加载
单例模式可以实现延迟加载,即在第一次访问时才创建实例,从而避免了程序启动时的性能开销。
4. 资源共享
单例模式允许多个对象共享同一个实例,适用于需要共享资源(如数据库连接池、线程池等)的场景。这可以有效地减少资源的重复创建和管理成本。
5. 简化代码
使用单例模式可以简化代码设计,因为开发者不需要显式地管理实例的创建和销毁。单例模式封装了这些细节,使得代码更加简洁和易于维护。
6. 线程安全
通过适当的实现(如双重检查锁定、静态内部类等),单例模式可以确保在多线程环境中的线程安全,避免了多个线程同时创建实例的问题。
7. 配置和状态管理
单例模式非常适合用于配置和状态管理类。由于只有一个实例存在,可以确保配置和状态的一致性和同步性,避免了不同实例之间的冲突和不一致。
单例模式的实现示例
以下是几种常见的单例模式实现方式:
- 懒汉式(线程不安全)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- 懒汉式(线程安全,双重检查锁定)
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
- 饿汉式(线程安全)
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
- 静态内部类(线程安全,推荐)
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
总结
单例模式通过确保类的唯一实例和提供全局访问点,带来了控制实例数量、资源共享、简化代码等诸多好处。它在许多应用场景中都能发挥重要作用,如全局配置管理、资源访问控制等。通过合理地选择和实现单例模式,可以有效地提升程序的性能和可靠性。
4.Redis内存结构知道哪些?
Redis 是一个开源的内存数据结构存储系统,支持多种数据结构,如字符串、哈希表、列表、集合和有序集合等。了解 Redis 的内存结构对优化性能和使用 Redis 提供的丰富功能非常有帮助。下面是 Redis 内存结构的详细解析:
1. 简单动态字符串(SDS)
Redis 中的字符串类型(String)使用 SDS(Simple Dynamic String)来表示。SDS 具备以下特性:
- 预分配空间:当 SDS 需要扩展时,会预分配额外的空间以减少内存重分配的次数。
- 惰性空间释放:当 SDS 缩小时,不会立即释放内存,而是通过
free
属性记录释放的空间,供以后使用。 - 二进制安全:SDS 可以存储任意二进制数据,包括
\0
字符。
2. 对象系统
Redis 通过对象系统来实现数据结构,主要包括以下几种对象:
- 字符串对象(String Object):用于表示字符串数据。
- 列表对象(List Object):用于表示列表数据,底层实现为双向链表或压缩列表。
- 哈希对象(Hash Object):用于表示哈希数据,底层实现为哈希表或压缩列表。
- 集合对象(Set Object):用于表示集合数据,底层实现为哈希表或整数集合。
- 有序集合对象(Sorted Set Object):用于表示有序集合数据,底层实现为跳跃表和哈希表。
3. 内存分配器
Redis 支持多种内存分配器,包括:
- jemalloc:默认的内存分配器,性能优越,适用于大多数场景。
- tcmalloc:谷歌开发的高性能内存分配器,适用于高并发场景。
- libc malloc:标准 C 库的内存分配器,适用于简单的场景。
4. 数据结构
Redis 支持多种数据结构,分别对应不同的底层实现:
- 字符串(String):最简单的数据类型,底层实现为 SDS。
- 列表(List):有序的字符串集合,底层实现为双向链表或压缩列表。
- 哈希(Hash):字段-值对的集合,底层实现为哈希表或压缩列表。
- 集合(Set):无序的字符串集合,底层实现为哈希表或整数集合。
- 有序集合(Sorted Set):带分数的有序字符串集合,底层实现为跳跃表和哈希表。
5. 压缩列表(Ziplist)
压缩列表是一种紧凑的数据结构,用于存储列表和哈希对象的元素。它通过紧凑的内存布局减少内存消耗,适用于元素数量较少且长度较短的场景。
6. 跳跃表(Skiplist)
跳跃表是一种随机化的数据结构,用于实现有序集合。它在有序性和查找性能上具有较好表现,适用于需要频繁查询和插入的有序数据。
7. 整数集合(Intset)
整数集合是一种紧凑的数据结构,用于存储整数集合对象的元素。它通过紧凑的内存布局减少内存消耗,适用于元素为整数且数量较少的场景。
8. 哈希表(Hashtable)
哈希表是 Redis 中的基础数据结构之一,用于实现哈希对象和集合对象。它通过键值对存储数据,具有快速的查找和插入性能。
内存优化
- 合理选择数据结构:根据实际应用选择合适的数据结构,如使用压缩列表和整数集合以减少内存消耗。
- 使用内存优化选项:如使用
maxmemory-policy
配置参数来设置内存淘汰策略。 - 定期清理过期数据:通过设置键的过期时间或使用
Redis
提供的定期清理机制来释放内存。 - 监控内存使用:使用 Redis 提供的内存统计命令(如
INFO memory
)来监控内存使用情况,并及时进行调整。
总结
Redis 的内存结构设计非常灵活,支持多种数据结构和内存分配器,以满足不同场景下的性能需求和内存优化要求。了解和合理利用这些内存结构,可以显著提升 Redis 的性能和资源利用效率。
5.说说Redis过期键的删除策略?
Redis 通过设置键的过期时间来管理数据的生命周期,并提供了多种删除过期键的策略。主要的删除策略包括定时删除、惰性删除和定期删除。每种策略都有其优缺点,Redis 通过结合这些策略来实现对过期键的高效管理。
1. 定时删除(主动删除)
定时删除是在设置键的过期时间时,创建一个定时任务,当过期时间到达时,立即删除该键。
- 优点:
- 能够及时释放内存。
- 缺点:
- 需要为每个过期键创建定时任务,增加系统开销。
2. 惰性删除(被动删除)
惰性删除是在访问键时,检查键是否过期,如果过期则删除该键。
- 优点:
- 不需要额外的系统资源,只有在访问键时才进行检查。
- 缺点:
- 可能会存在大量过期键占用内存的问题,特别是如果这些键很少被访问。
3. 定期删除(周期删除)
定期删除是 Redis 默认采用的主要策略。Redis 会周期性地扫描一部分键,检查并删除过期键。具体实现上,Redis 每隔一定时间就会随机检查一部分设置了过期时间的键,并删除其中的过期键。
- 优点:
- 平衡了及时删除和资源开销的问题,通过定期检查来减少过期键占用的内存。
- 缺点:
- 不能保证过期键能够立即被删除,可能会存在一定延迟。
定期删除的具体实现
Redis 定期删除策略的具体实现方式如下:
- 时间间隔:Redis 每 100 毫秒会进行一次过期键扫描。
- 随机抽样:每次扫描时随机抽取一部分设置了过期时间的键。
- 删除过期键:检查这些键是否过期,删除其中的过期键。
- 限制检查时间:如果删除的过期键数量比较多,会继续进行扫描,直到一定数量的过期键被删除或达到时间限制。
内存淘汰策略
当 Redis 内存使用达到上限时,会触发内存淘汰策略(Eviction Policy),以便释放内存空间。Redis 提供了多种内存淘汰策略,包括:
- noeviction:返回错误,不删除任何键。
- allkeys-lru:使用 LRU(Least Recently Used)算法,从所有键中选择最近最少使用的键进行删除。
- volatile-lru:使用 LRU 算法,从设置了过期时间的键中选择最近最少使用的键进行删除。
- allkeys-random:从所有键中随机选择键进行删除。
- volatile-random:从设置了过期时间的键中随机选择键进行删除。
- volatile-ttl:从设置了过期时间的键中选择即将过期的键进行删除。
总结
Redis 通过结合定时删除、惰性删除和定期删除三种策略,来高效地管理过期键的删除。定时删除保证了过期键的及时删除,惰性删除降低了系统开销,定期删除在及时性和资源开销之间取得了平衡。通过配置合适的内存淘汰策略,Redis 可以在内存使用达到上限时有效地释放内存,确保系统的稳定运行。
6.缓存穿透、缓存击穿、缓存雪崩是什么,解决方法是什么?
在高并发系统中,缓存可以显著提高数据访问速度,但也可能遇到一些问题,如缓存穿透、缓存击穿和缓存雪崩。这些问题的理解和解决方法如下:
1. 缓存穿透
缓存穿透指的是查询一个在缓存和数据库中都不存在的数据,导致每次请求都落到数据库上。如果有大量这样的请求,可能会让数据库压力骤增。
解决方法:
-
缓存空值:将查询结果为空的数据也缓存起来,并设置一个较短的过期时间,以防止对同一个不存在的数据进行重复查询。
Object value = cache.get(key); if (value == null) { value = database.get(key); if (value == null) { cache.put(key, null, shortExpireTime); // 设置空值缓存 } else { cache.put(key, value, expireTime); } } return value;
-
布隆过滤器:在缓存层前增加布隆过滤器,将所有可能存在的数据哈希到一个位数组中,查询前先通过布隆过滤器判断数据是否可能存在。如果布隆过滤器判断数据不存在,则直接返回,避免查询数据库。
if (!bloomFilter.mightContain(key)) { return null; // 直接返回,不查询数据库 } // 查询缓存和数据库的逻辑
2. 缓存击穿
缓存击穿指的是缓存中某个热点数据在过期的瞬间,大量并发请求涌入数据库,导致数据库压力骤增。这通常发生在某个热点数据突然失效时。
解决方法:
-
互斥锁(Mutex):当缓存失效时,通过加锁的方式控制只有一个线程去查询数据库并更新缓存,其他线程等待该线程完成后再获取缓存。
synchronized (lock) { Object value = cache.get(key); if (value == null) { value = database.get(key); cache.put(key, value, expireTime); } }
-
逻辑过期:设置一个较短的实际过期时间和一个较长的逻辑过期时间。在数据接近过期时,后台线程异步刷新缓存,而前台线程继续使用逻辑上未过期的数据。
if (isLogicalExpired(value)) { refreshCacheInBackground(key); } return value;
3. 缓存雪崩
缓存雪崩指的是缓存中的大量数据在同一时间失效,导致大量请求同时到达数据库,给数据库造成巨大的压力,甚至可能导致数据库崩溃。
解决方法:
-
缓存数据过期时间分散:为不同的缓存数据设置不同的过期时间,避免大量缓存数据在同一时间失效。可以在基准过期时间的基础上添加一个随机值。
int baseExpireTime = 60 * 60; // 1小时 int randomExpireTime = new Random().nextInt(30 * 60); // 0到30分钟的随机时间 int expireTime = baseExpireTime + randomExpireTime;
-
缓存预热:在系统启动时,将可能会被大量访问的数据预先加载到缓存中,确保在高并发访问时缓存中已有数据,避免缓存雪崩。例如,通过定时任务加载一些热点数据到缓存中。
-
双重缓存策略:使用主缓存和备用缓存两个层级,当主缓存失效时,首先尝试从备用缓存中获取数据。如果备用缓存中也没有数据,再访问数据库,并将数据同时更新到主缓存和备用缓存中。
Object value = primaryCache.get(key); if (value == null) { value = secondaryCache.get(key); if (value == null) { value = database.get(key); primaryCache.put(key, value, expireTime); secondaryCache.put(key, value, longerExpireTime); } else { primaryCache.put(key, value, expireTime); } } return value;
-
缓存降级:当发现缓存服务出现问题或缓存雪崩时,可以采取降级策略,即在一定时间内直接返回默认值或空值,避免大量请求直接打到数据库上,从而保护数据库不被压垮。
try { Object value = cache.get(key); if (value == null) { value = database.get(key); cache.put(key, value, expireTime); } return value; } catch (Exception e) { // 缓存降级,返回默认值或空值 return getDefaultValue(); }
通过合理使用以上策略,可以有效地避免缓存穿透、缓存击穿和缓存雪崩问题,确保系统在高并发环境下的稳定性和性能。
7.MySQL为什么要选用B+树?
MySQL 使用 B+ 树作为其索引结构的主要原因是 B+ 树具有良好的磁盘适应性、高效的范围查询性能以及结构上的优势。具体来说,B+ 树相比其他数据结构(如 B 树、二叉搜索树、哈希表等)有以下几个优点:
1. 高度平衡的树结构
B+ 树是一种自平衡的树数据结构,它能够确保所有叶子节点在同一层级上,从而保证了树的高度尽可能低。这意味着在最坏情况下,查找、插入和删除操作的时间复杂度都是 O(log n),其中 n 是树中元素的数量。这使得 B+ 树在数据量较大时依然能够保持高效的性能。
2. 叶子节点链表
B+ 树的叶子节点通过链表连接,使得范围查询(Range Query)非常高效。只需要找到范围的起点,然后通过链表顺序访问即可完成范围查询操作,而不需要再次进行树的遍历。这对于数据库的范围查询(如 WHERE 子句中的 BETWEEN 操作)非常重要。
3. 磁盘 I/O 效率高
B+ 树的节点通常设计为一个数据块的大小(例如 4KB),这使得每个节点可以存储多个键值对。由于每次磁盘 I/O 操作都会加载一个数据块,这样的设计使得每次 I/O 操作能够读取多个键值对,从而提高了磁盘 I/O 的效率。相比之下,二叉搜索树的节点通常只能存储一个键值对,在大数据量时会导致更多的磁盘 I/O 操作。
4. 顺序访问
B+ 树的所有实际数据都存储在叶子节点,并且叶子节点之间有顺序连接的指针。这使得 B+ 树在处理顺序访问和范围查询时非常高效。而 B 树等其他树结构在处理这些操作时可能需要更多的时间和复杂度。
5. 空间利用率高
B+ 树的内节点只存储键值,而叶子节点存储完整的键值对。这种设计使得内节点可以存储更多的键,从而降低了树的高度,减少了查找过程中需要访问的节点数量。此外,B+ 树的节点利用率一般较高,减少了浪费的存储空间。
6. 有序性
B+ 树中的数据是有序的,这使得它不仅适用于精确匹配查询,也适用于范围查询。哈希表虽然查找效率高,但不支持范围查询,因此不适合作为数据库的索引结构。
总结
MySQL 选择 B+ 树作为其索引结构主要是因为 B+ 树在处理大数据量时具有高效的查询性能,特别是在范围查询和顺序访问场景下。此外,B+ 树的磁盘 I/O 效率高,空间利用率好,能够充分利用磁盘存储的特点,适合用作数据库的索引结构。总的来说,B+ 树在性能和效率上具有显著优势,是 MySQL 索引的理想选择。
8.如何优化MySQL查询效率?
优化 MySQL 查询效率是数据库性能调优的核心任务之一。以下是一些常见的优化方法和最佳实践:
1. 使用合适的索引
- 创建索引:根据查询频率和查询条件,合理创建索引。常用的索引类型包括主键索引、唯一索引和普通索引。
- 联合索引:对于多列条件查询,创建联合索引(复合索引)可以减少查询时间。
- 覆盖索引:确保查询的字段都在索引中,这样可以避免回表操作,直接从索引中获取数据。
- 索引选择性:选择性高的列更适合做索引。
2. 查询优化
- 选择合适的 SQL 语句:避免使用
SELECT *
,只选择需要的列。使用明确的列名可以减少数据传输量。 - 合理使用 JOIN:尽量避免多表 JOIN,特别是大表的 JOIN,尽量在程序中进行数据的分解和组合。
- 使用子查询和临时表:有些复杂的查询可以通过子查询或者临时表来简化主查询。
- 合理使用 WHERE 条件:确保 WHERE 子句中的条件列都有索引。避免对索引列进行函数操作或类型转换,这会导致索引失效。
- LIMIT:对于需要分页的查询,使用 LIMIT 子句来控制返回的数据量。
3. 数据库设计
- 范式和反范式:在数据库设计中,平衡范式化和反范式化。范式化可以减少数据冗余,但有时反范式化可以提高查询性能。
- 分区表:对于大表,使用分区表(如水平分区)来提高查询效率。
- 表和列的设计:合理设计表和列,尽量使用简单的数据类型,避免不必要的复杂数据类型。
4. 优化配置
- 缓冲池和缓存:合理配置 MySQL 的缓冲池(如 InnoDB 的
innodb_buffer_pool_size
)和查询缓存(如query_cache_size
),以充分利用内存,提高查询性能。 - 连接池:使用连接池来管理数据库连接,减少频繁建立和关闭连接的开销。
5. 分析查询
-
EXPLAIN 语句:使用 EXPLAIN 语句分析查询计划,查看索引使用情况,识别可能的性能瓶颈。
EXPLAIN SELECT * FROM your_table WHERE your_column = 'your_value';
6. 减少锁争用
- 合适的事务隔离级别:选择合适的事务隔离级别,尽量使用较低的隔离级别如 READ COMMITTED 或 REPEATABLE READ。
- 短事务:尽量将事务范围缩小,避免长时间持有锁。
- 锁粒度:尽量选择合适的锁粒度,减少锁争用。
7. 使用缓存
- 应用层缓存:在应用层使用缓存(如 Redis、Memcached)来缓存热点数据,减少对数据库的直接访问。
- MySQL 查询缓存:启用 MySQL 查询缓存,缓存重复查询的结果。
8. 优化批量操作
-
批量插入和更新:对于大量的数据插入和更新,使用批量操作,减少多次单条操作的开销。
INSERT INTO your_table (col1, col2) VALUES (val1, val2), (val3, val4), ...;
9. 数据库分片
- 水平分片:将数据按某种规则分片存储到多个数据库实例中,减小单个数据库的负担。
- 垂直分片:将不同的表或表的不同字段分片到不同的数据库实例中。
10. 持续监控和调优
-
监控工具:使用监控工具(如 MySQL Enterprise Monitor、Percona Monitoring and Management)监控数据库性能。
-
慢查询日志:启用慢查询日志,分析慢查询,进行针对性优化。
SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1; -- 设置慢查询阈值为1秒
通过结合以上方法,可以有效地优化 MySQL 查询效率,提升数据库性能。根据具体的应用场景和性能瓶颈,有针对性地选择和组合这些方法,才能达到最佳的优化效果。
11.说说联合索引和覆盖索引?
联合索引和覆盖索引都是 MySQL 数据库中用于提高查询效率的重要概念。
联合索引(Composite Index)
联合索引是指针对多个列同时创建的索引,可以包含多个列的值,从而可以支持多列条件的查询。例如,对于一个包含 username
和 email
两列的用户表,可以创建一个联合索引来加速对这两列的查询操作。
优点:
- 支持多列条件查询:可以通过联合索引同时提高多列条件查询的效率,避免多次单列索引的查找操作。
- 减少索引数量:相比单列索引,联合索引可以减少索引的数量,节省存储空间。
注意事项:
- 列顺序:联合索引的列顺序很重要,查询时应该按照索引的列顺序进行查询,这样才能充分利用索引的优势。
- 索引宽度:联合索引的列越多,索引宽度就越大,可能会导致索引文件较大。
覆盖索引(Covering Index)
覆盖索引是指一个查询可以直接从索引中获取所需的数据,而不需要再去主键或者数据页中获取。当查询的列都包含在索引中时,就可以利用覆盖索引。
例如,对于一个包含 id
、username
和 email
三列的用户表,如果创建了一个 (id, username)
的联合索引,并且查询只需要 id
和 username
列的值,那么这个查询就可以直接从索引中获取数据,无需再去读取数据页。
优点:
- 减少 I/O 操作:由于查询可以直接从索引中获取数据,避免了从数据页读取数据的操作,因此可以减少 I/O 操作,提高查询效率。
- 减少内存使用:由于不需要将查询结果加载到内存中,可以减少内存的使用。
注意事项:
- 覆盖列:要使用覆盖索引,查询中需要的列都必须包含在索引中,否则无法完全覆盖查询需求。
- 索引宽度:覆盖索引的宽度应该尽量小,不要包含不必要的列,以减少索引的存储空间。
总结
联合索引适用于多列条件查询,可以提高多列条件查询的效率;而覆盖索引适用于查询中需要的列都包含在索引中,可以减少 I/O 操作,提高查询效率。在实际使用中,根据查询的具体情况和性能需求,合理选择并结合使用联合索引和覆盖索引,可以更有效地优化数据库的查询性能。
12.如何SQL优化?
SQL 优化是提高数据库查询性能的重要手段,以下是一些常见的 SQL 优化技巧:
-
合适的索引设计:
- 根据查询频率和查询条件,创建合适的索引,包括主键索引、唯一索引、普通索引和全文索引。
- 考虑使用复合索引(联合索引)来支持多列条件查询,确保索引选择性高。
-
优化查询语句:
- 尽量减少使用
SELECT *
,只选择需要的列。 - 使用
EXPLAIN
分析查询计划,查看索引使用情况,识别可能的性能瓶颈。 - 合理使用 JOIN 操作,避免多表 JOIN 或者使用不必要的 JOIN。
- 尽量减少使用
-
优化 WHERE 条件:
- 确保 WHERE 子句中的条件列都有索引,避免对索引列进行函数操作或类型转换。
- 使用合适的数据类型,尽量避免在 WHERE 子句中进行类型转换。
-
使用覆盖索引:
- 确保查询中需要的列都包含在索引中,这样可以避免回表操作,直接从索引中获取数据。
-
合理使用 GROUP BY 和 ORDER BY:
- 尽量减少 GROUP BY 和 ORDER BY 的使用,因为这些操作会增加查询的计算量。
- 如果必须使用 GROUP BY 和 ORDER BY,确保相应的列有合适的索引。
-
优化子查询:
- 尽量避免使用过多的子查询,可以通过 JOIN 操作或者临时表来替代部分子查询。
-
避免使用通配符查询:
- 尽量避免使用通配符(如
%
)开头的查询,因为这样会导致索引失效,影响查询性能。
- 尽量避免使用通配符(如
-
分页查询优化:
- 对于分页查询,使用 LIMIT 子句来限制返回的数据量,避免一次性查询大量数据。
-
定期分析和优化 SQL:
- 定期分析慢查询日志,优化慢查询,进行针对性的调优。
- 根据数据库的实际情况和性能监控结果,定期优化和调整 SQL 查询语句。
-
使用存储过程和触发器:
- 合理使用存储过程和触发器,可以减少网络通信开销,提高查询效率。
以上是一些常见的 SQL 优化技巧,通过结合使用这些方法,并根据实际情况进行调整和优化,可以有效提高数据库查询性能。
什么情况会导致索引失效
索引失效通常是由于以下几种情况导致的:
-
使用函数操作:在 WHERE 子句中对索引列进行函数操作会导致索引失效,因为数据库无法利用索引来加速函数计算。例如:
SELECT * FROM users WHERE YEAR(create_time) = 2022;
上述查询中对
create_time
列使用了 YEAR 函数,导致索引失效。 -
使用运算符:在 WHERE 子句中使用了不支持索引的运算符,也会导致索引失效。例如:
SELECT * FROM users WHERE create_time + 10 = '2022-06-01';
上述查询中使用了运算符 +,导致索引失效。
-
使用通配符开头的查询:在 LIKE 查询中使用通配符 % 开头会导致索引失效,因为索引无法加速通配符开头的模糊查询。例如:
SELECT * FROM users WHERE username LIKE '%john';
上述查询中 % 开头的模糊查询会导致索引失效。
-
类型转换:在 WHERE 子句中对索引列进行类型转换也会导致索引失效。例如:
SELECT * FROM users WHERE CAST(id AS VARCHAR) = '1001';
上述查询中对 id 列进行了类型转换,导致索引失效。
-
不满足索引的最左前缀原则:对于复合索引,如果查询条件不满足索引的最左前缀原则,也会导致索引失效。例如:
CREATE INDEX idx_name_age ON users (name, age); SELECT * FROM users WHERE age = 30;
上述查询中,虽然有复合索引 (name, age),但查询条件只涉及 age 列,不涉及 name 列,导致索引失效。
-
查询条件过于宽泛:如果查询条件过于宽泛,返回的数据量过大,数据库可能会放弃使用索引而进行全表扫描,导致索引失效。例如:
SELECT * FROM users WHERE status = 'active';
如果大部分数据的 status 都是 'active',数据库可能会选择全表扫描而不是使用索引。
为了避免索引失效,应该尽量避免上述情况,并且根据具体的查询需求合理设计索引,选择合适的数据类型和运算符,避免在索引列上进行函数操作或类型转换,优化查询条件,避免过于宽泛的查询。定期分析慢查询日志,识别索引失效的情况,并进行相应的调整和优化也是很重要的。
13.JVM 永久代存的是什么内容?
在 Java 8 及之前的版本中,JVM 中的永久代(PermGen)主要用于存储以下内容:
-
类的元数据信息:包括类的结构信息、方法信息、字段信息等,这些信息在类加载时被加载到永久代中。
-
常量池:包括字符串常量、静态常量等,这些常量在编译时被加载到永久代中。
-
静态变量:静态变量属于类的属性,在类加载时被加载到永久代中。
-
运行时常量池:是 JVM 在运行时动态生成的常量池,用于存储运行时产生的新常量。
-
方法区:方法区也属于永久代的一部分,用于存储类的方法字节码、常量、静态变量等信息。
需要注意的是,永久代的大小是有限制的,默认情况下在 32 位 JVM 中通常为 64MB 到 128MB,而在 64 位 JVM 中则可以更大一些。由于永久代的大小有限,如果其中的内容过多或者类加载过多,容易导致永久代内存溢出(PermGen Space OutOfMemoryError)的问题。
从 Java 8 开始,永久代被移除,取而代之的是元空间(Metaspace)。元空间不再是 JVM 堆的一部分,而是使用本地内存(Native Memory)来存储类的元数据信息、常量池等。这样做的好处是可以避免永久代内存溢出的问题,并且可以动态调整元空间的大小,提高了 JVM 对类元数据的管理效率。
14.SpringBoot如何做到启动的时候注入一些Bean?
在 Spring Boot 中,可以在应用启动时注入一些 Bean 的方式有多种,以下是其中的一些常见方式:
-
使用@ComponentScan注解:在启动类(例如 Application.java)上使用
@ComponentScan
注解指定要扫描的包路径,Spring Boot 会自动扫描并注册带有@Component
、@Service
、@Repository
、@Controller
等注解的 Bean。@SpringBootApplication @ComponentScan(basePackages = "com.example") public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
-
使用@Bean注解:在任何一个配置类中,使用
@Bean
注解定义要注入的 Bean,并将其加入到 Spring 容器中。@Configuration public class AppConfig { @Bean public MyBean myBean() { return new MyBean(); } }
-
使用@Configuration注解:可以创建一个配置类,使用
@Configuration
注解,并在该类中使用@Bean
注解定义要注入的 Bean。@Configuration public class AppConfig { @Bean public MyBean myBean() { return new MyBean(); } }
-
使用@Import注解:可以在配置类上使用
@Import
注解引入其他配置类,从而注入其中定义的 Bean。@Configuration @Import(AppConfig.class) public class AnotherConfig { // 其他配置 }
-
使用@Component注解:在任意一个类上使用
@Component
注解定义 Bean,并使用@Autowired
注解在其他类中注入该 Bean。@Component public class MyBean { // Bean 的实现 }
以上方式可以根据具体需求选择合适的方式来注入 Bean,在 Spring Boot 应用启动时,这些 Bean 会被 Spring 容器扫描并注入,可以在整个应用中使用。
15.SpringBoot 默认的包扫描路径?
Spring Boot 默认的包扫描路径是启动类所在包及其子包。具体来说,Spring Boot 会扫描启动类所在的包及其子包下所有带有 @Component
、@Service
、@Repository
、@Controller
等注解的类,并将它们注册为 Spring Bean。
例如,如果启动类 Application.java
的完整包路径是 com.example.demo
,那么 Spring Boot 默认会扫描 com.example.demo
包及其子包下的所有带有相应注解的类,并将它们注册为 Spring Bean。
如果需要修改默认的包扫描路径,可以在启动类上使用 @ComponentScan
注解来指定要扫描的包路径。例如:
@SpringBootApplication
@ComponentScan(basePackages = "com.example.custom")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
在上面的例子中,Spring Boot 将会扫描 com.example.custom
包及其子包下的所有类,并注册为 Spring Bean。
16.TCP 为什么需要 4 次挥手?
TCP 协议需要四次挥手是为了确保数据的可靠传输和连接的正常关闭。四次挥手的过程如下:
-
第一次挥手(FIN=1,序号 seq=x):
- 主动关闭方发送一个 FIN 报文段,表示它没有数据要发送了,但仍可以接收数据。
- 发送 FIN 报文段后,主动关闭方进入 FIN_WAIT_1 状态,等待对方的确认。
-
第二次挥手(ACK=1,ACK 序号 ack=x+1,序号 seq=y):
- 被动关闭方收到 FIN 报文段后,发送一个 ACK 报文段,确认收到关闭请求,并告诉对方自己也要关闭连接。
- 发送 ACK 报文段后,被动关闭方进入 CLOSE_WAIT 状态,等待发送完自己的数据后再发送 FIN 报文段。
-
第三次挥手(FIN=1,ACK=1,ACK 序号 ack=y+1,序号 seq=z):
- 被动关闭方发送一个 FIN 报文段,表示自己也没有数据要发送了,可以关闭连接。
- 发送 FIN 报文段后,被动关闭方进入 LAST_ACK 状态,等待对方的确认。
-
第四次挥手(ACK=1,ACK 序号 ack=z+1,序号 seq=y+1):
- 主动关闭方收到 FIN 报文段后,发送一个 ACK 报文段,确认收到关闭请求,并告诉对方自己也关闭连接。
- 发送 ACK 报文段后,主动关闭方进入 TIME_WAIT 状态,等待可能出现的延迟报文段。
四次挥手的过程中,主要是为了确保双方都能够安全地关闭连接,保证数据的可靠传输和顺序性。第一次挥手是为了告知对方自己没有数据要发送了;第二次挥手是为了确认对方也要关闭连接;第三次挥手是为了告知对方自己也要关闭连接;第四次挥手是为了确认对方关闭请求,并告知对方自己也关闭连接。通过四次挥手的过程,可以有效地避免因连接的关闭而导致数据丢失或者连接状态不一致的问题。
17.java.util.concurrent包下了解哪些?
java.util.concurrent
包提供了一组用于并发编程的工具和类,主要用于处理多线程编程中的并发和同步操作。以下是该包中常用的类和接口:
-
线程池(Executor Framework):
Executor
:执行器接口,定义了执行任务的方法。ExecutorService
:执行器服务接口,扩展了 Executor 接口,提供了更丰富的任务管理方法。ThreadPoolExecutor
:线程池执行器实现类,可以创建并管理线程池。
-
并发集合(Concurrent Collections):
ConcurrentHashMap
:并发安全的哈希表实现,用于替代 Hashtable。ConcurrentLinkedQueue
、ConcurrentLinkedDeque
:并发安全的队列和双端队列实现。ConcurrentSkipListMap
、ConcurrentSkipListSet
:并发安全的跳表实现,支持有序集合操作。
-
同步器(Synchronizers):
CountDownLatch
:倒计时门闩,用于等待其他线程执行完成再继续执行。CyclicBarrier
:循环屏障,用于等待一组线程达到某个屏障点后再继续执行。Semaphore
:信号量,用于控制同时访问某个资源的线程数量。
-
并发工具(Utilities):
Lock
、ReentrantLock
:显示锁接口和可重入锁实现,提供了比 synchronized 更灵活的同步机制。Condition
、ReadWriteLock
:条件变量和读写锁,用于更细粒度的线程等待和读写操作。Atomic
类(如AtomicInteger
、AtomicLong
、AtomicReference
等):原子操作类,提供了无锁的线程安全操作。
-
并发工具类(Utility Classes):
Executors
:线程池工厂类,提供了快速创建不同类型线程池的方法。TimeUnit
:时间单位枚举类,用于定义时间单位。ConcurrentHashMap.KeySetView
:ConcurrentHashMap
的键集合视图,支持并发安全的键集合操作。
这些类和接口提供了丰富的并发编程工具,可以帮助开发者更方便地处理多线程编程中的并发和同步问题。
18.为什么Redis单线程执行还这么快?
Redis 单线程执行依然能够保持高性能主要有以下几个原因:
-
非阻塞 I/O:Redis 使用非阻塞 I/O 模型,利用了操作系统提供的 epoll (Linux) 或 kqueue (BSD/macOS) 等事件机制,在单线程下可以处理大量的并发连接请求,减少了线程切换和同步等额外开销。
-
基于内存的高速访问:Redis 数据存储在内存中,读写操作都是基于内存的高速访问,相比于磁盘 I/O,内存访问速度更快,可以提供快速的响应速度。
-
单线程避免了竞态条件:由于 Redis 是单线程执行的,避免了多线程并发操作时可能出现的竞态条件和锁竞争问题,简化了并发编程模型,减少了线程同步开销。
-
采用事件驱动模型:Redis 使用事件驱动模型处理客户端请求,通过事件循环机制监听和处理网络事件,只有在需要执行操作时才会进行 CPU 计算,避免了 CPU 过度消耗。
-
高效的数据结构和算法:Redis 内部使用了高效的数据结构和算法,如哈希表、跳表、有序集合等,对于不同的数据操作能够提供快速的响应和高效的存储。
-
优化的网络协议:Redis 使用自定义的 RESP(REdis Serialization Protocol)网络协议,协议简单高效,减少了网络传输的开销和数据解析的时间。
总体来说,Redis 在设计和实现上充分考虑了高性能和高并发的需求,采用了多种优化策略和技术手段,使得单线程执行依然能够保持高速响应和高并发处理能力。
19.Redis如何扫描前缀相同的key?
Redis 提供了 SCAN
命令用于扫描指定模式的键,可以通过指定前缀来扫描前缀相同的键。SCAN
命令返回一个游标(cursor)和一批匹配的键,可以多次调用 SCAN
命令来逐批获取匹配的键。以下是使用 SCAN
命令扫描前缀相同的键的示例:
-
使用
SCAN
命令扫描键:SCAN 0 MATCH prefix*
SCAN
命令的第一个参数是游标,初始为 0。MATCH
参数用于指定匹配模式,可以使用通配符*
来匹配前缀相同的键。
-
示例代码:
Jedis jedis = new Jedis("localhost", 6379); String cursor = "0"; String pattern = "prefix*"; ScanParams params = new ScanParams().match(pattern); do { ScanResult<String> scanResult = jedis.scan(cursor, params); List<String> keys = scanResult.getResult(); for (String key : keys) { System.out.println(key); } cursor = scanResult.getCursor(); } while (!"0".equals(cursor));
上述示例中,通过循环调用
SCAN
命令来获取所有匹配模式的键,并输出到控制台。
需要注意的是,由于 SCAN
命令是迭代式的扫描操作,可以在大量键的情况下进行高效的扫描,但也需要注意控制游标的使用,避免一次性返回过多的键,影响性能。
20.Redis的keys和scan有什么区别?
KEYS
和 SCAN
是 Redis 中用于扫描键的两个命令,它们有一些重要的区别:
-
返回结果的方式:
KEYS
命令会立即返回所有匹配模式的键,返回的是一个数组,包含了所有匹配的键。如果匹配的键很多,一次性返回可能会影响 Redis 的性能。SCAN
命令是迭代式的扫描操作,会返回一个游标和一批匹配的键,客户端需要通过多次调用SCAN
命令来逐批获取匹配的键,避免一次性返回过多的键。
-
性能影响:
KEYS
命令在匹配的键很多时会对 Redis 服务器产生较大的负载,因为一次性返回大量的键可能会导致服务器阻塞或延迟。SCAN
命令通过分批次返回匹配的键,可以在大量键的情况下进行高效的扫描,减少了一次性返回大量键对服务器性能的影响。
-
用途:
KEYS
命令通常用于开发、调试或者管理工作中,例如查找特定前缀或后缀的键。SCAN
命令更适合用于生产环境下的大规模键扫描,通过分批次返回匹配的键,减少对服务器性能的影响。
综上所述,SCAN
命令相比于 KEYS
命令更适合在生产环境下进行大规模键扫描,可以高效地处理大量键的情况,并且不会对服务器性能产生过大的影响。
21.如何使用Redis实现分布式锁?
使用 Redis 实现分布式锁可以通过以下步骤完成:
-
获取锁:在需要获取锁的地方执行如下代码:
Jedis jedis = new Jedis("localhost", 6379); String lockKey = "resource_lock"; String requestId = UUID.randomUUID().toString(); int expireTime = 30000; // 锁的过期时间,单位毫秒 boolean locked = jedis.set(lockKey, requestId, "NX", "PX", expireTime) != null;
lockKey
:锁的名称,用于区分不同资源的锁。requestId
:请求标识,用于区分不同客户端的锁请求。expireTime
:锁的过期时间,避免锁被永久占用,导致死锁。
-
释放锁:在不再需要锁的地方执行如下代码:
if (locked) { jedis.del(lockKey); }
通过以上代码,可以实现基于 Redis 的分布式锁。具体实现思路是利用 Redis 的 SETNX(SET if Not eXists)命令和过期时间来实现锁的获取和释放:
- 使用
SETNX
命令尝试设置锁,如果成功返回 1(表示锁被获取),否则返回 0(表示锁已被占用)。 - 设置锁的过期时间,避免因为某个客户端意外终止而导致锁一直被占用。
需要注意的是,分布式锁的实现需要考虑到以下问题:
- 锁的粒度:锁的粒度应该尽量小,避免对整个系统加锁导致性能瓶颈。
- 锁的超时处理:获取锁后,需要考虑处理业务逻辑的时间,确保在锁超时前完成操作,避免锁被其他请求获取。
- 锁的释放:确保在不再需要锁的地方及时释放锁,避免死锁或者长时间占用锁。
以上是简单的分布式锁实现方式,实际应用中可能需要根据具体场景进行适当调整和优化。
22.长连接的好处与坏处?
长连接(Keep-Alive)指的是在网络通信中,客户端与服务器之间建立一次连接后,在一段时间内保持连接处于打开状态,以便后续可以重复使用这个连接进行多次请求和响应。长连接的好处和坏处如下:
好处:
-
减少连接建立和关闭的开销:长连接可以减少因频繁建立和关闭连接而产生的额外开销,例如 TCP 的三次握手和四次挥手过程,以及服务器资源的分配和释放。
-
降低网络延迟:长连接可以减少每次请求的网络延迟,因为在已建立的连接上进行通信,避免了重新建立连接的时间消耗。
-
节省带宽资源:长连接可以通过复用连接来节省带宽资源,因为在已建立的连接上进行通信时,不需要重复发送 TCP 握手等信息。
-
提高并发处理能力:长连接可以降低服务器的连接管理负担,提高服务器的并发处理能力,因为不需要频繁地创建和销毁连接。
坏处:
-
占用服务器资源:长连接会占用服务器的连接资源,尤其在高并发环境下,长时间保持连接可能会占用较多的服务器资源。
-
容易产生连接泄漏:长连接如果没有及时释放,可能会导致连接泄漏问题,造成服务器连接资源的浪费。
-
可能引发连接状态不一致:长连接在保持连接期间,可能会出现连接状态不一致的情况,例如客户端或服务器意外断开连接,但另一方并未感知到连接已断开。
-
适用场景有限:长连接适用于一些需要频繁通信并且连接持续时间较长的场景,对于短时通信或者连接使用频率较低的场景,长连接并不适合。
综合考虑,长连接在合适的场景下可以提高网络通信的效率和性能,但需要注意合理管理连接资源,避免因连接资源过多而导致的性能问题。
23.TCP和HTTP有什么区别?
TCP(Transmission Control Protocol)和HTTP(Hypertext Transfer Protocol)是两种不同的网络协议,各自有不同的作用和特点。它们的主要区别如下:
-
功能:
- TCP 是一种传输层协议,主要负责在网络中可靠地传输数据,确保数据的完整性、顺序性和可靠性。
- HTTP 是一种应用层协议,主要用于在客户端和服务器之间传输超文本文档,支持 Web 页面的浏览和数据交互。
-
层级:
- TCP 是传输层协议,位于 OSI 模型的第四层(传输层)。
- HTTP 是应用层协议,位于 OSI 模型的第七层(应用层)。
-
传输方式:
- TCP 是一种面向连接的协议,通过建立连接、传输数据、断开连接的方式来传输数据。
- HTTP 是基于 TCP 的应用层协议,在 TCP 连接上进行数据传输。
-
数据格式:
- TCP 没有特定的数据格式,它只负责在网络中传输数据的可靠性和完整性。
- HTTP 使用一种基于文本的协议格式,包含请求头、请求体、响应头和响应体等部分。
-
端口号:
- TCP 使用端口号来标识不同的网络应用程序或服务,常见的 HTTP 通信使用的端口号是 80(HTTP)和 443(HTTPS)。
-
状态:
- TCP 是一种状态传输协议,建立连接时需要进行三次握手,断开连接时需要进行四次挥手。
- HTTP 是一种无状态协议,每个请求和响应之间是相互独立的,不保存连接状态。
综上所述,TCP 是一种底层的传输协议,负责数据的可靠传输;而 HTTP 是一种基于 TCP 的应用层协议,用于在 Web 上进行数据交互和资源传输。它们在功能、层级、传输方式、数据格式、端口号和状态等方面有着明显的区别。
24.说说HTTPS的执行流程?
HTTPS(Hypertext Transfer Protocol Secure)是基于 HTTP 协议和 SSL/TLS 协议的加密传输协议,用于在网络上安全地传输数据。HTTPS 的执行流程如下:
-
建立连接:
- 客户端向服务器发起 HTTPS 请求,请求连接到指定的 HTTPS 端口(通常是 443 端口)。
- 服务器响应客户端请求,返回证书信息。
-
验证证书:
- 客户端收到服务器返回的证书,会对证书进行验证,包括验证证书的有效性、证书是否过期、证书的颁发机构是否可信等。
- 如果证书验证通过,客户端生成随机数(pre-master secret)并使用服务器公钥加密,发送给服务器。
-
建立安全连接:
- 服务器使用私钥解密客户端发送的随机数,得到 pre-master secret。
- 客户端和服务器利用 pre-master secret 和双方的随机数生成对称加密的会话密钥(session key),用于后续的数据加密和解密。
-
加密通信:
- 客户端和服务器使用协商好的会话密钥进行对称加密通信,保证数据在传输过程中的机密性。
- 客户端发送加密的 HTTP 请求,服务器接收并解密请求,执行相应的操作。
-
数据传输:
- 在建立安全连接后,客户端和服务器之间的数据传输都是经过加密的,保证数据的保密性和完整性。
- 客户端和服务器之间的通信过程中,每次通信都会重新使用会话密钥,确保通信的安全性。
-
断开连接:
- 当通信结束时,客户端和服务器可以选择断开连接或者保持连接,如果断开连接,会话密钥也会随之失效。
通过以上流程,HTTPS 实现了在不可信网络上的安全数据传输,保护了用户的隐私和数据安全。主要通过 SSL/TLS 协议进行加密通信,确保数据在传输过程中的保密性、完整性和真实性。
25.说说ThreadLocal底层实现?
ThreadLocal
是 Java 中用于实现线程局部变量的类,它允许将数据与线程关联起来,使得每个线程都可以拥有独立的变量副本。ThreadLocal
的底层实现主要依靠 Thread
类的 threadLocals
属性和 ThreadLocalMap
类。以下是 ThreadLocal
的底层实现原理:
-
ThreadLocalMap:
- 每个线程对象都有一个
ThreadLocalMap
类型的属性threadLocals
,用于存储线程局部变量。 ThreadLocalMap
是一个自定义的哈希表,其 key 是ThreadLocal
对象,value 是对应的变量副本。
- 每个线程对象都有一个
-
ThreadLocal 对象:
ThreadLocal
类中有一个静态内部类ThreadLocalMap
,用于存储当前线程的局部变量。ThreadLocal
对象通过Thread.currentThread()
方法获取当前线程,然后从当前线程的threadLocals
属性中获取ThreadLocalMap
对象。
-
存取操作:
- 当调用
ThreadLocal
的set
方法时,实际上是调用当前线程的threadLocals
的set
方法,在ThreadLocalMap
中以ThreadLocal
对象为 key,存储对应的变量副本。 - 当调用
ThreadLocal
的get
方法时,实际上是调用当前线程的threadLocals
的get
方法,从ThreadLocalMap
中获取对应的变量副本。
- 当调用
-
清理操作:
- 当
ThreadLocal
对象被销毁或者不再被使用时,如果没有手动调用remove
方法来清理变量副本,可能会导致内存泄漏。 - Java 提供了
ThreadLocal
的弱引用版本WeakThreadLocal
,可以在ThreadLocal
对象没有强引用时自动回收变量副本,避免内存泄漏问题。
- 当
综上所述,ThreadLocal
的底层实现依赖于 Thread
类的 threadLocals
属性和 ThreadLocalMap
类,通过哈希表存储线程局部变量,实现了线程隔离和独立的变量副本。需要注意及时清理不再使用的 ThreadLocal
对象,避免内存泄漏问题。
26.ThreadLocal父线程和子线程的数据传递?
ThreadLocal
在父线程和子线程之间传递数据时,需要注意以下几点:
-
父线程设置数据:在父线程中使用
ThreadLocal
的set
方法设置数据。 -
子线程获取数据:在子线程中可以直接通过
ThreadLocal
的get
方法获取父线程设置的数据,因为ThreadLocal
的数据对于每个线程都是独立的。 -
传递性:当子线程创建时,子线程会继承父线程的
ThreadLocal
变量,也就是说,子线程会拥有父线程设置的ThreadLocal
变量的副本。
下面是一个简单的示例代码,演示了父线程设置数据,子线程获取数据的过程:
public class ThreadLocalDemo {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 在父线程中设置数据
threadLocal.set("父线程数据");
// 创建子线程并启动
Thread thread = new Thread(() -> {
// 子线程获取父线程设置的数据
String data = threadLocal.get();
System.out.println("子线程获取到的数据:" + data);
});
thread.start();
}
}
在这个示例中,父线程通过 ThreadLocal
的 set
方法设置了数据,然后创建了一个子线程,在子线程中可以通过 ThreadLocal
的 get
方法直接获取到父线程设置的数据。需要注意的是,ThreadLocal
的数据在每个线程中是独立的,子线程只能获取到父线程设置的数据的副本,对数据的修改不会影响父线程的数据。
27.synchronized和volatile有什么区别?
volatile
和 synchronized
是 Java 中用于处理多线程并发问题的两种不同机制。它们在使用场景、功能和实现方式上有明显的区别。
1. 功能和作用:
-
volatile:
- 可见性:保证一个变量在多个线程之间的可见性。当一个线程修改了被
volatile
修饰的变量,其他线程能够立即看到这个修改。 - 禁止指令重排序:防止编译器和 CPU 对代码进行重排序,确保代码按照程序的顺序执行。
- 不保证原子性:对
volatile
变量的操作不是原子的,因此不能保证并发情况下的安全性。
- 可见性:保证一个变量在多个线程之间的可见性。当一个线程修改了被
-
synchronized:
- 可见性:进入
synchronized
块之前,线程会清空工作内存,重新从主内存中加载最新的变量值;在退出synchronized
块时,会把变量的修改刷新到主内存中。 - 原子性:保证对同步代码块的访问是互斥的,即同一时刻只有一个线程可以执行同步代码块,保证了对变量操作的原子性。
- 阻塞机制:线程在进入
synchronized
块时会获得锁,其他线程在尝试进入该同步块时会被阻塞,直到获得锁。
- 可见性:进入
2. 使用场景:
-
volatile:
- 适用于状态标记的变量(如布尔值),需要简单地通知其他线程状态变化。
- 适用于轻量级的读写操作,不涉及复合操作(如
i++
或者i = i + 1
等)。
-
synchronized:
- 适用于需要对代码块或方法进行同步,确保同一时刻只有一个线程执行。
- 适用于需要保证复合操作(如自增、自减)的原子性和一致性的场景。
3. 实现原理:
-
volatile:
- 基于内存屏障(Memory Barrier)实现。内存屏障是一种 CPU 指令,用于禁止特定的指令重排序,确保变量的可见性和顺序性。
-
synchronized:
- 基于对象的监视器锁(Monitor)实现。每个对象都有一个监视器,当线程进入同步块时,会尝试获取该对象的监视器锁,成功获取锁后才能执行同步块的代码,退出同步块时释放锁。
4. 性能:
-
volatile:
- 相对轻量级,不会造成线程的阻塞和上下文切换,性能开销较低。
- 只适用于变量的读写操作,不适用于复合操作。
-
synchronized:
- 相对重量级,可能会造成线程的阻塞和上下文切换,性能开销较高。
- 能保证同步代码块的互斥执行,适用于需要严格同步的场景。
总结:
- 使用
volatile
修饰的变量能够保证可见性和禁止指令重排序,但不保证操作的原子性,适用于状态标记等简单场景。 - 使用
synchronized
关键字可以保证代码块的可见性和原子性,适用于需要严格同步和互斥访问的场景。
28.说说ThreadPoolExecutor的参数?
ThreadPoolExecutor
是 Java 提供的一个强大的线程池实现类,它可以通过灵活的参数配置来管理线程的创建、执行和销毁。ThreadPoolExecutor
构造方法的参数如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数解释:
-
corePoolSize(核心线程数):
- 指定了线程池中始终保持存活的核心线程数量。即使这些线程处于空闲状态,也不会被回收,除非
allowCoreThreadTimeOut
被设置为true
。 - 当任务提交时,如果当前线程数少于
corePoolSize
,则会创建新线程来处理任务,即使此时有空闲的核心线程。
- 指定了线程池中始终保持存活的核心线程数量。即使这些线程处于空闲状态,也不会被回收,除非
-
maximumPoolSize(最大线程数):
- 指定了线程池能够容纳的最大线程数量。核心线程之外的线程称为非核心线程,它们在空闲时间超过
keepAliveTime
时会被终止和回收。 - 当任务提交时,如果当前线程数大于等于
corePoolSize
且小于maximumPoolSize
,则会创建非核心线程来处理任务。
- 指定了线程池能够容纳的最大线程数量。核心线程之外的线程称为非核心线程,它们在空闲时间超过
-
keepAliveTime(线程存活时间):
- 指定了非核心线程在空闲时的存活时间。超过这个时间,非核心线程会被终止和回收。
- 当
allowCoreThreadTimeOut
被设置为true
时,此参数也适用于核心线程。
-
unit(时间单位):
keepAliveTime
参数的时间单位。可以是TimeUnit
枚举中的任意一个值,如TimeUnit.SECONDS
、TimeUnit.MILLISECONDS
等。
-
workQueue(任务队列):
- 一个阻塞队列,用于保存等待执行的任务。可以选择不同类型的阻塞队列实现来满足不同的需求,如
ArrayBlockingQueue
、LinkedBlockingQueue
、SynchronousQueue
等。
- 一个阻塞队列,用于保存等待执行的任务。可以选择不同类型的阻塞队列实现来满足不同的需求,如
-
threadFactory(线程工厂):
- 用于创建新线程的工厂。可以通过自定义
ThreadFactory
来为线程池创建线程,设置线程的名称、优先级等。
- 用于创建新线程的工厂。可以通过自定义
-
handler(拒绝策略):
- 当线程池和任务队列都满了,无法处理新任务时,使用
RejectedExecutionHandler
来处理被拒绝的任务。Java 提供了几种常见的拒绝策略:AbortPolicy
:默认策略,直接抛出RejectedExecutionException
异常。CallerRunsPolicy
:调用执行execute
方法的线程运行被拒绝的任务。DiscardPolicy
:直接丢弃被拒绝的任务,不予处理。DiscardOldestPolicy
:丢弃最早的未处理任务,然后重新尝试执行被拒绝的任务。
- 当线程池和任务队列都满了,无法处理新任务时,使用
示例:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
60, // keepAliveTime
TimeUnit.SECONDS, // unit
new LinkedBlockingQueue<>(100), // workQueue
Executors.defaultThreadFactory(), // threadFactory
new ThreadPoolExecutor.AbortPolicy() // handler
);
这个示例创建了一个核心线程数为 5,最大线程数为 10,线程存活时间为 60 秒,任务队列容量为 100 的线程池。默认线程工厂用于创建线程,拒绝策略为直接抛出异常。
通过配置这些参数,可以灵活地控制线程池的行为和性能,以适应不同的并发场景和需求。
29.workQueue(任务队列)
在 ThreadPoolExecutor
中,任务队列 (workQueue
) 是用于存储等待执行的任务的阻塞队列。Java 提供了几种常见的任务队列类型,每种队列都有不同的特性和适用场景。以下是常见的几种任务队列:
1. ArrayBlockingQueue
-
特点:基于数组的有界阻塞队列,按照先进先出(FIFO)顺序存储任务。
-
适用场景:适用于有界任务队列的场景,能够限制队列的长度,防止过多任务堆积导致内存耗尽。
-
示例:
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(100);
2. LinkedBlockingQueue
-
特点:基于链表的阻塞队列,可以是有界的也可以是无界的(默认最大值为 Integer.MAX_VALUE),按照先进先出(FIFO)顺序存储任务。
-
适用场景:适用于任务提交频率高于任务处理频率的场景,能够有效缓冲任务。
-
示例:
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(); // 或者指定容量 BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(100);
3. SynchronousQueue
-
特点:不存储任务的队列,每个插入操作必须等待另一个线程的相应移除操作,即每个任务必须被立即处理或被拒绝。
-
适用场景:适用于直接提交任务的场景,任务直接交给线程执行而不进行排队,常用于对响应时间要求较高的场景。
-
示例:
BlockingQueue<Runnable> queue = new SynchronousQueue<>();
4. PriorityBlockingQueue
-
特点:基于优先级的无界阻塞队列,任务按照优先级排序,优先级由任务对象的
compareTo
方法决定。 -
适用场景:适用于需要对任务进行优先级排序的场景。
-
示例:
BlockingQueue<Runnable> queue = new PriorityBlockingQueue<>();
5. DelayQueue
-
特点:基于优先级的无界阻塞队列,只有在延迟期满时才能从队列中取出任务。任务必须实现
Delayed
接口。 -
适用场景:适用于需要对任务进行延迟处理的场景。
-
示例:
BlockingQueue<Delayed> queue = new DelayQueue<>();
选择合适的任务队列:
- ArrayBlockingQueue 和 LinkedBlockingQueue:适用于大多数需要存储和调度任务的场景。
ArrayBlockingQueue
可以防止过多任务堆积,LinkedBlockingQueue
能够有效缓冲任务。 - SynchronousQueue:适用于高实时性场景,任务直接提交给线程执行,不进行排队。
- PriorityBlockingQueue:适用于需要对任务进行优先级排序的场景。
- DelayQueue:适用于需要对任务进行延迟处理的场景。
根据具体的应用需求选择合适的任务队列,可以更好地优化线程池的性能和行为。
30.线程池的执行流程
使用线程池可以提高资源利用率,减少线程创建和销毁的开销,同时便于管理和控制线程的执行。以下是使用线程池的典型流程,以及 ThreadPoolExecutor
的执行流程。
使用线程池的流程
- 创建线程池:根据需要选择合适的线程池类型,可以使用
Executors
工具类创建常见的线程池,也可以直接创建ThreadPoolExecutor
实例进行自定义配置。 - 提交任务:通过线程池的
execute
或submit
方法提交任务(实现Runnable
或Callable
接口的对象)。 - 执行任务:线程池中的线程从任务队列中取出任务并执行。
- 关闭线程池:当不再需要提交新任务时,调用
shutdown
或shutdownNow
方法关闭线程池,确保所有任务都被处理完。
线程池的执行流程
-
初始化线程池:
- 创建
ThreadPoolExecutor
实例时,配置核心线程数、最大线程数、线程存活时间、时间单位、任务队列、线程工厂和拒绝策略。
- 创建
-
提交任务:
- 任务通过
execute
或submit
方法提交到线程池。execute
方法用于提交不需要返回结果的任务(Runnable
),submit
方法用于提交需要返回结果的任务(Callable
)。
- 任务通过
-
任务处理流程:
- 核心线程池处理:
- 如果当前线程数少于核心线程数,则创建新线程执行任务,即使有空闲的核心线程。
- 任务队列处理:
- 如果当前线程数大于等于核心线程数,且任务队列未满,则将任务加入任务队列等待处理。
- 最大线程池处理:
- 如果当前线程数大于等于核心线程数,且任务队列已满,但线程数小于最大线程数,则创建新线程执行任务。
- 拒绝策略处理:
- 如果当前线程数达到最大线程数且任务队列已满,则执行拒绝策略。默认策略是
AbortPolicy
,直接抛出RejectedExecutionException
异常。
- 如果当前线程数达到最大线程数且任务队列已满,则执行拒绝策略。默认策略是
- 核心线程池处理:
-
执行任务:
- 线程从任务队列中取出任务并执行。线程执行任务时会调用任务的
run
方法。
- 线程从任务队列中取出任务并执行。线程执行任务时会调用任务的
-
线程存活时间:
- 非核心线程在空闲时间超过
keepAliveTime
后会被终止和回收。如果设置了allowCoreThreadTimeOut
为true
,核心线程在空闲时间超过keepAliveTime
后也会被终止和回收。
- 非核心线程在空闲时间超过
-
关闭线程池:
- 调用
shutdown
方法后,线程池不再接收新任务,但会继续执行已提交的任务,直到所有任务完成。 - 调用
shutdownNow
方法后,线程池不再接收新任务,并尝试停止正在执行的任务,返回尚未执行的任务列表。
- 调用
示例代码
以下是一个使用 ThreadPoolExecutor
的示例代码:
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
60, // keepAliveTime
TimeUnit.SECONDS, // unit
new LinkedBlockingQueue<>(100), // workQueue
Executors.defaultThreadFactory(), // threadFactory
new ThreadPoolExecutor.AbortPolicy() // handler
);
// 提交任务
for (int i = 0; i < 20; i++) {
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " is executing task.");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
}
这个示例创建了一个自定义配置的 ThreadPoolExecutor
,提交了20个任务到线程池,并在所有任务完成后关闭线程池。
31.说说MySQL的事务?
MySQL 的事务 是指一组操作(SQL语句)作为一个单元执行,事务保证了这些操作要么全部成功,要么全部失败。事务具有四个主要特性,通常称为 ACID 特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
1. 原子性(Atomicity)
- 定义:事务中的所有操作要么全部执行,要么全部不执行。即,事务是不可分割的最小操作单元。
- 示例:在银行转账操作中,A 账户转出 100 元,B 账户转入 100 元,这两个操作要么全部执行,要么全部不执行。
2. 一致性(Consistency)
- 定义:事务完成后,数据库必须从一个一致性状态转换到另一个一致性状态。在一致性状态中,数据满足所有完整性约束。
- 示例:在银行转账操作中,A 和 B 账户余额之和在事务开始和结束时应该保持不变。
3. 隔离性(Isolation)
- 定义:一个事务的执行不应影响其他事务的执行。即,一个事务在未提交前,对其他事务是不可见的。
- 示例:在并发操作中,一个事务对同一数据的修改在其提交之前对其他事务是不可见的。
4. 持久性(Durability)
- 定义:一旦事务提交,其所做的修改就会永久保存到数据库中,即使系统崩溃,数据也不会丢失。
- 示例:在银行转账操作中,A 账户扣除的 100 元和 B 账户增加的 100 元在事务提交后将永久保存在数据库中。
MySQL 事务控制语句
-
开始事务:
START TRANSACTION;
或者
BEGIN;
-
提交事务:
COMMIT;
-
回滚事务:
ROLLBACK;
-
保存点(部分回滚):
SAVEPOINT savepoint_name;
-
回滚到保存点:
ROLLBACK TO SAVEPOINT savepoint_name;
-
释放保存点:
RELEASE SAVEPOINT savepoint_name;
事务隔离级别
MySQL 支持四种事务隔离级别,每种级别定义了事务间相互隔离的程度,从而解决不同的并发问题:
1. 读未提交(READ UNCOMMITTED)
- 允许事务读取未提交的数据,可能会导致脏读(Dirty Read)。
- 并发问题:脏读、不可重复读、幻读。
2. 读已提交(READ COMMITTED)
- 只允许事务读取已提交的数据,避免脏读。
- 并发问题:不可重复读、幻读。
3. 可重复读(REPEATABLE READ)
- 确保在同一个事务中多次读取同一数据的结果一致,避免脏读和不可重复读。
- 并发问题:幻读。
- 注:InnoDB 引擎通过间隙锁(Next-Key Locking)机制解决了幻读问题。
4. 可序列化(SERIALIZABLE)
- 强制事务顺序执行,完全避免脏读、不可重复读和幻读,但性能较差。
- 并发问题:无。
设置事务隔离级别
可以在会话级别或全局级别设置事务隔离级别:
-
会话级别:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-
全局级别:
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
示例
以下是一个使用事务的示例代码:
-- 开始事务
START TRANSACTION;
-- 执行操作
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
-- 提交事务
COMMIT;
在这个示例中,如果两个更新操作都成功,事务将提交,两个更新操作的结果将被永久保存。如果其中任何一个操作失败,事务将回滚,两个更新操作的结果都不会保存。
32.Spring事务是怎么实现的?
Spring事务的实现 基于AOP(面向切面编程)和事务管理器(Transaction Manager)实现,它提供了声明式事务管理和编程式事务管理。Spring事务管理使开发人员能够将事务逻辑从业务逻辑中分离,从而简化了事务管理。
声明式事务管理 是通过AOP切面在运行时动态地将事务管理逻辑应用到目标方法上。它主要通过@Transactional
注解实现。
编程式事务管理 则是通过手动编写代码来控制事务的边界,通常使用TransactionTemplate
或PlatformTransactionManager
接口。
以下是Spring事务实现的主要步骤和机制:
1. 声明式事务管理
使用@Transactional
注解:
- 在类或方法上添加
@Transactional
注解。 - Spring在启动时会扫描这些注解,并使用AOP代理这些带有
@Transactional
注解的方法。 - 当带有
@Transactional
的方法被调用时,Spring会在方法执行前开启一个事务,在方法执行后根据方法执行情况提交或回滚事务。
配置事务管理器:
- Spring需要配置一个事务管理器(例如
DataSourceTransactionManager
)来处理事务的开始、提交和回滚。
@Configuration
@EnableTransactionManagement
public class AppConfig {
@Bean
public DataSource dataSource() {
// 配置数据源
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
示例代码:
@Service
public class MyService {
@Transactional
public void performTransaction() {
// 事务性操作
}
}
当调用performTransaction
方法时,Spring会自动开启一个事务,方法执行完成后,事务将被提交。如果方法执行过程中抛出未捕获的运行时异常,事务将被回滚。
2. 编程式事务管理
使用TransactionTemplate
:
TransactionTemplate
封装了事务的开始、提交和回滚逻辑,开发者可以在代码中明确地控制事务边界。
@Service
public class MyService {
private final TransactionTemplate transactionTemplate;
@Autowired
public MyService(TransactionTemplate transactionTemplate) {
this.transactionTemplate = transactionTemplate;
}
public void performTransaction() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
// 事务性操作
} catch (Exception e) {
status.setRollbackOnly();
}
}
});
}
}
使用PlatformTransactionManager
:
- 通过直接使用
PlatformTransactionManager
接口来手动管理事务。
@Service
public class MyService {
private final PlatformTransactionManager transactionManager;
@Autowired
public MyService(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
public void performTransaction() {
TransactionDefinition definition = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(definition);
try {
// 事务性操作
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
}
}
}
事务传播行为:
- Spring事务支持多种传播行为(Propagation),如
REQUIRED
、REQUIRES_NEW
等,用于控制方法间的事务传播方式。
隔离级别:
@Transactional
注解支持指定事务的隔离级别,如Isolation.READ_COMMITTED
等。
总结:
Spring事务通过AOP和事务管理器结合,使得声明式事务管理非常简洁,而编程式事务管理则提供了更灵活的控制方式。通过配置事务管理器并使用@Transactional
注解或编程式管理,Spring提供了强大的事务管理能力,使得开发者可以专注于业务逻辑而无需担心底层的事务处理。