• JavaWeb 并发:FOR UPDATE 实战,监测并解决。


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

    微博:BYSocket

    豆瓣:BYSocket

     

    一、前言

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

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

     

    二、故事是这样的

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

     

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @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如下:

    1
    2
    3
    4
    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 ,但在不同的平台上有更好的可用性,安装使用也比较简单。

    简单使用方式如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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
    可见后台程序报错了。什么错误呢?

    1
    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方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @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如下:

    1
    2
    3
    4
    5
    6
    7
    <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:当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理

    1
    <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:线程优先级

    1
    2
    3
    <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

    七、总结

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

  • 相关阅读:
    oracle第四天笔记
    oracle第三天笔记
    oracle第二天笔记
    select min from 连接
    decode 函数用法
    服务器
    婚姻
    黑马2018年JavaEE课程大纲
    Kubernetes本地私有仓库配置
    ELK系统分析nginx日志
  • 原文地址:https://www.cnblogs.com/jpfss/p/9364945.html
Copyright © 2020-2023  润新知