JDBC之Connection
Connection
Java连接MySQL
Connection必备知识点
使用MySQLClient发送指令给MySQLServer,可以通过connection进行SQL操作,也可以进行事务等操作。
但是下面首先了解以下知识点:
连接方式:在Java中,使用了java.sql.Connection
实例来表示应用和数据库的一个连接,通信的方式目前基本上采用的是TCP/IP 连接方式。
连接创建时间:java连接MySQL从发送请求到响应,经过多次测试,平均消耗时间为0.1S。要知道在程序中0.1S占比是很大的。
长链接:通过上面的测试,可以知道,客户端连接到服务端的一个连接,是一个长链接。这是一个很消耗性能和时间的事情。因为MySQL服务端需要当前连接分配资源并对资源进行管理。
超时时间:通过上面的描述,如果在长连接内,客户端没有指令发出,要超过服务端的数据。那么MySQL客户端也会做出对应的措施来断掉来自客户端连接
客户端如果长时间不发送command到Server端,连接器就会自动将它断开。这个时间是由参数 wait_timeout 控制的,默认值是 8 小时。
查看wait_timeout对应的SQL:
mysql> show global variables like "wait_timeout";
mysql>set global wait_timeout=28800; 设置全局服务器关闭非交互连接之前等待活动的秒数
利用JDBC使用Connection
概念:接口的实现在数据库驱动中。所有与数据库交互都是基于连接对象的。
作用:
- 1、创建执行SQL的对象(Statement[普通]、PreparedStatement[预编译]、CallableStatement[存储过程]);
- 2、事务操作:(1)setAutoCommit、commit/rollback (2)设置隔离级别[无用]
- 3、设置事务回滚点
- 4、关闭连接:close
问题一:为什么Java操作数据库的连接不是越多越好
手动创建Java连接数据库的connection连接,会带来问题。
应用程序和数据库之间建立 Connection
连接,而数据库机器会为之分配一定的线程资源来维护这种连接,连接数越多,消耗数据库的线程资源也就越多;
另外不同的connection实例之间,可能会操作相同的表数据,也就是高并发,为了支持数据库对ACID特性的支持,数据库又会牺牲更多的资源。
简单地来说,建立Connection
连接,会消耗数据库系统的如下资源:
资源 | 说明 |
---|---|
线程数 | 线程越多,线程的上下文切换会越频繁,会影响其处理能力 |
创建Connection的开销 | 由于Connection负责和数据库之间的通信,在创建环节会做大量的初始化 ,创建过程所需时间和内存资源上都有一定的开销 |
内存资源 | 为了维护Connection对象会消耗一定的内存 |
锁占用 | 在高并发模式下,不同的Connection可能会操作相同的表数据,就会存在锁的情况,数据库为了维护这种锁会有不少的内存开销 |
上述的几种资源会限制数据库的链接数和处理性能。
结论: 数据库资源是比较宝贵的有限资源,当应用程序有数据库连接需求过大时,很容易会达到数据库的连接并发瓶颈。
所以这也是为什么要使用数据库连接池的原因
问题二、数据库最多支持多少Connection连接
以 MYSQL为例,可以通过如下语句查询数据库的最大支持情况:
-- 查看当前数据库最多支持多少数据库连接
show variables like '%max_connections%';
-- 设置当前运行时mysql的最大连接数,服务重启连接数将还原
set GLOBAL max_connections = 200;
-- 修改 my.ini 或者my.cnf 配置文件
max_connections = 200;
具体的设置范围,应当具体的业务背景来调优。
问题三、数据库连接connection执行SQL的顺序
上面也说过,MySQLClient连接到MySQLServer时,在长连接内,客户端没有指令发出,要超过服务端的数据。那么MySQL客户端也会做出对应的措施来断掉来自客户端连接。默认时间是8小时。
也就是说,客户端在持有连接八小时内,没有执行任何命令,那么服务端就会断掉对应的连接。
如果时不时的给MySQL服务端发送一些无用的查询SQL,那么MySQL就会认为当前数据库连接是在起到作用的
我觉得这就是使用数据库连接池的操作过程,为了防止断开数据库连接
那么我们可以给一个数据库连接connection定义一个生命周期。从创建出来数据库连接开始到断开数据库连接之间称之为数据库连接的生命周期。
场景一、一个线程操作一个数据库连接
自动提交
一个线程在持有数据库连接中,默认是在MySQL默认隔离级别下,对于增删改查的SQL来说,都是按照下面的流程来执行
默认添加上了begin和commit/rollback,针对的是每一条增删改查的SQL。
如以下SQL:
select * from account;
update account set name = 'xxx' where id = 1;
select * from account;
那么按照上面的执行流程,在数据库中真实的指令应该如下所示:
begin;
select * from account;
commit/rollback;
begin;
update account set name = 'xxx' where id = 1;
commit/rollback;
begin;
select * from account;
commit/rollback;
问题以及弊端
首先需要知道的是,在上面的三条SQL中,只有一个update操作会创建事务id,对于查询来说不会创建事务id。
那么创建数据库事务id,也需要消耗性能,可以忽略不计。
但是如果想要实现一组SQL是属于同一个事务中的,也就是想让一批SQL的执行是原子性的。要么是同时成功,要么是同时失败,那么就需要手动的来开启事务。
手动提交[开启事务]
按照上面的分析接着向下走,原始一组SQL如下所示:
select * from account;
update account set name = 'xxx' where id = 1;
select * from account;
那么如果手动开启了事务的执行流程应该如下所示:
begin;
select * from account;
update account set name = 'xxx' where id = 1;
select * from account;
commit/rollback;
对于上面的只会创建一个事务id,那么创建事务的性能消耗更能够忽略不计了。
场景二、多个线程操作同一个数据库连接
多个线程操作同一个数据库连接,我们理想的情况是下面这样子
但是也可能存在下面这种情况:
thread_1和thread_2在交叉执行,那么来列表说明一下情况:
用上面的SQL来进行举例说明:
select * from account;
update account set name = 'xxx' where id = x;
select * from account;
thread_1 | thread_2 |
---|---|
begin | |
select * from account; | begin; |
update account set name = 'xxx' where id = 1; | |
select * from account; | |
update account set name = 'xxx' where id = 2; | |
select * from account; | |
commit | |
select * from account; | |
rollback; |
本来thread_2中应该执行的SQL是:
select * from account;
update account set name = 'xxx' where id = 2;
select * from account;
但是现在执行的却是:
select * from account;
update account set name = 'xxx' where id = 1;
update account set name = 'xxx' where id = 2;
select * from account;
但是因为thread_2将其中的一条SQL给提交了,那么就造成了对持有同一个数据库连接的事务造成了影响,但是这又是别的线程所不知道的情况。
但是thread_1又将数据进行了回滚,那么就造成了数据安全问题
解决方式
如果是单机应用的话,可以采用syncronized关键字、lock锁来进行解决,但是只针对于单机应用来说。
如果是分布式架构,那么就需要通过分布式锁来进行解决;
对应的伪代码如下所示:
java.sql.Connection sharedConnection = <创建流程>
## thread #1 的业务伪代码:
synchronized(sharedConnection){
try{
begin;
` update xxx`;
`update yyy`;
`commit`;
} catch(Exception e){
`rollback`;
}
}
## thread #2 的业务伪代码:
synchronized(sharedConnection){
try{
begin;
`delete zzz`;
`insert ttt`;
`rollback`;
} catch(Exception e){
`rollback`;
}
}
小结
综上所述,解决多个线程访问同一个Connection
对象时,必须遵循两个基本原则:
1、以资源互斥的方式访问Connection对象;
2、在线程执行结束时,应当最终及时提交(commit)或回滚(rollback)对Connection的影响;不允许存在尚未被提交或者回滚的语句。
问题四、线程生命周期和数据库连接生命周期
我们最容易理解的是:请求------>controller---->service----->dao---->响应
上述我们可以理解一个线程的生命周期,但是相对比来说,我们的数据库连接只是在其中占据了很小的一部分。
那么我们可以总结出来:线程的执行时间远远大于数据库连接执行SQL的时间。
画图如下所示:
这种情况就会出现狼多肉少的情况,那么就可能会导致多个线程公用一个数据库连接的情况。