要我说,多线程事务它必须就是个伪命题!(上)

简介: 要我说,多线程事务它必须就是个伪命题!(上)

别问,问就是不行


分布式事务你应该是知道的。但是这个多线程事务......


没事,我慢慢给你说。


image.png


如图所示,有个小伙伴想要实现多线程事务。


这个需求其实我在不同的地方看到过很多次,所以我才说:这个问题又出现了。


那么有解决方案吗?


在此之前,我的回答都是非常的肯定:毋庸置疑,肯定是没有的。


image.png


为什么呢?


我们先从理论上去推理一下。


来,首先我问你,事务的特性是什么?


这个不难吧?八股文必背内容之一,ACID 必须张口就来:


  • 原子性(Atomicity)
  • 一致性(Consistency)
  • 隔离性(Isolation)
  • 持久性(Durability)


那么问题又来了,你觉得如果有多线程事务,那么我们破坏了哪个特性?


多线程事务你也别想的多深奥,你就想,两个不同的用户各自发起了一个下单请求,这个请求对应的后台实现逻辑中是有事务存在的。


这不就是多线程事务吗?


这种场景下你没有想过怎么分别去控制两个用户的事务操作吧?


因为这两个操作之间就是完全隔离的,各自拿着各自的链接玩儿。


所以多个事务之间的最基本的原则是什么?


隔离性。两个事务操作之间不应该相互干扰。


而多线程事务想要实现的是 A 线程异常了。A,B 线程的事务一起回滚。


image.png


事务的特性里面就卡的死死的。所以,多线程事务从理论上就是行不通的。

通过理论指导实践,那么多线程事务的代码也就是写不出来的。

前面说到隔离性。那么请问,Spring 的源码里面,对于事务的隔离性是如何保证的呢?

答案就是 ThreadLocal。

在事务开启的时候,把当前的链接保存在了 ThreadLocal 里面,从而保证了多线程之间的隔离性:


image.png


可以看到,这个 resource 对象是一个 ThreadLocal 对象。

在下面这个方法中进行了赋值操作:

org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin


image.png


其中的 bindResource 方法中,就是把当前链接绑定到当前线程中,其中的 resource 就是我们刚刚说的 ThreadLocal:



image.png


就是每个线程里面都各自玩自己的,我们不可能打破 ThreadLocal 的使用规则,让各个线程共享同一个 ThreadLocal 吧?

铁子,你要是这样去做的话,那岂不是走远了?

所以,无论从理论上,还是代码实现上,我都认为这个需求是不能实现的。

至少我之前是这样想的。

但是事情,稍稍的发生了一点点的变化。


image.png


说个场景,常规实现

任何脱离场景讨论技术实现的行为都是耍流氓。

所以,我们先看一下场景是什么。

假设我们有一个大数据系统,每天指定时间,我们就需要从大数据系统中拉取 50w 条数据,对数据进行一个清洗操作,然后把数据保存到我们业务系统的数据库中。

对于业务系统而言,这 50w 条数据,必须全部落库,差一条都不行。要么就是一条都不插入。

在这个过程中,不会去调用其他的外部接口,也不会有其他的流程去操作这个表的数据。

既然说到一条不差了,那么对于大家直观而言,想到的肯定是两个解决方案:

  1. for 循环中一条条的事务插入。
  2. 直接一条语句批量插入。

对于这种需求,开启事务,然后在 for 循环中一条条的插入可以说是非常 low 的解决方案了。


image.png


效率非常的低下,给大家演示一下。

比如,我们有一个 Student 表,表结构非常简单,如下:



image.png


image.png


这种情况下,我们可以通过下面的链接,模拟插入指定数量的数据:

http://127.0.0.1:8081/insertOneByOne?num=xxx

我尝试了把 num 设置为 50w,让它慢慢的跑着,但是我还是太年轻了,等了非常长的时间都没有等到结果。

于是我把 num 改为了 5000,运行结果如下:

insertOneByOne执行耗时:133449ms,num=5000

一条条的插入 5000 条数据,耗时 133.5 s 的样子。

按照这个速度,插入 50w 条数据得 13350s,大概也是这么多小时:


image.png


这谁顶得住啊。

所以,这方案拥有巨大的优化空间。

比如我们优化为这样批量插入:


image.png


其对应的 sql 语句是这样的:

insert into table ([列名],[列名]) VALUES ([列值],[列值]), ([列值],[列值]);

我们还是通过前端接口调用:


image.png


当我们的 num 设置为 5000 的时候,我页面刷新了 10 次,你看耗时基本上在 200ms 毫秒以内:


image.png


从 133.5s 到 200ms,朋友们,这是什么东西?

这是质的飞跃啊。性能提升了近 667 倍的样子。


image.png


为什么批量插入能有这么大的飞跃呢?

你想啊,之前 for 循环插入,虽然 SpringBoot 2.0 默认使用了 HikariPool,连接池里面默认给你搞 10 个连接。

但是你只需要一个连接,开启一次事务。这个不耗时。

耗时的地方是你 5000 次 IO 呀。

所以,耗时长是必然的。

而批量插入只是一条 sql 语句,所以只需要一个连接,还不需要开启事务。

为啥不用开启事务?

你一条 sql 开启事务有锤子用啊?

那么,如果我们一口气插入 50w 条数据,会是怎么样的呢?

来,搞一波,试一下:

http://127.0.0.1:8081/insertBatch?num=500000


image.png


说你这个包太大了。可以通过设置 max_allowed_packet 来改变包大小。

我们可以通过下面的语句查询当前的配置大小:

select @@max_allowed_packet;


image.png

目录
相关文章
|
监控 安全 数据库
要我说,多线程事务它必须就是个伪命题!(下)
要我说,多线程事务它必须就是个伪命题!(下)
160 0
要我说,多线程事务它必须就是个伪命题!(下)
|
SQL 关系型数据库 MySQL
要我说,多线程事务它必须就是个伪命题!(中)
要我说,多线程事务它必须就是个伪命题!(中)
216 0
要我说,多线程事务它必须就是个伪命题!(中)
|
17天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
47 1
|
3月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
65 1
|
3月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
47 3
|
3月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
29 2
|
3月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
47 2
|
3月前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
54 1
|
3月前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
62 1
|
3月前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
54 1