专业在线打字练习网站-巧手打字通,只输出有价值的知识。
前言
系统性能如同企业的生命线,一丝瓶颈都可能引发连锁反应。本次“巧手打字通课堂”将带您深入剖析那些隐藏在背后的性能杀手,让我们一起来揭开它们的神秘面纱。
一 并行处理的那些难题
1. 锁粒度,要细化
你能看出下面代码中存在的问题吗?
public final class CachedAround implements Around {
private final transient ConcurrentMap<Object, Tunnel> tunnels = new ConcurrentHashMap<>(0);
public Object doAround(ProceedingJoinPoint point) throws Throwable {
final Object key = generateKey(point);
// 优先读取缓存结果数据
Object obj = readCache(key);
if (obj != null) {
return fromStoreValue(obj);
}
Tunnel tunnel;
synchronized (this.tunnels) {
tunnel = this.tunnels.get(key);
if (tunnel == null) {
tunnel = new Tunnel(point);
this.tunnels.put(key, tunnel);
}
}
// 执行业务处理逻辑
obj = tunnel.proceed();
// 将处理结果进行写入缓存
writeCache(point, key, obj);
return obj;
}
/**
* 业务逻辑执行
*/
private static final class Tunnel implements Serializable {
private final transient ProceedingJoinPoint point;
private volatile boolean executed;
private Object value;
Tunnel(final ProceedingJoinPoint pnt) {
this.point = pnt;
}
public synchronized Object proceed() throws Throwable {
if (!this.executed) {
value = point.proceed();
this.executed = true;
}
return value;
}
}
}
上述代码旨在首先尝试从缓存中检索服务的结果,以优化性能。若缓存中不存在有效数据(即缓存失效),则按照常规流程处理请求,并将新生成的结果存入缓存中,确保后续请求能够直接利用缓存中的数据,从而减少重复处理并提高响应效率。
该代码在低访问量时运行尚算平稳,但面对高并发时,性能瓶颈显著。
关键在于synchronized (this.tunnels)导致的单点锁争用,尤其在缓存穿透情况下,造成严重的线性阻塞。
为解决此问题,需引入更细粒度的锁机制,如基于请求类型或业务key的锁,以避免全局单一锁点的限制。
2. 请求叠加翻倍,需谨防
在通过RPC获取某些数据时,切记不要在实现层面将请求次数叠加扩散。
public static void main(String[] args) {
int requestNum=100;
List<Person> personList = new ArrayList<>();
// 一个请求,扩散成上百的RPC调用请求
for (int i = 0; i < requestNum; i++) {
// 从数据库一条一条的查询
Person person1 = findPersonFromDataBase(i);
// 从另一个服务一条一条的获取
Person person2 = findPersonFromRemoteService(i);
// 从缓存中一条一条的获取
Person person3 = getPersonFromRedis(i);
}
Person person = Person.of("John");
}
上述案例在请求量较小时不易察觉风险,但随请求量激增,问题便凸显出来。为有效预防此类情况,可通过调用批量处理接口来优化处理流程,从而降低风险。
3. @Transaction,有局限
@Transactional 注解是管理数据事务的强大工具,支持包括事务控制、读写分离在内的多种功能,它既可以应用于类级别,也能作用于方法级别,极大地方便了开发过程。
然而,在使用时需注意以下几点潜在“陷阱”,以确保事务管理的正确性和效率:
- 动态代理限制:由于@Transactional通过动态代理实现,因此被注解的方法必须是public的,否则事务管理将不会生效。这是因为动态代理仅能对外部可访问的方法进行拦截和处理。
- 异常回滚策略:默认情况下,仅当遇到RuntimeException(运行时异常)时,事务才会自动回滚。若希望所有类型的异常(包括Exception及其子类)都能触发回滚,需明确指定@Transactional(rollbackFor=Exception.class)。
- 内部方法调用失效:若一个未加@Transactional注解的方法(如A方法)在同一类中调用了另一个带有@Transactional注解的方法(如B方法),则B方法的事务注解将不会生效。这是因为内部调用不会通过代理对象进行,从而绕过了Spring的事务管理机制。
- 异常捕获与事务回滚:若事务方法内部捕获了异常且未重新抛出,则Spring无法感知到异常的发生,因此不会触发事务回滚。确保在需要事务回滚的场景中,不要无差别地捕获所有异常或至少应重新抛出需要回滚的异常。
非事务性操作影响:在事务方法中执行非事务性操作,如远程过程调用(RPC)或复杂的数据库查询,可能会显著影响事务的效率和数据库的吞吐量。这些操作可能因网络延迟、外部系统响应等因素而延长事务的持续时间,进而影响到整个应用的性能。举例:
@Transactional(rollbackFor = Exception.class) public Boolean cancelOrder(String orderNo) { // 1.查询订单状态 OrderInfo orderInfo = dbMapper.get(orderNo); // 2.订单状态幂等设计 if (orderInfo.getStatus == OrderStatusEnum.CANCEL) { return true; } // 3.订单退费,促销回滚等 if (orderInfo.getStatus >= OrderStatusEnum.PAY) { payService.rollback(orderNo); } // 4.更新数据库订单状态 int r = dbMapper.update(orderNo, OrderStatusEnum.CANCEL); return r > 0; }
在给出的示例代码中,订单状态更新的数据库事务中不恰当地包含了订单查询和支付回滚等非数据库事务性操作。这种做法延长了数据库会话的持续时间,将原本应简洁的小事务扩展成了复杂的大事务。在高并发环境下,这种设计会显著增加数据库的负载,导致吞吐量显著下降。为了优化性能,建议将除了直接更新订单状态的数据库操作(即第4步)之外的所有其他逻辑,如查询和支付回滚处理,都移至数据库事务处理流程之外进行。
总之,在设计事务时,应尽量避免在事务中执行过多或复杂的非事务性操作。
4. 数据库死锁,要关注
数据库死锁是指两个或多个事务在执行过程中,因相互竞争资源而陷入一种彼此等待对方释放资源的僵局状态,若无外部干预,这些事务将无法继续推进。
现代数据库系统普遍内置了死锁检测和解决机制,它们能够自动识别并处理这类情况,常见的解决策略包括强制终止其中一个或多个事务并回滚其操作,以打破死锁循环。但这无疑还是会对系统的性能吞吐量和业务支撑能力都产生不利影响。
死锁典型场景:
在并发执行的环境中,每个事务分别执行两条SQL语句,每条语句执行时都获取了不同的锁。随后,这些事务尝试获取对方已持有的锁,由于它们加锁的顺序不一致,导致彼此都在等待对方释放资源,从而形成了一个锁等待的环路,最终造成了死锁现象。简而言之,就是两个或更多会话(Session)因加锁顺序不一致而相互阻塞,形成了死锁。
在单表操作中,同样也有可能造成死锁:
如果name和pubtime这两个字段分别建立了索引,并且当两个事务分别通过name索引和pubtime索引访问数据时,它们可能会尝试对各自访问到的数据行在对应的聚簇索引上加上排他锁(X锁)。由于这两个事务加锁的顺序可能不一致,且每个事务都在等待对方释放其已持有的锁,这种情况下就有可能发生死锁。
鉴于死锁难以完全避免,我们应当采取一系列策略来尽量减少其发生的风险。以下是五条实用的建议:
- 保持一致的访问顺序:在处理多个表或行时,确保所有事务以固定的顺序访问这些资源。例如,在批量更新作业的场景下,可以先对涉及的ID列表进行排序,然后依次执行更新操作,这样可以有效避免交叉等待锁的情况。同样地,调整不同事务中的SQL执行顺序,使其保持一致,也是预防死锁的有效手段。
- 拆分大事务:大事务更容易导致死锁,因为它们需要长时间占用大量资源。如果业务逻辑允许,应尽量将大事务拆分成多个小事务来执行,这样可以减少锁资源的占用时间,降低死锁的风险。
- 批量锁定资源:在同一个事务中,应尽可能一次性锁定所有需要的资源,而不是分步进行。这样可以减少因资源请求顺序不一致而导致的死锁情况。
- 调整隔离级别:如果业务场景允许,适当降低事务的隔离级别也是一个减少死锁的有效方法。比如,将隔离级别从可重复读(RR)调整为读已提交(RC),可以避免一些因间隙锁(gap lock)引起的死锁问题。
- 优化索引设计:合理的索引可以确保数据库查询和更新操作高效执行,减少对非目标行的锁定。尽量规避数据库扫描全表,并对每一行数据加锁,这将大大增加死锁的风险。因此,为表添加合适的索引是预防死锁的重要措施之一。
二 数据量大的那些难题
1.深分页,有妙招
大部分存储中间件都会存在深分页问题,以数据库为例,假设订单表里已经存了8000万条数据,我们想遍历整个订单表,对今年生产的订单进行归档。如何快速的低成本的实现这个功能呢?
起初,可以使用下面语句进行分页查询:
SELECT * FROM my_order WHERE create_time > '2023-01-01 00:00:00' LIMIT 100000, 20;
随着页面的加深,这条sql语句的执行时间将会越来越长。其原因是在使用普通索引查询时,要想获取其它列数据,mysql需要回表,因为普通索引上存储的是主键,通过主键获取行数据。
在查询0~20条的数据时,需要回表20次,查询100000~100020的数据需要回表100020次,性能就会变得极差。
那么该如何减少回表次数呢?
- 方案一
首先,利用普通索引快速定位到第100001条数据的ID(假定ID是递增的,且普通索引即建立在ID上),由于该索引直接存储了主键ID,因此此过程无需进行回表操作。
随后,基于获取到的这个主键ID,通过主键索引直接访问并检索紧接着的10条数据记录,因为主键索引的节点中直接包含了行数据,所以可以直接提取这些数据,从而提高了查询效率。
SELECT * FROM my_order WHERE id > (SELECT a.id FROM my_order a WHERE a.create_time > '2023-01-01 00:00:00' LIMIT 100000, 1) LIMIT 10;
- 方案二
如果表采用了数据库的自增主键,那么可以通过将当前页最后一条记录的主键ID作为查询下一页数据的起始ID参数,来高效地实现目标数据的快速定位。
这种方法利用了主键的自增特性,简化了查询过程,提高了数据检索的效率。
SELECT * FROM my_order WHERE id > lastReturnID LIMIT 10;
2. 序列化,能耗高
在日常开发过程中,下面几个操作会显著增加了系统资源的消耗,导致服务整体性能不稳定,需要认真考虑并采取相应措施来化解:
- 频繁使用Java反射操作处理大对象,尤其是在对象拷贝等场景中,这会显著增加CPU负担和内存使用。
- 数据对象频繁地进行JSON序列化与反序列化转换,如日志记录、DTO(数据传输对象)之间的转换等,这些操作同样会消耗大量CPU资源并影响系统响应速度。
- 日志打印过于频繁,尤其是在对请求入参和出参进行无差别记录时,不仅增加了磁盘I/O负担,还可能泄露敏感信息,影响系统安全性。
内存中持续存储大量的大对象,如文件和图片上传下载功能中暂存的数据,这些大对象会占用大量内存资源,影响系统的稳定性和扩展性。
三 总结
本文主要剖析了并行处理与大数据处理领域内影响系统性能的几个难题。
- 在并行处理架构中,不恰当的锁粒度设计往往引发资源激烈争用,加之请求叠加效应,导致系统负载成倍增加,显著加剧了系统压力。此外,@Transaction注解的局限性束缚了事务管理的灵活应变能力,而数据库死锁则作为并发控制领域的难题,阻碍着系统的高效运行。
- 大数据处理领域,深度分页查询的性能瓶颈日益显著,成为制约数据处理效率的关键因素。同时,在数据序列化过程中,潜在的资源消耗陷阱不容忽视,需采取有效措施以规避资源浪费,防止其成为系统性能提升的瓶颈。
最后,希望本文对读者有所启发和帮助。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。