一个简单的优化例子(锁问题)

本文分析了一项批量处理操作因锁竞争导致性能下降的问题。通过详细的SQL跟踪和会话级监控,发现长时间的网络延迟成为锁等待的主要原因。

前一阵刚给同事做完一个tuning的培训,顺便把以前做过的case重新整理了一下,今天发出来。

case的开始是客户开始抱怨一个批量处理的操作现在变得很慢,在开发的帮助下,我们可以在生产环境中轻易的测试这个操作,而且发现响应时间确实是比以前慢了。这一般是个好的开始,因为我们至少知道问题是什么。

首先,我们通过session级别的trace,发现这个批处理在执行的过程中存在最多的等待事件是enqueue:

PHP code:

OVERALLTOTALS
FORALLNON-RECURSIVESTATEMENTS



callcountcpuelapseddiskquerycurrentrows

-----------------------------------------------------------------------

Parse280.010.000000

Execute280.1019.76025712514

Fetch140.000.00056014

-----------------------------------------------------------------------

total700.1119.78031312528



Missesinlibrarycacheduringparse
:0



Elapsedtimes
includewaitingonfollowingevents:

EventwaitedonTimesMax.WaitTotalWaited

----------------------------------------Waited----------------------

SQL*Netmessagetoclient1840.000.00

SQL
*Netmessagefromclient18418.75220.84

enqueue142.6217.64

logfilesync140.020.08

SQL
*Netbreak/resettoclient140.292.03

bufferbusywaits40.020.03



而引起最多enqueue等待的SQL是
:



selectrowid,bsr.*

from

BATCH_SUB_REQUESTbsrwherebsr
.BSR_REQUEST_ID=:1andbsr.BSR_item=(select

bsr2
.BSR_ITEMfromBATCH_SUB_REQUESTbsr2/*+INDEX(bsr2

idx_batch_sub_request)*/
where(bsr2.BSR_STATUS='new')and

bsr2.BSR_request_id=bsr.BSR_request_idandrownum=1)forupdate





callcountcpuelapseddiskquerycurrentrows

-----------------------------------------------------------------------

Parse70.000.000000

Execute70.0617.700229410

Fetch70.000.0003507

-----------------------------------------------------------------------

total210.0617.710264417



Missesinlibrarycacheduringparse
:0

Optimizergoal
:CHOOSE

Parsinguserid
:54



RowsRowSourceOperation

----------------------------------------------------------

1FORUPDATE(cr=5r=0w=0time=947us)

3TABLEACCESSBYINDEXROWIDBATCH_SUB_REQUEST(cr=25r=0w=0time=3205us)

3INDEXRANGESCANPK_BATCH_SUB_REQUEST(cr=22r=0w=0time=3141us)(objectid40913)

2COUNTSTOPKEY(cr=10r=0w=0time=477us)

2TABLEACCESSBYINDEXROWIDBATCH_SUB_REQUEST(cr=10r=0w=0time=437us)

2INDEXRANGESCANIDX_BATCH_SUB_REQUEST(cr=6r=0w=0time=305us)(objectid43816)





Elapsedtimesincludewaitingonfollowingevents:

EventwaitedonTimesMax.WaitTotalWaited

----------------------------------------Waited----------------------

SQL*Netmessagetoclient420.000.00

SQL
*Netmessagefromclient420.3112.25

enqueue142.6217.64

bufferbusywaits40.020.03

********************************************************************************



在进行测试的时候,我通过v$session_wait检查了enqueue类型,是mode6的TX锁,也就是由dml操作引起的行级锁:

SQL
>SELECTchr(to_char(bitand(p1,-16777216))/16777215)||

chr(to_char(bitand(p1,16711680))/65535)"Lock",

to_char(bitand(p1,65535))"Mode"

FROMv$session_wait

WHEREevent
='enqueue'andsid=31;



LockMode

----------------------------------------------

TX6



所有的迹象都指向了一个SQL的lock问题,让我们看看这个SQL:

select rowid, bsr.*
from
BATCH_SUB_REQUEST bsr where bsr.BSR_REQUEST_ID=:1 and bsr.BSR_item=(select
bsr2.BSR_ITEM from BATCH_SUB_REQUEST bsr2 /*+ INDEX (bsr2
idx_batch_sub_request) */ where (bsr2.BSR_STATUS='new' )and
bsr2.BSR_request_id = bsr.BSR_request_idand rownum=1) for update

这是一个select ... for update的语句,目的是为了从batch_sub_request这个表里面选出一行,这里面的条件rownum=1引起了我的注意,因为这会使select锁定这张表的地一样,我知道系统上一般会有10个左右进程会一起运行这个同样的select,按照这种写法,同一时间只能有一个select ... for update可以成功,其他的进程必须等待,而等待事件就是enqueue。这显然是有问题的。

我就这个问题和开发组的同事交流了一下,开发的同事一方面同意这个SQL是有问题,但另一方面也向我提出了另一个费解的问题,同样的设计,为什么在测试环境下性能就是好的呢?
这确实是个很奇怪的问题,我们通过在测试环境下重做这个批处理的操作,并且trace下来,发现测试环境下虽然最大的等待事件还是enqueue,但每次等待的时间都是很短的,只需要零点几秒,而在生产环境中每次enqueue的等待时间是2秒以上。

我仔细的看了生产上的trace文件的每一个环节,发现select ... for update之后应该马上做一个update并且commit,从而释放锁。从这个角度来看,其他进程等待lock的时间应该取决于这个update的速度,但是当我兴高采烈的检查update的性能的时候,发现这个update只需要0.01秒都不到的时间。在10个左右的并发情况下,等待最长的那个进程也不应该等待超过0.1秒!
到底我们遗漏什么?最后的可能性是select ... for update和update之间有些东西被我们忽略了,带着这个问题我和开发再次对整个流程交流了一下,终于发现原来当我们做完select ... for update之后,系统还要做一件事情,那就是把select出来的结果发到前端服务器的java程序,前端的java程序要做一些处理。“但是,”开发说,“我们肯定前端的这个处理是很快的,绝不会超过0.1秒”。如果数据库和应用都很快,那慢的的东西就只能有一个了--------网络。

在我们的环境了,有9太前端服务器,3台在美国,3台在亚洲,3台在欧洲,为了提高这个批处理操作的性能,我们允许所有的9台前端服务器帮助处理一个批处理操作,这本来是个好的想法,但是当亚洲的前端成功的做完了select ... for update, 把结果发回给亚洲的前端,亚洲的前端处理好之后再返回下一条指令给数据库,这个过程中如果网络很慢的话可能需要5秒以上,要知道,在这5秒里,所有的其他8台前端的进程都在等待enqueue,这就是enqueue很大的原因!在我们的情况里,本来希望更多的server可以提高性能,但由于网络原因导致部分网络比较差的server堵塞了好的server。

之后我们单独用美国的前端,亚洲的前端,欧洲的前端做测试,发现只用美国前端的时候性能很好,而只用亚洲或欧洲的前端的时候性能都很差,因此问题就是慢的网络导致了大量的enqueue。

这个案例其实很简单,关键是:
1, 开始和开发沟通不充分,漏掉了很重要的环节,主要我自己没多少开发经验。
2, 我自己也有点思维定势,看到SQL*Net message from client很高也没在意,看到enqueue就一头扑上去了。其实如果我们在系统级别监控如果发现SQL*Net message from client很高,可能是很难判断到底应不应该忽略,但如果是在session级别监控到,就要具体分析了。
3,设计还是很重要,这里如果在设计阶段就把lock的影响考虑进去,也不会最后在生产上由于网络不好导致问题。

只用AM的前端监控到的性能:

PHP code:

selectrowid
,bsr.*

from

BATCH_SUB_REQUESTbsrwherebsr
.BSR_REQUEST_ID=:1andbsr.BSR_item=(select

bsr2
.BSR_ITEMfromBATCH_SUB_REQUESTbsr2/*+INDEX(bsr2

idx_batch_sub_request)*/
where(bsr2.BSR_STATUS='new')and

bsr2.BSR_request_id=bsr.BSR_request_idandrownum=1)forupdate





callcountcpuelapseddiskquerycurrentrows

-----------------------------------------------------------------------

Parse360.000.000000

Execute360.050.110521890

Fetch360.050.040180036

-----------------------------------------------------------------------

total1080.100.1607018936



Missesinlibrarycacheduringparse
:0

Optimizergoal
:CHOOSE

Parsinguserid
:54



RowsRowSourceOperation

----------------------------------------------------------

1FORUPDATE(cr=5r=0w=0time=798us)

2TABLEACCESSBYINDEXROWIDBATCH_SUB_REQUEST(cr=15r=0w=0time=1888us)

2INDEXRANGESCANPK_BATCH_SUB_REQUEST(cr=13r=0w=0time=1856us)(objectid40913)

1COUNTSTOPKEY(cr=5r=0w=0time=160us)

1TABLEACCESSBYINDEXROWIDBATCH_SUB_REQUEST(cr=5r=0w=0time=144us)

1INDEXRANGESCANIDX_BATCH_SUB_REQUEST(cr=4r=0w=0time=115us)(objectid43816)





Elapsedtimesincludewaitingonfollowingevents:

EventwaitedonTimesMax.WaitTotalWaited

----------------------------------------Waited----------------------

SQL*Netmessagefromclient2180.040.20

SQL
*Netmessagetoclient2170.000.00

enqueue80.000.04

********************************************************************************

<think>嗯,用户让我用Java写一个单例模式的例子。首先,我得回想一下单例模式的定义和用途。单例模式属于创建型设计模式,确保一个类只有一个实例,并提供全局访问点。这在需要控制资源或者共享资源时很有用,比如数据库连接池、日志对象等。 接下来,我需要考虑不同的实现方式。常见的单例实现有懒汉式、饿汉式、双重检查、静态内部类、枚举等。用户可能希望一个简单明了的例子,所以应该选择最典型的几种来说明。 首先是饿汉式,这个实现简单,类加载时就初始化实例,线程安全。不过可能会浪费资源,如果实例很大且不常用的话。然后是懒汉式,延迟加载,但需要考虑线程安全问题。早期的懒汉式不加同步的话,多线程环境下会出问题,所以需要加synchronized关键字,但这样会影响性能。 然后双重检查,结合了懒汉式的延迟加载和线程安全,通过两次检查实例是否存在,减少同步的开销。不过要注意volatile关键字的使用,防止指令重排序导致的问题。 静态内部类方式利用了类加载机制,静态内部类在第一次使用时才会加载,从而实现了延迟加载,并且由JVM保证线程安全。这种方式比较优雅,代码也简洁。 还有枚举方式,Effective Java推荐的方法,不仅能避免多线程问题,还能防止反序列化重新创建新对象。不过可能不太灵活,比如无法继承其他类。 用户可能需要一个具体的例子,我应该给出几种常见的实现,并说明优缺点,让用户根据场景选择。同时要提醒注意事项,比如线程安全、序列化问题、反射攻击等。 现在需要把这些整理成代码示例,用中文解释清楚每种实现的原理和适用情况。可能用户是刚开始学习设计模式,所以需要解释得详细一些,确保他们能理解不同实现方式的区别和适用场景。 还要注意代码格式,用户要求行内数学表达式用$...$,但这里主要是代码块,所以需要用正确的Java语法,可能用三个反引号包裹代码块。不过用户没有特别说明代码块的格式,但根据系统指令,可能需要使用$$...$$,但代码块通常用反引号。不过原例子里用户用了三个反引号,可能应该遵循这一点。不过原例子里用户给的是系统指令中的例子,所以可能用户希望用标准的代码块格式。 最后,要确保回答结构清晰,分点列出不同的实现方式,每种方式给出代码示例、优缺点和适用场景。这样用户能一目了然,选择适合自己需求的方式。</think>好的!单例模式是设计模式中最基础的模式之一,主要作用是保证一个类仅有一个实例,并提供全局访问点。以下是几种常见的Java实现方式及详细说明: --- ### 一、饿汉式(线程安全) ```java public class Singleton { // 类加载时直接初始化实例 private static final Singleton INSTANCE = new Singleton(); // 私有化构造方法 private Singleton() {} // 提供全局访问点 public static Singleton getInstance() { return INSTANCE; } } ``` **特点** - **线程安全**:实例在类加载时创建,由JVM保证线程安全 - **缺点**:可能浪费资源(即使未使用也会创建实例) - **适用场景**:实例较小且一定会被使用的情况 --- ### 二、懒汉式(线程不安全→改进为线程安全) ```java // 基础版(线程不安全) 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 Singleton instance; private Singleton() {} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } ``` **特点** - **延迟加载**:首次调用`getInstance()`时创建实例 - **缺点**:同步影响性能 - **适用场景**:单线程环境或对性能要求不高的情况 --- ### 三、双重检查(DCL,线程安全) ```java public class Singleton { // volatile禁止指令重排序 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; } } ``` **特点** - **延迟加载 + 高效线程安全**:通过两次判空和`volatile`关键字优化性能 - **适用场景**:高并发环境 --- ### 四、静态内部类(线程安全) ```java public class Singleton { private Singleton() {} // 静态内部类在第一次使用时加载 private static class Holder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; // 触发类加载并初始化实例 } } ``` **特点** - **天然线程安全**:由JVM保证类加载过程的线程安全 - **延迟加载**:内部类在调用`getInstance()`时加载 - **推荐方式**:简洁高效,无性能优 --- ### 五、枚举实现(最简洁的线程安全方式) ```java public enum Singleton { INSTANCE; // 枚举实例天然单例 public void doSomething() { // 功能方法 } } ``` **特点** - **绝对单例**:防止反射攻击和序列化破坏 - **代码极简**:直接通过`Singleton.INSTANCE`调用 - **推荐方式**:符合《Effective Java》建议 --- ### 关键注意事项 1. **反射攻击**:除枚举方式外,其他实现可能通过反射破坏单例(可通过构造方法中抛出异常防御) 2. **序列化安全**:若需序列化,应实现`readResolve()`方法返回已有实例 3. **多类加载器**:不同类加载器可能加载多个实例(需统一类加载器环境) 实际开发中推荐优先选择 **静态内部类** 或 **枚举** 实现方式。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值