Posted in Working Skills, 技术

JavaEE 并发:一、FOR UPDATE 实战,监测并解决。

synchronized

 

Writer:BYSocket(泥沙砖瓦浆木匠)

微博:BYSocket

豆瓣:BYSocket

 

一、前言

针对并发,老生常谈了。目前一个通用的做法有两种:锁机制:1.悲观锁;2.乐观锁。

但是这篇我主要用于记录我这次处理的经历,另外希望能看的大神,大牛,技师者,学长,兄长,大哥们能在评论中发表自己的看法和解决技巧等。

 

二、故事是这样的

一个表,暂且叫 wallet,其中3个字段是 金额。初始值为0,如下图所示:
image

 

然后我们写了一个极为简单的Controller,并写了下面的Service代码:

@Override
	public void testLock(int lockId)
	{
		Wallet wallet = walletMapper.selectByPrimaryKey(4);
		
		BigDecimal one = new BigDecimal(1.00);
		BigDecimal two = new BigDecimal(2.00);
		BigDecimal three = new BigDecimal(3.00);
		
		wallet.setWalletAmount(wallet.getWalletAmount().add(one));
		wallet.setWalletAvailableAmount(wallet.getWalletAvailableAmount().subtract(two));
		wallet.setOldAmount(wallet.getOldAmount().add(three));		
		
		walletMapper.updateByPrimaryKeySelective(wallet);
	}

就简单的通过主键读取到一个对象,注意这个对象是没加锁的。也就是说,所对应的SQL如下:

SELECT 
    <include refid="Base_Column_List" />
    FROM wallet
    WHERE wallet_id = #{walletId,jdbcType=INTEGER}

我这边是MyBiatis,大家应该看得懂的。然后一个增加1 一个减少2 一个增加 3。

 

三、测试是这样

我用了Web应用压力测试工具:Boomhttps://github.com/rakyll/boom Go编写的HTTP(S)负载生成器,ApacheBench(AB)的替代工具。Boom是一个微型程序,能够对Web应用程序进行负载测试。它类似于 Apache Bench ,但在不同的平台上有更好的可用性,安装使用也比较简单。

简单使用方式如下:

boom -n 1000 -c 200 http://www.baidu.com

Options:
  -n  Number of requests to run.
  -c  Number of requests to run concurrently. Total number of requests cannot
      be smaller than the concurency level.
  -q  Rate limit, in seconds (QPS).
  -o  Output type. If none provided, a summary is printed.
      "csv" is the only supported alternative. Dumps the response
      metrics in comma-seperated values format.
 
  -m  HTTP method, one of GET, POST, PUT, DELETE, HEAD, OPTIONS.
  -h  Custom HTTP headers, name1:value1;name2:value2.
  -d  HTTP request body.
  -T  Content-type, defaults to "text/html".
  -a  Basic authentication, username:password.
 
  -allow-insecure Allow bad/expired TLS/SSL certificates.

所以我就如图进行压力测试,可见这个小工具还挺美的,这里我连接数1000,并发数100
image
可见后台程序报错了。什么错误呢?

Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction

原来并发导致update死表了。数据库的数据不用看了肯定是错误的。

 

四、FOR UPDATE的使用

先补一下其知识:利用select * for update 可以锁表/锁行。自然锁表的压力远大于锁行。所以我们采用锁行。什么时候锁表呢?

假设有个表单products ,里面有id跟name二个栏位,id是主键。
例1: (明确指定主键,并且有此笔资料,row lock)
SELECT * FROM wallet WHERE id=’3′ FOR UPDATE;
例2: (明确指定主键,若查无此笔资料,无lock)
SELECT * FROM wallet WHERE id=’-1′ FOR UPDATE;
例2: (无主键,table lock)
SELECT * FROM wallet WHERE name=’Mouse’ FOR UPDATE;
例3: (主键不明确,table lock)
SELECT * FROM wallet WHERE id<>’3′ FOR UPDATE;
例4: (主键不明确,table lock)
SELECT * FROM wallet WHERE id LIKE ‘3’ FOR UPDATE;

 

因此我们更新了下Service层的Mapper方法:

@Override
	public void testLock(int lockId)
	{
		Wallet wallet = walletMapper.selectForUpdate(4);
		
		BigDecimal one = new BigDecimal(1.00);
		BigDecimal two = new BigDecimal(2.00);
		BigDecimal three = new BigDecimal(3.00);
		
		wallet.setWalletAmount(wallet.getWalletAmount().add(one));
		wallet.setWalletAvailableAmount(wallet.getWalletAvailableAmount().subtract(two));
		wallet.setOldAmount(wallet.getOldAmount().add(three));		
		
		walletMapper.updateByPrimaryKeySelective(wallet);
	}

所对应的SQL如下:

  <select id="selectForUpdate" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
    SELECT 
    <include refid="Base_Column_List" />
    FROM wallet
    WHERE wallet_id = #{walletId,jdbcType=INTEGER}
    FOR UPDATE
  </select>

自然大家可以看到,我这边加了锁,是通过主键锁行。

 

按着上面的测试连接数1000,并发数100,控制台没报错。

image

数据库结果也是很不错。

image

 

五、加大压力

按着上面的测试连接数5000,并发数350,控制台还是没报错。

image

数据库结果却是很出错了!!!
image

少update了很多值。为什么呢?

 

六、jvisualvm 小工具检测,发现Tomcat线程连接数默认不够

然后我用jvisualvm 小工具检测。多测了几次,发现连接数5000,并发数350,并发数上升。有一个图的值始终不变。如图:

QQ截图20150322124739

发现图中 tomcat的守护线程一直在200左右。后来我去找了下tomcat的server.xml发现了,使用了默认,大概就是200左右。

 

所以就配置了一下,大致配置方法有两种如下:

第1种方式:配置Connector
maxThreads:tomcat可用于请求处理的最大线程数
minSpareThreads:tomcat初始线程数,即最小空闲线程数
maxSpareThreads:tomcat最大空闲线程数,超过的会被关闭
acceptCount:当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理

<Connectorport="8080"maxHttpHeaderSize="8192"maxThreads="150"minSpareThreads="25"maxSpareThreads="75"enableLookups="false"redirectPort="8443"acceptCount="100"connectionTimeout="20000"disableUploadTimeout="true"/>

 

第2种方式:配置Executor和Connector

name:线程池的名字
class:线程池的类名
namePrefix:线程池中线程的命名前缀
maxThreads:线程池的最大线程数
minSpareThreads:线程池的最小空闲线程数
maxIdleTime:超过最小空闲线程数时,多的线程会等待这个时间长度,然后关闭
threadPriority:线程优先级

<Executorname="tomcatThreadPool"namePrefix="req-exec-"maxThreads="1000"minSpareThreads="50"maxIdleTime="60000"/>

<Connectorport="8080"protocol="HTTP/1.1"executor="tomcatThreadPool"/>

 

maxThreads:线程池的最大线程数,直接配置1000,然后用连接数10000,并发数800测试。轻松见图:

UFRJFLLO6@F1)LWQ6EQJ8P8

image

七、总结

感谢帮助我的人。希望有大牛在此讨论相关。小生感激不尽。



8 thoughts on “JavaEE 并发:一、FOR UPDATE 实战,监测并解决。

  1. 今天偶然看 《High Performance Mysql》里有一段谈到这个,想起豪久前看过楼主你这篇文章。场景是介绍用计数表来汇总点击(访问)数,不用扫描明细表计算,提高查询速度表结构CREATE TABLE hit_counter ( cnt int unsigned not null) ENGINE=InnoDB;语句:UPDATE hit_counter SET cnt = cnt + 1;原文语句 The problem is that this single row is effectively a global “mutex” for any transactionthat updates the counter. It will serialize those transactions说是在数据库层面保障了事务安全,我搞了测试工具试了下确实是没问题的我仔细想了下对于比较简单的业务这么搞是最简单应该也是速度最快的,但复杂的业务如果不用存储过程,还是得在程序的层面去处理保障事务安全资料出处 《High Performance Mysql》第三版 第四章 Optimizing Schema and Data Types ——Cache And Summary Tables ———Counter Tables

  2. 楼主这篇博文对我这种6月份刚毕业的小菜鸟真是帮助很大以前虽然知道有类似Boom之类的压力测试工具但自己从来没有使用过而像jvisualvm以前也是听过但没有用过for update自己平时用plsql手动操作数据时会用到,并不知道它有个锁的功能!顺便又把行锁表锁mysql引擎给回顾了一下像您的这篇文章又是实际项目问题又结合了各种工具的使用实践性很强我是亲手做了一遍同时也自己着重拓展了解了for update在mysql及oracle下的差异和tomcat的配置 感觉受益良多这个问题业余时间我会持续关注,包括和其他有经验的项目组成员沟通交流下,看他们是从哪个层面解决问题的,也会上网搜一下其他人的解决方案,要是有新的发现一定会po上来和您交流下另外想问下你关于jvisualvm这个工具的学习和使用方面有些什么好的入门级资料推荐(中英文都可以),因为现在对我来说没使用经验直接看oracle官网的guides还是有些吃力再次感谢!!

发表评论

电子邮件地址不会被公开。 必填项已用*标注