本篇是《ThreadLocal 那点事儿》的续集,如果您没看上一篇,就就有点亏了。如果您错过了这一篇,那亏得就更大了。

还是保持我一贯的 Style,用一个 Demo 来说话吧。用户提出一个需求:当修改产品价格的时候,需要记录操作日志,什么时候做了什么事情。

想必这个案例,只要是做过应用系统的小伙伴们,都应该遇到过吧?无外乎数据库里就两张表:product 与 log,用两条 SQL 语句应该可以解决问题:

update product set price = ? where id = ?
insert into log (created, description) values (?, ?)

But!要确保这两条 SQL 语句必须在同一个事务里进行提交,否则有可能 update 提交了,但 insert 却没有提交。如果这样的事情真的发生了,我们肯定会被用户指着鼻子狂骂:“为什么产品价格改了,却看不到什么时候改的呢?”。

聪明的我在接到这个需求以后,是这样做的:

首先,我写一个 DBUtil 的工具类,封装了数据库的常用操作:

public class DBUtil {
// 数据库配置
private static final String driver = "com.mysql.jdbc.Driver";
private static final String url = "jdbc:mysql://localhost:3306/demo";
private static final String username = "root";
private static final String password = "root"; // 定义一个数据库连接
private static Connection conn = null; // 获取连接
public static Connection getConnection() {
try {
Class.forName(driver);
conn = DriverManager.getConnection(url, username, password);
} catch (Exception e) {
e.printStackTrace();
}
return conn;
} // 关闭连接
public static void closeConnection() {
try {
if (conn != null) {
conn.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

里面搞了一个 static 的 Connection,这下子数据库连接就好操作了,牛逼吧!

然后,我定义了一个接口,用于给逻辑层来调用:

public interface ProductService {

    void updateProductPrice(long productId, int price);
}

根据用户提出的需求,我想这个接口完全够用了。根据 productId 去更新对应 Product 的 price,然后再插入一条数据到 log 表中。

其实业务逻辑也不太复杂,于是我快速地完成了 ProductService 接口的实现类:

public class ProductServiceImpl implements ProductService {

    private static final String UPDATE_PRODUCT_SQL = "update product set price = ? where id = ?";
private static final String INSERT_LOG_SQL = "insert into log (created, description) values (?, ?)"; public void updateProductPrice(long productId, int price) {
try {
// 获取连接
Connection conn = DBUtil.getConnection();
conn.setAutoCommit(false); // 关闭自动提交事务(开启事务) // 执行操作
updateProduct(conn, UPDATE_PRODUCT_SQL, productId, price); // 更新产品
insertLog(conn, INSERT_LOG_SQL, "Create product."); // 插入日志 // 提交事务
conn.commit();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭连接
DBUtil.closeConnection();
}
} private void updateProduct(Connection conn, String updateProductSQL, long productId, int productPrice) throws Exception {
PreparedStatement pstmt = conn.prepareStatement(updateProductSQL);
pstmt.setInt(1, productPrice);
pstmt.setLong(2, productId);
int rows = pstmt.executeUpdate();
if (rows != 0) {
System.out.println("Update product success!");
}
} private void insertLog(Connection conn, String insertLogSQL, String logDescription) throws Exception {
PreparedStatement pstmt = conn.prepareStatement(insertLogSQL);
pstmt.setString(1, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS").format(new Date()));
pstmt.setString(2, logDescription);
int rows = pstmt.executeUpdate();
if (rows != 0) {
System.out.println("Insert log success!");
}
}
}

代码的可读性还算不错吧?这里我用到了 JDBC 的高级特性 Transaction 了。暗自庆幸了一番之后,我想是不是有必要写一个客户端,来测试一下执行结果是不是我想要的呢? 于是我偷懒,直接在 ProductServiceImpl 中增加了一个 main() 方法:

public static void main(String[] args) {
ProductService productService = new ProductServiceImpl();
productService.updateProductPrice(1, 3000);
}

我想让 productId 为 1 的产品的价格修改为 3000。于是我把程序跑了一遍,控制台输出:

Update product success!
Insert log success!

应该是对了。作为一名专业的程序员,为了万无一失,我一定要到数据库里在看看。没错!product 表对应的记录更新了,log 表也插入了一条记录。这样就可以将 ProductService 接口交付给别人来调用了。

几个小时过去了,QA 妹妹开始骂我:“我靠!我才模拟了 10 个请求,你这个接口怎么就挂了?说是数据库连接关闭了!”。

听到这样的叫声,让我浑身打颤,立马中断了我的小视频,赶紧打开 IDE,找到了这个 ProductServiceImpl 这个实现类。好像没有 Bug 吧?但我现在不敢给她任何回应,我确实有点怕她的。

我突然想起,她是用工具模拟的,也就是模拟多个线程了!那我自己也可以模拟啊,于是我写了一个线程类:

public class ClientThread extends Thread {

    private ProductService productService;

    public ClientThread(ProductService productService) {
this.productService = productService;
} @Override
public void run() {
System.out.println(Thread.currentThread().getName());
productService.updateProductPrice(1, 3000);
}
}

我用这线程去调用 ProduceService 的方法,看看是不是有问题。此时,我还要再修改一下 main() 方法:

// public static void main(String[] args) {
// ProductService productService = new ProductServiceImpl();
// productService.updateProductPrice(1, 3000);
// } public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
ProductService productService = new ProductServiceImpl();
ClientThread thread = new ClientThread(productService);
thread.start();
}
}

我也模拟 10 个线程吧,我就不信那个邪了!

运行结果真的让我很晕、很晕:

Thread-1
Thread-3
Thread-5
Thread-7
Thread-9
Thread-0
Thread-2
Thread-4
Thread-6
Thread-8
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: No operations allowed after connection closed.
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:411)
at com.mysql.jdbc.Util.getInstance(Util.java:386)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1015)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:989)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:975)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:920)
at com.mysql.jdbc.ConnectionImpl.throwConnectionClosedException(ConnectionImpl.java:1304)
at com.mysql.jdbc.ConnectionImpl.checkClosed(ConnectionImpl.java:1296)
at com.mysql.jdbc.ConnectionImpl.commit(ConnectionImpl.java:1699)
at com.smart.sample.test.transaction.solution1.ProductServiceImpl.updateProductPrice(ProductServiceImpl.java:25)
at com.smart.sample.test.transaction.ClientThread.run(ClientThread.java:18)

我靠!竟然在多线程的环境下报错了,果然是数据库连接关闭了。怎么回事呢?我陷入了沉思中。于是我 Copy 了一把那句报错信息,在百度、Google,还有 OSC 里都找了,解答实在是千奇百怪。

我突然想起,既然是跟 Connection 有关系,那我就将主要精力放在检查 Connection 相关的代码上吧。是不是 Connection 不应该是 static 的呢?我当初设计成 static 的主要是为了让 DBUtil 的 static 方法访问起来更加方便,用 static 变量来存放 Connection 也提高了性能啊。怎么搞呢?

于是我看到了 OSC 上非常火爆的一篇文章《ThreadLocal 那点事儿》,终于才让我明白了!原来要使每个线程都拥有自己的连接,而不是共享同一个连接,否则线程1有可能会关闭线程2的连接,所以线程2就报错了。一定是这样!

我赶紧将 DBUtil 给重构了:

public class DBUtil {
// 数据库配置
private static final String driver = "com.mysql.jdbc.Driver";
private static final String url = "jdbc:mysql://localhost:3306/demo";
private static final String username = "root";
private static final String password = "root"; // 定义一个用于放置数据库连接的局部线程变量(使每个线程都拥有自己的连接)
private static ThreadLocal<Connection> connContainer = new ThreadLocal<Connection>(); // 获取连接
public static Connection getConnection() {
Connection conn = connContainer.get();
try {
if (conn == null) {
Class.forName(driver);
conn = DriverManager.getConnection(url, username, password);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
connContainer.set(conn);
}
return conn;
} // 关闭连接
public static void closeConnection() {
Connection conn = connContainer.get();
try {
if (conn != null) {
conn.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
connContainer.remove();
}
}
}

我把 Connection 放到了 ThreadLocal 中,这样每个线程之间就隔离了,不会相互干扰了。

此外,在 getConnection() 方法中,首先从 ThreadLocal 中(也就是 connContainer 中) 获取 Connection,如果没有,就通过 JDBC 来创建连接,最后再把创建好的连接放入这个 ThreadLocal 中。可以把 ThreadLocal 看做是一个容器,一点不假。

同样,我也对 closeConnection() 方法做了重构,先从容器中获取 Connection,拿到了就 close 掉,最后从容器中将其 remove 掉,以保持容器的清洁。

这下应该行了吧?我再次运行 main() 方法:

Thread-0
Thread-2
Thread-4
Thread-6
Thread-8
Thread-1
Thread-3
Thread-5
Thread-7
Thread-9
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!

我去!总算是解决了,QA 妹妹,你应该会对我微笑一下吧?

https://my.oschina.net/huangyong/blog/159725?p=6

ThreadLocal使用场景案例的更多相关文章

  1. 深入MySQL用户自定义变量:使用详解及其使用场景案例

    一.前言 在前段工作中,曾几次收到超级话题积分漏记的用户反馈.通过源码的阅读分析后,发现问题出在高并发分布式场景下的计数器上.计数器的值会影响用户当前行为所获得积分的大小.比如,当用户在某超级话题下连 ...

  2. ThreadLocal应用场景以及源码分析

    一.应用篇 ThreadLocal介绍 ThreadLocal如果单纯从字面上理解的话好像是“本地线程”的意思,其实并不是这个意思,只是这个名字起的太容易让人误解了,它的真正的意思是线程本地变量. 实 ...

  3. Hadoop企业开发场景案例,虚拟机服务器调优

    Hadoop企业开发场景案例 1 案例需求 ​ (1)需求:从1G数据中,统计每个单词出现次数.服务器3台,每台配置4G内存,4核CPU,4线程. ​ (2)需求分析: ​ 1G/128m = 8个M ...

  4. 【Ray Tracing The Next Week 超详解】 光线追踪2-7 任意长方体 && 场景案例

    上一篇比较简单,很久才发是因为做了一些好玩的场景,后来发现这一章是专门写场景例子的,所以就安排到了这一篇 Preface 这一篇要介绍的内容有: 1. 自己做的光照例子 2. Cornell box画 ...

  5. PHP(Mysql/Redis)消息队列的介绍及应用场景案例--转载

    郑重提示:本博客转载自好友博客,个人觉得写的很牛逼所以未经同意强行转载,原博客连接 http://www.cnblogs.com/wt645631686/p/8243438.html 欢迎访问 在进行 ...

  6. Locust性能测试2-先登录场景案例

    前言 有很多网站不登录的话,是无法访问到里面的页面的,这就需要先登录了 实现场景:先登录(只登录一次),然后访问页面->我的地盘页->产品页->项目页 官方案例 下面是一个简单的lo ...

  7. java ThreadLocal(应用场景及使用方式及原理)

    尽管ThreadLocal与并发问题相关,可是很多程序猿只将它作为一种用于"方便传參"的工具,胖哥觉得这或许并非ThreadLocal设计的目的,它本身是为线程安全和某些特定场景的 ...

  8. PHP(Mysql/Redis)消息队列的介绍及应用场景案例

    在进行网站设计的时候,有时候会遇到给用户大量发送短信,或者订单系统有大量的日志需要记录,还有做秒杀设计的时候,服务器无法承受这种瞬间的压力,无法正常处理,咱们怎么才能保证系统正常有效的运行呢?这时候我 ...

  9. Locust性能测试_先登录场景案例

    前言 有很多网站不登录的话,是无法访问到里面的页面的,这就需要先登录了实现场景:先登录(只登录一次),然后访问页面->我的地盘页->产品页->项目页 官方案例 下面是一个简单的loc ...

随机推荐

  1. 爬取 豆瓣电影Top250

    目标 学习爬虫,爬豆瓣榜单,获取爬取静态页面信息的能力 豆瓣电影 Top 250  https://movie.douban.com/top250 代码 import requests from bs ...

  2. Linux源码编译nginx

    1.安装nginx 安装编译工具及库文件 yum -y install make zlib zlib-devel gcc-c++ libtool openssl openssl-devel 首先要安装 ...

  3. Linux磁盘查询指令

    磁盘情况查询: 查询系统整体磁盘使用情况: df -h 查询指定目录的磁盘占用情况 du -h /目录 查询指定目录的磁盘占用情况,默认为当前目录 -s  指定目录占用大小汇总 -h 带计量单位 -a ...

  4. 工作不久的安卓开发者,他们是这样规划自己的Android学习路线

    Android开发工作者工作不久的时候,会有一段迷茫期,觉得自己应该再学一点,却不知道从何学起,该怎样规划自己的学习路线呢?今天,我给大家梳理一下Android基础,就像建造房屋一样,要建造一座宏伟的 ...

  5. <人人都懂设计模式>-中介模式

    真正的用房屋中介来作例子, 好的书籍总是让人记忆深刻. class HouseInfo: def __init__(self, area, price, has_window, has_bathroo ...

  6. 02-docker入门-docker常用的一些命令

    在这里,有必要先对ducker在做一次介绍 ducker 是一个容器. 容器内部运行的是一个系统. 系统内部安装好了要调试 / 发布的工程,然后这个系统被打包成了一个镜像. ducker 就是这个镜像 ...

  7. Tomcat8 访问 manager App 失败

    Tomcat8 访问 manager App 失败 进入 tomcat 8 的下面路径 修改 上面 的 context.xml 注释了下面的框框 保存退出.重启tomcat

  8. springCloud值Eureka

    Spring Cloud特点 约定优于配置 开箱即用.快速启动 适用于各种环境      PC Server  云环境  容器(Docker) 轻量级的组件  服务发现Eureka 组件的支持很丰富, ...

  9. java 的守护进程脚本

    #!/bin/sh ] do Tag=`ps -ef|grep 'jar包名称'|grep -v grep|wc -l|awk '{printf $1"\n"}'` ] then ...

  10. 原生PHP+原生ajax批量删除(超简单),ajax删除,ajax即点即改,完整代码,完整实例

    效果图: 建表:company DROP TABLE IF EXISTS `company`;CREATE TABLE `company` ( `id` int(11) NOT NULL AUTO_I ...