`
younglibin
  • 浏览: 1207626 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

多线程并发开卡卡号重复问题

阅读更多

<div class="iteye-blog-content-contain" style="font-size: 14px;">
<p class="MsoListParagraph" style="">&lt;!--[if !supportLists]--&gt;<span style="font-family: Wingdings;" lang="EN-US">Ø<span style="font-size: 7pt; line-height: normal; font-family: 'Times New Roman';">  </span></span>&lt;!--[endif]--&gt;<strong><span style="font-family: 宋体;">问题背景</span></strong></p>
<p class="MsoNormal"><span style="font-family: 宋体;">之前写过一个单线程程序, 从数据库查询卡号的, 今天要做批量导入, 发现单线程太慢, 于是修改为多线程方式, 只是在前段开了多线程,以为就OK了, 谁知问题这时就出现了, 由于有实务问题, 事物是在县城内开启的, 这样就导致了每一个线程就是一个事物, 就导致事物之内更新的数据在其他线程读不到。</span></p>
<p class="MsoNormal"> </p>
<p class="MsoNormal"><span lang="EN-US"> </span></p>
<div class="quote_title">多线程开启 写道</div>
<div class="quote_div">/**<br> * 异步多线程导入<br> */<br> public void batchRelateAllExternalCardThread(<br> List&lt;ExternalCard&gt; externalCardList, int type)<br> throws InterruptedException {<br> long startTime = System.currentTimeMillis();<br> ConcurrentLinkedQueue&lt;ExternalCard&gt; clQueue = new ConcurrentLinkedQueue&lt;ExternalCard&gt;(<br> externalCardList);<br> ExecutorService es = Executors.newFixedThreadPool(THREAD_NUM);<br> for (int i = 0; i &lt; THREAD_NUM; i++) {<br> es.execute(new RelateCardThread(clQueue, type));<br> }<br> boolean next = true;// 主线程退出标记<br> while (next) {<br> if (THREAD_NUM == threadNum.get()) {<br> es.shutdown(); // 这里是让 线程池推出使用的 ,但是主线程并没有退出<br> if (es.isShutdown()) {<br> next = false;<br> }<br> } else {<br> Thread.sleep(5000);<br> }<br> }<br> }</div>
<p class="MsoNormal"><span lang="EN-US"> </span></p>
<div class="quote_title">线程执行 写道</div>
<div class="quote_div">class RelateCardThread implements Runnable {<br> private ConcurrentLinkedQueue&lt;ExternalCard&gt; clQueue;<br> private int type;<br><br> public RelateCardThread(ConcurrentLinkedQueue&lt;ExternalCard&gt; clQueue,<br> int type) {<br> this.clQueue = clQueue;<br> this.type = type;<br> }<br><br> public void run() {<br> int count = 0;<br> while (!clQueue.isEmpty()) {<br> logger.info("============================================="<br> + Thread.currentThread().getId() + "执行:" + count);<br> ExternalCard ec = clQueue.poll();<br> if (null != ec) {<br> relateExternalCard(ec, type);<br> }<br> count++;<br> }<br> threadNum.getAndIncrement();<br> }<br> }</div>
<p class="MsoNormal"><span lang="EN-US"> </span></p>
<div class="quote_title">写道</div>
<div class="quote_div">private void relateExternalCard(ExternalCard ec, int type) {<br> resultInfo = cardCtlService.activeOtherCardAndRelateCard(<br> mbTemp, merchant, ec, ec.getCardType(), null,<br> WebCommonContents.ExternalCardRelateSource.CRM<br> .getCode());<br> }}}</div>
<p class="MsoNormal"><span lang="EN-US"> </span></p>
<pre name="code" class="java">@Transactional(value = "trade", readOnly = false, propagation = Propagation.REQUIRED)
@Override
public ResultInfo activeOtherCardAndRelateCard(){

//在这个方法中需要同步获取这个对象, 之前我们采用的 synchronized 的
      cardRecord = cardRecordBizService.selectCardRecord(cardRecord);

}</pre>
<p class="MsoNormal"><span lang="EN-US"> </span></p>
<p class="MsoNormal"> </p>
<p class="MsoNormal"> </p>
<p class="MsoNormal"><span lang="EN-US"> </span></p>
<p class="MsoNormal"><span style="font-family: 宋体;">通过上面伪代码,可以看出整个代码逻辑:</span></p>
<p class="MsoListParagraph" style="">&lt;!--[if !supportLists]--&gt;<span lang="EN-US">1.<span style="font-size: 7pt; line-height: normal; font-family: 'Times New Roman';">       </span></span>&lt;!--[endif]--&gt;<span style="font-family: 宋体;">并行计算每个线程对部分卡进行开卡、激活。</span></p>
<p class="MsoListParagraph" style="">&lt;!--[if !supportLists]--&gt;<span lang="EN-US">2.<span style="font-size: 7pt; line-height: normal; font-family: 'Times New Roman';">       </span></span><span style="font-family: monospace; font-size: 1em; line-height: 1.5; background-color: #fafafa;">selectCardRecord</span><span style="">方法加了线程同步锁处理,意图解决卡号重复问题。</span></p>
<p class="MsoListParagraph" style="">&lt;!--[if !supportLists]--&gt;<span lang="EN-US">3.<span style="font-size: 7pt; line-height: normal; font-family: 'Times New Roman';">       </span></span>&lt;!--[endif]--&gt;<span style="font-family: 宋体;">事务</span><span lang="EN-US">Transaction</span><span style="font-family: 宋体;">在线程内,因此每个线程开启一个事务,导入数据不在一个大事务中,效率较高也保证批量多个卡激活互不影响,每个卡激活一个独立事务处理。</span></p>
<p class="MsoNormal"><span lang="EN-US"> </span></p>
<p class="MsoNormal"><span lang="EN-US"> </span></p>
<p class="MsoListParagraph" style="">&lt;!--[if !supportLists]--&gt;<span style="font-family: Wingdings;" lang="EN-US">Ø<span style="font-size: 7pt; line-height: normal; font-family: 'Times New Roman';">  </span></span>&lt;!--[endif]--&gt;<strong><span style="font-family: 宋体;">问题分析</span></strong></p>
<p class="MsoNormal"><span style="font-family: 宋体;">开发对</span><span style="font-family: monospace; font-size: 1em; line-height: 1.5; background-color: #fafafa;">selectCardRecord</span><span style="font-size: 12px; line-height: 1.5; font-family: 宋体;">加了线程同步锁,但仍然出现卡号重复。实际上,再仔细查看代码逻辑,会发现,<span style="background: yellow;">卡号流水并不通过代码维护,而是通过数据库的激活标识</span></span><span style="font-size: 12px; line-height: 1.5; background: yellow;" lang="EN-US">flag</span><span style="font-size: 12px; line-height: 1.5; font-family: 宋体; background: yellow;">维护</span><span style="font-size: 12px; line-height: 1.5; font-family: 宋体;">。每次</span><span style="font-size: 12px; line-height: 1.5;" lang="EN-US">SQL</span><span style="font-size: 12px; line-height: 1.5; font-family: 宋体;">取出一条没有激活的卡号,然后设置为激活。</span></p>
<p class="MsoNormal"><span lang="EN-US"> </span></p>
<p class="MsoNormal"><span style="font-family: 宋体;">问题出在并发和事务的关系上,虽然代码试图线程同步,但数据库事务提交先后顺序并不受代码控制。</span></p>
<p class="MsoNormal"><br><img src="http://dl2.iteye.com/upload/attachment/0101/9683/c54ef4e4-8298-3824-9469-022c91f0df6f.png" alt=""><br> <br>&lt;!--[endif]--&gt;&lt;!--[endif]--&gt;</p>
<p class="MsoListParagraph" style="">&lt;!--[if !supportLists]--&gt;<span lang="EN-US">1.<span style="font-size: 7pt; line-height: normal; font-family: 'Times New Roman';">       </span></span>&lt;!--[endif]--&gt;<span lang="EN-US">Time1</span><span style="font-family: 宋体;">时刻,线程</span><span lang="EN-US">1</span><span style="font-family: 宋体;">开启事务</span><span lang="EN-US">T1</span><span style="font-family: 宋体;">,获取第一个可用的卡号</span><span lang="EN-US">ID=1</span></p>
<p class="MsoListParagraph" style="">&lt;!--[if !supportLists]--&gt;<span lang="EN-US">2.<span style="font-size: 7pt; line-height: normal; font-family: 'Times New Roman';">       </span></span>&lt;!--[endif]--&gt;<span lang="EN-US">Time1’</span><span style="font-family: 宋体;">时刻,线程</span><span lang="EN-US">2</span><span style="font-family: 宋体;">开启事务</span><span lang="EN-US">T2</span><span style="font-family: 宋体;">,由于</span><span lang="EN-US">T1</span><span style="font-family: 宋体;">未提交,所以仍然获取可用的卡号</span><span lang="EN-US">ID=1</span></p>
<p class="MsoListParagraph" style="">&lt;!--[if !supportLists]--&gt;<span lang="EN-US">3.<span style="font-size: 7pt; line-height: normal; font-family: 'Times New Roman';">       </span></span>&lt;!--[endif]--&gt;<span lang="EN-US">Time2</span><span style="font-family: 宋体;">时刻,</span><span lang="EN-US">T1</span><span style="font-family: 宋体;">提交,将标识更改为</span><span lang="EN-US">true</span></p>
<p class="MsoListParagraph" style="">&lt;!--[if !supportLists]--&gt;<span lang="EN-US">4.<span style="font-size: 7pt; line-height: normal; font-family: 'Times New Roman';">       </span></span>&lt;!--[endif]--&gt;<span lang="EN-US">Time2’</span><span style="font-family: 宋体;">时刻,</span><span lang="EN-US">T2</span><span style="font-family: 宋体;">提交,再次更改标识为</span><span lang="EN-US">true</span></p>
<p class="MsoNormal"><span lang="EN-US"> </span></p>
<p class="MsoNormal"><span lang="EN-US">T1</span><span style="font-family: 宋体;">,</span><span lang="EN-US">T2</span><span style="font-family: 宋体;">返回了相同的</span><span lang="EN-US">ID=1</span><span style="font-family: 宋体;">的卡号,导致卡号重复。这是典型的脏读问题。</span></p>
<p class="MsoNormal"><span lang="EN-US"> </span></p>
<p class="MsoNormal"> </p>
<p class="MsoNormal"><span lang="EN-US">尝试解决方案:</span></p>
<p class="MsoNormal"><span lang="EN-US">最开始 想到了使用脏的的问题, 于是我们叫事物级别修改为:isolation = Isolation.READ_UNCOMMITTED</span></p>
<p class="MsoNormal"><span lang="EN-US">这是虽然读到的卡号不重复了, 但是,我么在更新卡信息的时候,考虑到是藏读引起的, 么有起到隔离作用, 这是就在想到行锁, 使用行锁后, 发现解决了这个问题, 且由于所范围的缩小,加快了多线程的效率;</span></p>
<p class="MsoNormal"><span lang="EN-US"> </span></p>
<p class="MsoListParagraph" style="">&lt;!--[if !supportLists]--&gt;<span style="font-family: Wingdings;" lang="EN-US">Ø<span style="font-size: 7pt; line-height: normal; font-family: 'Times New Roman';">  </span></span>&lt;!--[endif]--&gt;<strong><span style="font-family: 宋体;">解决方案</span></strong></p>
<p class="MsoNormal"><span style="font-family: 宋体;">分析出原因后,有多种可行的改法。</span><span lang="EN-US">Postgres</span><span style="font-family: 宋体;">默认隔离级别是读提交</span><span lang="EN-US">read committed</span><span style="font-family: 宋体;">,在代码改动较小的前提下,我们可以选择对读数据加行锁,阻塞其他读直到事务提交。</span></p>
<p class="MsoNormal"><span style="font-family: 宋体;">数据库的锁从粒度上简单说有两种</span><span lang="EN-US">(</span><span style="font-family: 宋体;">实际比这个要多,只说最简单情况</span><span lang="EN-US">)</span><span style="font-family: 宋体;">:</span></p>
<p class="MsoListParagraph" style="">&lt;!--[if !supportLists]--&gt;<span lang="EN-US">1.<span style="font-size: 7pt; line-height: normal; font-family: 'Times New Roman';">       </span></span>&lt;!--[endif]--&gt;<span style="font-family: 宋体;">表级锁</span></p>
<p class="MsoListParagraph" style="">&lt;!--[if !supportLists]--&gt;<span lang="EN-US">2.<span style="font-size: 7pt; line-height: normal; font-family: 'Times New Roman';">       </span></span>&lt;!--[endif]--&gt;<span style="font-family: 宋体;">行级锁</span></p>
<p class="MsoNormal"><span lang="EN-US"> </span></p>
<p class="MsoNormal"><span style="font-family: 宋体;">从锁行为上分:</span></p>
<p class="MsoListParagraph" style="">&lt;!--[if !supportLists]--&gt;<span lang="EN-US">1.<span style="font-size: 7pt; line-height: normal; font-family: 'Times New Roman';">       </span></span>&lt;!--[endif]--&gt;<span style="font-family: 宋体;">共享锁</span></p>
<p class="MsoListParagraph" style="">&lt;!--[if !supportLists]--&gt;<span lang="EN-US">2.<span style="font-size: 7pt; line-height: normal; font-family: 'Times New Roman';">       </span></span>&lt;!--[endif]--&gt;<span style="font-family: 宋体;">排它锁</span></p>
<p class="MsoNormal"><span lang="EN-US"> </span></p>
<p class="MsoNormal"><span style="font-family: 宋体;">我们的需求是对数据的读取加锁,阻塞其他事务读取相同行,不同行的数据不阻塞。根据这个需求,代码最小改动的方案如下:</span></p>
<p class="MsoNormal"><span lang="EN-US">  String id = executeQuery(‘select id from trade.card_record where batch=xxx and flag=false limit 1 <span style="background: yellow;">for update</span>’);</span></p>
<p class="MsoNormal"><span lang="EN-US"> </span></p>
<p class="MsoNormal"><span style="font-family: 宋体; background: yellow;">对</span><span style="background: yellow;" lang="EN-US">select</span><span style="font-family: 宋体; background: yellow;">读加行级排它锁,这样就达到了我们的目的。</span> <span style="font-family: 宋体;">原有代码中的</span><span lang="EN-US">synchronized</span><span style="font-family: 宋体;">没有任何作用,去掉即可。</span></p>
<p class="MsoNormal"><span lang="EN-US"> </span></p>
<p class="MsoListParagraph" style="">&lt;!--[if !supportLists]--&gt;<span style="font-family: Wingdings;" lang="EN-US">Ø<span style="font-size: 7pt; line-height: normal; font-family: 'Times New Roman';">  </span></span>&lt;!--[endif]--&gt;<strong><span style="font-family: 宋体;">测试结果</span></strong></p>
<p class="MsoNormal"><span style="font-family: 宋体;">并发冲突问题验证解决,改为并行计算后,</span><span lang="EN-US">40w</span><span style="font-family: 宋体;">数据导入测试结果预计</span><span lang="EN-US">2</span><span style="font-family: 宋体;">小时,提高</span><span lang="EN-US">4</span><span style="font-family: 宋体;">倍。根据硬件和线程数的不同,效率会有所波动。</span></p>
<p class="MsoNormal"><span lang="EN-US"> </span></p>
<p class="MsoListParagraph" style="">&lt;!--[if !supportLists]--&gt;<span style="font-family: Wingdings;" lang="EN-US">Ø<span style="font-size: 7pt; line-height: normal; font-family: 'Times New Roman';">  </span></span>&lt;!--[endif]--&gt;<strong><span style="font-family: 宋体;">案例总结</span></strong></p>
<p class="MsoNormal"><span style="font-family: 宋体;">这个</span><span lang="EN-US">bug</span><span style="font-family: 宋体;">实际上是未考虑到并发情况而产生的,一直埋藏在软</span><span lang="EN-US">pos</span><span style="font-family: 宋体;">的代码中多年。并行计算加重了并发的程度,才会很容易暴露这个</span><span lang="EN-US">bug</span><span style="font-family: 宋体;">。</span></p>
<p> </p>
<p class="MsoNormal"><span style="font-family: 宋体;">在程序代码设计中,对于系统中“唯一流水号”这样的情况,我们要特别重视并发的访问,同时要考虑事务、线程之间的数据一致性。希望通过这个案例,让大家认识到事务的概念。</span></p>
</div>

 

  • 大小: 7.6 KB
分享到:
评论

相关推荐

    模拟ATM功能编写的

    在这个项目中,我们模拟了一个简单的ATM程序,虽然功能并不全面,但足以让我们理解多线程和数据库在实际应用中的作用。 首先,我们要讨论的是多线程技术。在ATM程序中,多线程是非常关键的,因为一个ATM可能同时...

    2022卡卡百度指数批量采集软件.zip

    软件预览图:https://img-blog.csdnimg.cn/d8dd76ab6bbe438cb115273f4fa25689.png 多线程并发日采集几十万(去重后)业内领先。 永久更新维护!

    2021卡卡百度关键词指数在线批量查询.zip

    软件预览图:https://img-blog.csdnimg.cn/d8dd76ab6bbe438cb115273f4fa25689.png 多线程并发日采集几十万(去重后)业内领先。 永久更新维护!

    2021卡卡百度关键词搜索指数批量查询软件.zip

    软件预览图:https://img-blog.csdnimg.cn/d8dd76ab6bbe438cb115273f4fa25689.png 多线程并发日采集几十万(去重后)业内领先。 永久更新维护!

    多线程

    在计算机科学中,多线程是一种程序设计模型,允许单个应用程序同时执行多个并发线程,从而提高了系统资源的利用率和程序的响应速度。在Java编程语言中,多线程支持是其核心特性之一,使得Java开发者能够创建高效、...

    java开放性实验ATM柜员机

    6. **线程安全**:ATM柜员机系统可能会有多用户同时操作,这就需要考虑并发问题。Java提供了synchronized关键字和其他并发控制机制来保证线程安全,防止数据竞争和不一致。 7. **用户界面设计**:尽管题目没有明确...

    Windows网络编程技术

    多线程、异步I/O(如WSAAsyncSelect或WSAEventSelect)和IOCP(I/O完成端口)是常见的并发处理模型。 1. 多线程:每个连接由一个单独的线程处理,但过多的线程会导致资源消耗过大。 2. 异步I/O:通过事件回调实现...

    java-tutorial:Java实践代码,多线程,数据结构,算法,设计模式,Spring,RabbitMQ,RocketMQ

    Java实践basic-cornerstone:Java基础(Java8,Java11),包含并发编程,同步和锁,线程线程池,泛型,数据结构,算法... 设计:Java实现设计模式basic:Java基础代码实践leet:Leetcode刷题nettt:netty库实践...

    tomcat7 tomcat 6 xshell6.zip

    1. **多线程上下文类加载器**:这使得类加载更加灵活,有助于避免类加载冲突。 2. **NIO.2支持**:除了原有的BIO(阻塞I/O)和NIO(非阻塞I/O),Tomcat 7增加了对Java NIO.2 API的支持,提供了更好的性能和可扩展性...

    tomcat apr模式所需资源包

    APR模式利用操作系统底层的I/O功能,如TCP/IP套接字和多线程,从而提供了比默认的Java NIO或BIO模型更高的效率。 首先,我们需要理解什么是APR。APR是Apache HTTP服务器项目的一部分,它提供了一个跨平台的API,...

    Linux进程编程c/s

    生产者-消费者问题是多线程或多进程同步问题的一个实例,它展示了如何通过共享内存和信号量机制来协调不同进程之间的数据交换。 在生产者-消费者模型中,通常有两类进程:生产者和消费者。生产者负责生成数据,而...

    web_crawler

    5. **多线程与并发**:为了提高爬取效率,通常会采用多线程或异步I/O的方式来并行处理多个网页。Java提供了Thread类和ExecutorService接口,可以方便地实现多线程编程。 6. **URL管理**:防止重复爬取和无限循环是...

    (OPC 源程序示例).rar

    8. 多线程编程:由于OPC应用通常需要处理并发请求,多线程编程技术会贯穿其中。开发者可以从源码中学习如何在OPC上下文中实现线程安全的数据访问。 9. 编程语言和库:OPC示例可能使用VB.NET、C#、C++或其他支持COM...

    SE_Final:软件工程课程的最终项目

    在多线程或并发环境下,当多个线程访问共享资源且顺序不确定时,可能会出现竞态条件。这种情况下,程序的行为可能不一致,导致数据错误或系统崩溃。在服务器与数据库通信中,如果未正确处理竞态条件,可能会出现数据...

    客户端和服务器通信C语言源代码

    实践中,你可能会遇到并发连接处理、多线程、异步IO等问题,这些都是更高级的主题,但基础的客户端-服务器通信是学习这些高级技术的前提。希望这个详细的解析能帮助你更好地理解C语言实现的客户端和服务器通信。

    intel 志强e5 v3系列资料

    Intel Xeon E5 v3产品家族包含了多种型号,如E5-2600 v3和E5-4600 v3等,它们基于Haswell微架构,采用22纳米工艺制造,提供多核心多线程处理能力,旨在满足高负载、高性能计算的需求。这些处理器通常具有以下关键...

    eventSelect模型服务端设计

    这样,服务器就能高效地处理多个并发连接,而无需为每个连接创建单独的线程或进程。 `eventSelect`模型的优点在于其轻量级和高效性,特别是在连接数量相对较少,但并发处理需求较高的场景。然而,当监控的套接字...

    咖啡时间(休闲时光

    10. **多线程**:Java内置了对多线程的支持,通过Thread类或Runnable接口可以创建并运行多个线程,提高程序的并发性能。 在“咖啡时间”,无论是初学者还是经验丰富的开发者,都可以通过阅读Java相关的书籍、文档,...

    TurnAttention:这是一个基于WebSocket和Spring Framework技术的浏览器游戏

    此外,游戏逻辑的实现可能涉及到多线程和并发控制。由于多个玩家可能同时进行操作,因此需要有效地管理这些并发请求,以避免数据冲突和同步问题。Spring框架提供的线程池和并发工具类可以帮助我们优雅地解决这些问题...

    JavaSE基础实例

    8. **多线程**:Java提供了内置支持多线程的能力,通过`Thread`类或实现`Runnable`接口可以创建线程。了解同步机制(如`synchronized`关键字、`wait()`、`notify()`)和线程通信(如`join()`、`InterruptedException...

Global site tag (gtag.js) - Google Analytics