3-MySQL jdbc,事务,连接
1 jdbc
1.1 jdbc概述#
JDBC(Java DataBase Connectivity,java数据库连接技术)是一种用于执行SQL语句的Java API。
JDBC是Java访问数据库的标准规范,可以为不同的关系型数据库提供统一访问,它由一组用Java语言编写的接口和类组成。
JDBC需要连接驱动,驱动是两个设备要进行通信,满足一定通信数据格式,数据格式由设备提供商规定,设备提供商为设备提供驱动软件,通过软件可以与该设备进行通信。
JDBC与数据库驱动的关系:接口与实现的关系。
1.2 jdbc原理#
Java提供访问数据库规范称为JDBC,而生产厂商提供规范的实现类称为驱动。
其中:
DriverManager:用于注册驱动
Connection:表示与数据库创建的连接
Statement:操作数据库sql语句的对象
ResultSet:结果集或一张虚拟表
1.3 jdbc编程步骤#
1.3.1 加载数据库驱动 #
Class.forName("com.mysql.jdbc.Driver");
创建 com.mysql.jdbc.Driver 这个类的对象供连接数据库使用
JAVA 规范中明确规定:所有的驱动程序必须在静态初始化代码块中将驱动注册到驱动程序管理器中。
注意:mysql5之后的驱动jar包可以省略注册驱动的步骤。在jar包中,存在一个java.sql.Driver配置文件,文件中指定了com.mysql.jdbc.Driver
1.3.2 通过DriverManager 获取数据库连接#
// 获取数据库连接
Connection conn = DriverManager.getConnection(String url, String user, String password);
//参数说明:
// 1)url:需要连接数据库的位置(网址)
// 2)user:登录数据库用户名
// 3)password:登录数据库密码
Connection 连接是通过 DriverManager 的静态方法 getConnection(.....)来得到的, 这个方法的实质是把参数传到实际的 Driver 中的 connect()方法中来获得数据库连接的。
Mysql 的 url 格式:jdbc:mysql://localhost或ip:3306/要连接的数据库名称 [?characterEncoding=UTF-8]
如:
本地连接:jdbc:mysql://localhost:3306/hainiudb
远程连接:jdbc:mysql://192.168.31.131:3306/hainiudb
注意:如果出现关于SSL的警告,可以在连接后添加useSSL=false
1.3.3 获得一个 Statement 对象#
Statement stmt = conn.createStatement();
1.3.4 通过 Statement 执行 Sql 语句#
1.3.5 处理结果集#
使用 Connection 对象获得一个 Statement, Statement 中的 executeQuery(String sql) 方法可以使用 select 语句查询,并且返回一个结果集 ResultSet 通过遍历这个结果集,可以获得 select语句的查寻结果,.
ResultSet 的 next()方法会操作一个游标从第一条记录的前面开始读取,直到最后一条记录。
executeUpdate(String sql) 方法用于执行 DDL 和 DML 语句, 比如可以update, delete 操作。
只有执行 select 语句才有结果集返回。
1.3.6 关闭数据库连接(释放资源)#
rs.close(); //关闭结果集ResultSet
stmt.close();// 关闭Statement
con.close();// 关闭数据库连接Connection
//ResultSet Statement Connection 是依次依赖的。
注意:要按先 ResultSet 结果集,后 Statement,最后 Connection 的顺序关闭资源,
因为 Statement 和 ResultSet 是需要连接时才可以使用的,所以在使用结束之后有
可能其它的 Statement 还需要连接,所以不能先关闭 Connection。
2 jdbc编程实例
2.1 环境准备#
创建mysql工程项目
加载mysql驱动时,要添加mysql 的驱动包。
在pom文件汇总引入mysql驱动包
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
2.2 数据准备#
沿用 hainiudb 数据库的 product 表 和 category 表
2.3 jdbc添加一行数据#
需求:用java jdbc给category表添加一行图书类别
package com.hainiu.demo;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
/**
* 需求:用java jdbc给category表添加一行图书类别
*/
public class JDBCInsertDemo {
public static void main(String[] args) {
// 数据库连接四大属性
String driver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://10.88.10.9:3306/hainiudb?characterEncoding=UTF-8";
String user = "root";
String password = "12345678";
// 获取数据库连接
Connection conn = null;
Statement stat = null;
try{
// 加载驱动
Class.forName(driver);
conn = (Connection) DriverManager.getConnection(url, user, password);
// 编写SQL语句
String sql = "insert into category(cid, cname) values (7, '图书');";
// 创建SQL语句的执行对象
stat = conn.createStatement();
// 执行SQL语句并获取结果
int executeUpdate = stmt.executeUpdate(sql);
if(executeUpdate > 0){
System.out.println("插入成功");
}else{
System.out.println("插入失败");
}
}catch(Exception e){
e.printStackTrace();
}finally {
//--关闭连接
if(conn!=null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}finally {
conn=null;
}
}
if(stat!=null) {
try {
stat.close();
} catch (SQLException e) {
e.printStackTrace();
}finally {
stat=null;
}
}
}
}
}
添加完毕,去数据库查看结果。
2.4 jdbc修改数据#
需求:用java jdbc 把category表中 图书 类别 改成 图书/电子书 类别
package com.hainiu.demo;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
/**
* 需求:用java jdbc 把category表中 图书 类别 改成 图书/电子书 类别
*/
public class JDBCUpdateDemo {
public static void main(String[] args) {
// 数据库连接四大属性
String driver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://127.0.0.1:3306/hainiudb?characterEncoding=UTF-8";
String user = "root";
String password = "12345678";
// 获取数据库连接
Connection conn = null;
Statement stat = null;
try{
// 加载驱动
Class.forName(driver);
conn = (Connection) DriverManager.getConnection(url, user, password);
// 编写SQL语句
String sql = "update category set cname='图书/电子书' where cname='图书';";
// 创建SQL语句的执行对象
stat = conn.createStatement();
// 执行SQL语句并获取结果
int executeUpdate = stmt.executeUpdate(sql);
if(executeUpdate > 0){
System.out.println("修改成功");
}else{
System.out.println("修改失败");
}
}catch(Exception e){
e.printStackTrace();
}finally {
//--关闭连接
if(conn!=null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}finally {
conn=null;
}
}
if(stat!=null) {
try {
stat.close();
} catch (SQLException e) {
e.printStackTrace();
}finally {
stat=null;
}
}
}
}
}
修改完毕,去数据库查看结果。
2.5 jdbc删除数据#
需求:用java jdbc 把category表中 图书/电子书 类别删除
package com.hainiu.demo;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
public class JDBCDeleteDemo {
public static void main(String[] args) {
// 数据库连接四大属性
String driver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://127.0.0.1:3306/hainiudb?characterEncoding=UTF-8";
String user = "root";
String password = "12345678";
// 获取数据库连接
Connection conn = null;
Statement stat = null;
try{
// 加载驱动
Class.forName(driver);
conn = DriverManager.getConnection(url, user, password);
// 编写SQL语句
String sql = "delete from category where cname='图书/电子书';";
// 创建SQL语句的执行对象
stat = conn.createStatement();
// 执行SQL语句并获取结果
int executeUpdate = stmt.executeUpdate(sql);
if(executeUpdate > 0){
System.out.println("删除成功");
}else{
System.out.println("删除失败");
}
}catch(Exception e){
e.printStackTrace();
}finally {
//--关闭连接
if(conn!=null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}finally {
conn=null;
}
}
if(stat!=null) {
try {
stat.close();
} catch (SQLException e) {
e.printStackTrace();
}finally {
stat=null;
}
}
}
}
}
删除完毕,去数据库查看结果。
2.6 jdbc查询数据#
需求:用java jdbc查询product表中价格>5000的商品, 并把多条商品信息封装成Product对象列表返回。
通过查询SQL : select pname, price from product where price>5000;
发现查询会两个字段,要把这两个字段的数据封装到对象中
首先要定义Product类
然后再将查询结果放到该对象中
最后再把对象放到列表中返回
1)定义Product类
package com.hainiuxy;
/**
* 描述商品的实体类
*/
public class Product {
/**
* 商品名称
*/
private String pname;
/**
* 商品价格
*/
private int price;
public Product() {
}
public Product(String pname, int price) {
this.pname = pname;
this.price = price;
}
public String getPname() {
return pname;
}
public void setPname(String pname) {
this.pname = pname;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
@Override
public String toString() {
return "Product[" + this.pname + ", " + this.price + "]";
}
}
2)代码实现
package com.hainiuxy;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
/**
* 需求:用java jdbc查询product表中价格>5000的商品, 并把多条商品信息封装成Product对象列表返回。
*/
public class JDBCSearchDemo {
public static void main(String[] args) throws Exception {
// 数据库连接四大属性
String driver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://127.0.0.1:3306/hainiudb?characterEncoding=UTF-8";
String user = "root";
String password = "12345678";
// 加载驱动
Class.forName(driver);
// 采用自动关闭连接方式
try (
// 获取数据库连接
Connection conn = (Connection) DriverManager.getConnection(url, user, password);
) {
List<Product> products = getProducts(conn);
for (Product p : products) {
System.out.println(p);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 将查询结果封装成Product对象列表返回
*
* @param conn 连接
*/
private static List<Product> getProducts(Connection conn) {
// 编写SQL语句
String sql = "select pname, price from product where price>5000";
List<Product> list = new ArrayList<Product>();
try (
// 创建SQL语句的执行对象
Statement stmt = conn.createStatement();
// 执行SQL语句并获取结果
ResultSet rs = stmt.executeQuery(sql);
) {
// 遍历数据
while (rs.next()) {
String pname = rs.getString(1);
int price = rs.getInt(2);
// 将一行的结果封装成 Product 对象
Product product = new Product(pname, price);
list.add(product);
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
}
在控制台查看输出结果。
2.7 封装ConnectionUtil工具用于获取连接#
发现上面的代码除了业务逻辑外,就剩下获取连接的代码了
为了使程序员更专注于业务,现封装ConnectionUtil工具类来获取连接
ConnectionUtil工具类代码如下:
public class ConnectionUtil {
// 私有构造 不让其他人创建对象
private ConnectionUtil() {}
// 数据库连接四大属性
private static final String DRIVER = "com.mysql.jdbc.Driver";
private static final String URL = "jdbc:mysql://127.0.0.1:3306/hainiudb?characterEncoding=UTF-8";
private static final String USERNAME = "root";
private static final String PASSWORD = "12345678";
// 静态代码块 -- 加载驱动
static {
try {
Class.forName(DRIVER);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* 获取数据库连接
* @return
* @throws SQLException
*/
public static Connection getConnection() throws SQLException {
Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
return connection;
}
/**
* 关闭结果集、SQL处理对象和连接
* @param rs
* @param stat
* @param conn
* @throws SQLException
*/
public static void close(ResultSet rs, Statement stat, Connection conn) throws SQLException {
//--关闭连接
if(conn!=null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}finally {
conn=null;
}
}
if(rs!=null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}finally {
rs=null;
}
}
if(stat!=null) {
try {
stat.close();
} catch (SQLException e) {
e.printStackTrace();
}finally {
stat=null;
}
}
}
}
改造后代码如下:
/**
* 需求:用java jdbc查询product表中价格>5000的商品, 并把多条商品信息封装成Product对象列表返回。
*/
public class JDBCSearchDemo1 {
public static void main(String[] args) throws Exception {
try (
// 获取数据库连接
Connection conn = ConnectionUtil.getConnection();
) {
List<Product> products = getProducts(conn);
for (Product p : products) {
System.out.println(p);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 将查询结果封装成Product对象列表返回
*
* @param conn
* 连接
*/
private static List<Product> getProducts(Connection conn) {
// 编写SQL语句
String sql = "select pname, price from product where price>5000";
List<Product> list = new ArrayList<Product>();
try (
// 创建SQL语句的执行对象
Statement stmt = conn.createStatement();
// 执行SQL语句并获取结果
ResultSet rs = stmt.executeQuery(sql);) {
// 遍历数据
while (rs.next()) {
String pname = rs.getString(1);
int price = rs.getInt(2);
// 将一行的结果封装成 Product 对象
Product product = new Product(pname, price);
list.add(product);
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
}
3 预处理对象
3.1 SQL注入问题#
SQL注入:用户输入的内容作为了SQL语句语法的一部分,改变了原有SQL真正的意义。
比如:
package com.hainiu.test;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Scanner;
import com.hainiu.util.JdbcUtil;
/** @author 作者 薪牛:
@version 创建时间:2022年12月5日 下午6:49:30
**/
public class LoginTest {
public static void main(String[] args) {
// 要求用户输入用户名和密码
Scanner sc = new Scanner(System.in);
System.out.println("请求输入用户名:");
String name = sc.nextLine();
String name = sc.nextLine();
System.out.println("请输入密码:");
String password = sc.nextLine();
loginTest(name, password);
// loginTestPrepared(name,password);
}
private static void loginTest(String name, String password) {
// 将获取到的值和数据库中的数据作比对
Connection conn = null;
Statement stat = null;
ResultSet rs = null;
try {
conn = JdbcUtil.getConnection();
stat = conn.createStatement();
rs = stat.executeQuery("select * from user1 where name='" + name + "' and password='" + password + "'");
if (rs.next()) {
System.out.println("登录成功");
} else {
System.out.println("登录失败"); }
} catch (Exception e) {
e.printStackTrace();
} finally {
JdbcUtil.close(conn, stat, rs);
}
}
}
SQL注入是很危险的,如果有个user表,登录时需要校验用户名和密码,那要是被SQL注入,系统就可以随意登录了。
如何解决?
3.2 预处理对象#
PreparedStatement:
预编译对象,是Statement对象的子类。
特点:
性能高
会把sql语句先编译
能过滤掉用户输入的关键字。
PreparedStatement预处理对象,处理的每条sql语句中所有的实际参数,都必须使用占位符?替换。
使用预处理对象解决SQL注入问题
示例:
private static void loginTest2(String name, String password) {
// 将获取到的值和数据库中的数据作比对
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JdbcUtil.getConnection();
ps = conn.prepareStatement("select * from user1 where name=? and password=?");
ps.setString(1, name);
ps.setString(2, password);
rs=ps.executeQuery();
if (rs.next()) {
System.out.println("登录成功");
} else {
System.out.println("登录失败");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
JdbcUtil.close(conn, ps, rs);
}
}
4 JDBC事务
4.1 什么是事务#
数据库的事务(Transaction)是一种机制、一个操作序列,包含了一组数据库操作命令;
事务把所有的命令作为一个整体一起向系统提交或撤销操作请求,即这一组数据库命令要么同时成功,要么同时失败。
比如转账业务:
小强 给 小亮 转账 100 元, 小强-100, 小亮+100,只有两个update操作都成功时转账才能成功。
如果有一方update 失败, 转账都会失败。
为了 保证两个update操作都成功,需要把这两个操作放在一个事务中。
不是所有的存储引擎支持事务,MyISAM不支持事务;InnoDB支持事务。
4.2 事务的4个特性#
-
原子性(Atomicity): 事务是不可分割的最小操作单位,要么同时成功,要么同时失败
-
一致性(Consistency) :执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的
-
隔离性(Isolation) :多个用户并发访问数据库时,一个用户的事务不能被其它用户的事务所干扰,多个并发事务之间数据要相互隔离。
-
持久性(Durability) :事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
4.3 MySQL事务操作#
MySQL数据库是默认自动提交事务的。
准备数据:
-- 创建账号表
create table account(
id int primary key auto_increment,
`name` varchar(20),
money double
);
-- 添加数据
insert into account values (null,'小明',10000);
insert into account values (null,'小强',10000);
insert into account values (null,'小亮',10000);
查看事务的提交方式:
-- 查询结果:1 表示自动提交; 0 表示手动提交
SELECT @@autocommit;
-- 修改事务的提交方式
SET @@autocommit = 0; -- 设置手动提交
事务操作:
-- 开启事务
START TRANSACTION;
或者
BEGIN;
-- 增删改操作
-- 提交事务
COMMIT; -- 操作生效,数据发生变化 -- 持久化
-- 回滚事务
ROLLBACK; -- 操作无效,数据回滚到开启事务之前的状态
注意:当autocommit的值为0时,可以省略START TRANSACTION 或者 BEGIN 。此时当前连接下的所有SQL语句都是事务的形式,需要手动提交或回滚。
示例:
-- 开启事务(提交或者回滚事务会关闭事务)
START TRANSACTION;
-- 给小明的账户减少1000元
UPDATE account SET money = money - 1000 WHERE `name` = '小明';
-- 给小强的账户增加1000元
UPDATE account SET money = money + 1000 WHERE `name` = '小强';
-- 提交事务
COMMIT;
此时,两个sql语句执行成功,我们提交事务,数据被持久化到数据库。
-- 开启事务(提交或者回滚事务会关闭事务)
START TRANSACTION;
-- 给小明的账户减少1000元
UPDATE account SET money = money - 1000 WHERE `name` = '小明';
-- 出错了
-- 给小强的账户增加1000元
UPDATE account SET money = money + 1000 WHERE `name` = '小强';
-- 回滚事务
ROLLBACK;
如果在多条SQL语句执行过程中,产生错误,回滚事务,此时数据回退到开启事务之前的状态。
4.4 JDBC事务操作#
语法:
try {
// 设置开启事务
conn.setAutoCommit(false);
// jdbc操作1
...
// jdbc操作n
// 提交事务 -- 如果SQL语句全部执行成功,没有异常,提交事务
conn.commit();
} catch(Exception e) {
// 事务回滚 -- 如果捕获到异常, 回滚事务
conn.rollback();
}
代码示例:
版本一:不加入事务管理
public class TransactionDemo01 {
public static void main(String[] args) throws SQLException {
// 获取连接
Connection conn = ConnectionUtil.getConnection();
int money = 1000;
String sql1 = "update account set money = money-? where name=?";
String sql2 = "update account set money = money-? where name=?";
try (
PreparedStatement state1 = conn.prepareStatement(sql1);
PreparedStatement state2 = conn.prepareStatement(sql2);
) {
// 第一个操作
state1.setInt(1, money);
state1.setString(2, "小明");
// 执行
int result1 = state1.executeUpdate();
System.out.println(result1);
// 出现异常
System.out.println(1/0);
// 第二个操作
state2.setInt(1, money);
state2.setString(2, "小强");
// 执行
int result2 = state1.executeUpdate();
System.out.println(result2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
此时,我们查看数据库就会发现数据出现问题,小明的账户减少了,但小强的账户却没有增加。
版本2:加入事务管理
public class TransactionDemo02 {
public static void main(String[] args) throws SQLException {
// 获取连接
Connection conn = ConnectionUtil.getConnection();
int money = 1000;
String sql1 = "update account set money = money-? where name=?";
String sql2 = "update account set money = money-? where name=?";
try (
PreparedStatement state1 = conn.prepareStatement(sql1);
PreparedStatement state2 = conn.prepareStatement(sql2);
) {
// 开启事务 -- 通过连接对象设置自动提交为false
conn.setAutoCommit(false);
// 第一个操作
state1.setInt(1, money);
state1.setString(2, "小明");
// 执行
int result1 = state1.executeUpdate();
System.out.println(result1);
// 出现异常
System.out.println(1/0);
// 第二个操作
state2.setInt(1, money);
state2.setString(2, "小强");
// 执行
int result2 = state1.executeUpdate();
System.out.println(result2);
// 如果SQL语句全部执行成功,提交事务
conn.commit();
} catch (Exception e) {
e.printStackTrace();
// 如果捕获到异常,回滚事务
conn.rollback();
}
}
}
此时,我们查看数据库,发现数据仍然保持原来的状态。
4.5 事务的隔离级别#
4.5.1 事务的并发访问问题#
如果不考虑隔离性,事务存在3种并发访问问题。
1)脏读:一个事务读到了另一个事务未提交的数据。
2)不可重复读:一个事务读到了另一个事务已经提交(update)的数据。引发另一个事务,在事务中的多次查询结果不一致。
3)虚读 /幻读:一个事务读到了另一个事务已经提交(insert)的数据。导致另一个事务,在事务中多次查询的结果不一致。
4.5.2 事务并发问题的解决方案------隔离级别#
数据库规范规定了4种隔离级别,分别用于描述两个事务并发的所有情况。
- read uncommitted 读未提交,一个事务读到另一个事务没有提交的数据。
- read committed 读已提交,一个事务读到另一个事务已经提交的数据。
- repeatable read 可重复读,在一个事务中读到的数据始终保持一致,无论另一个事务是否提交。
- serializable 串行化,同时只能执行一个事务,相当于事务中的单线程。
这四种隔离级别解决的并发访问问题如下:
安全和性能对比
安全性:serializable > repeatable read > read committed > read uncommitted
性能: serializable < repeatable read < read committed < read uncommitted
SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。
Read Uncommitted(读取未提交内容)
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。
Read Committed(读取提交内容)
这是大多数数据库系统的默认隔离级别(例如oracle,但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。
Repeatable Read(可重读)
这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的"幻影" 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。
Serializable(可串行化)
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
这四种隔离级别采取不同的锁类型来实现,若读取的是同一个数据的话,就容易发生问题。例如:
脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新了原有的数据。
幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。
查询和设置数据库隔离级别的命令:
-- 查询数据库事务隔离级别
SELECT @@tx_isolation;
-- 修改数据库事务隔离级别
-- 设置全局事务隔离级别 -- 在重启MySQL数据库之后失效
-- 设置全局事务隔离级别 读未提交: READ UNCOMMITTED
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 设置全局事务隔离级别 读已提交: READ COMMITTED
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置全局事务隔离级别 可重复读: REPEATABLE READ
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 设置全局事务隔离级别 串行化: SERIALIZABLE
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- ----------------------------------------------------
-- 设置会话事务隔离级别 -- 在重新建立连接之后失效
-- 设置会话事务隔离级别 读未提交: READ UNCOMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 设置会话事务隔离级别 读已提交: READ COMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置会话事务隔离级别 可重复读: REPEATABLE READ
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 设置会话事务隔离级别 串行化: SERIALIZABLE
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
4.5.3 四种隔离级别演示#
脏读问题:
再开启一个事务进行一下修改金额操作
此时查看第一个事务,虽然第二个事务还没有提交事务操作,但是数据发生了变化
怎么解决这种问题?--将隔离级别改为读已提交
脏读问题解决:
通过第二个会话窗口开启事务,并修改小明账户的金额,此时不提交事务,发现两次查看到的信息一直
如果第二个事务发生了commit操作,则看到的数据则不一致,这样就解决了一个事务读取到另一个事务还未提交的脏读的情况。
但是对于第一个事务来说,第二个事务的操作是透明的,但是当第一个事务多次的时候,看到的东西是不一样的,这就是不可重复读的问题。
怎么解决呢--》将隔离级别改为可重复读即可
不可重复读问题解决:
第一个事务修改隔离级别并查看账户信息。
第二个事务,修改隔离级别,并进行账户金额修改,并且commit
发现第一个事务多次查看账户信息,是不变的,这样就解决了不可重复读的问题。
知道第一个事务结束后,才能看到第二个事务修改后的结果
但是不可重复读的问题解决了,新的问题出现了,幻读的问题出现了
第一个事务开启事务查看账户信息
第二个账户开启事务,新增'小赵'的账户信息
第一个事务此时想新增'小王'信息,发现报错,但是对于小王来说,查的信息是没有id为4的信息,为什么不让插入,难道有幻觉吗?这就是幻读的问题
怎么解决幻读的问题,将隔离级别改为串行化
幻读问题解决:
第一个事务修改隔离级别为 串行化 并开启事务进行查询账户信息
第二个事务修改隔离级别并开启事务添加小王的账户信息,发现事务卡着不动,添加不进去
知道第一个事务进行了commit之后,第二个事务才能进行操作,串行化能保证同一时刻有一个事务进行操作,避免了幻读的问题,但是性能不高,所以一般不用
第二个事务执行成功
5 数据库连接池
5.1 为什么用连接池#
如果每一次数据访问请求都需经历下面的过程:
建立数据库连接 --> 打开数据库 --> 存取数据 --> 关闭数据库连接
而连接并打开数据库是一件既消耗资源又费时的工作,那么频繁发生这种数据库操作时,系统的性能必然会急剧下降。
5.2 连接池原理#
理解为存放多个连接的集合。
目的:解决建立数据库连接耗费资源和时间很多的问题,提高性能。
连接池的使用:
连接池初始化时,会创建多个连接放池子里
在使用连接时,从连接池中申请连接使用,使用完成之后,再将连接交还给连接池,以备后续重复利用
5.3 编写标准的数据源(规范)#
Java为数据库连接池提供了公共的接口:javax.sql.DataSource,各个厂商需要让自己的连接池实现这个接口。这样应用程序可以方便的切换不同厂商的连接池。
常见的连接池技术
DBCP(DataBase Connection Pool)数据库连接池,是java数据库连接池的一种,由Apache开发,通过数据库连接池,可以让程序自动管理数据库连接的释放和断开。
Druid 是Java语言中最好的数据库连接池。Druid能够提供强大的监控和扩展功能。
C3P0是一个开源的JDBC连接池,它实现了数据源和JNDI绑定,支持JDBC3规范和JDBC2的标准扩展。目前使用它的开源项目有Hibernate,Spring等。
Tomcat-JDBC是Spring Boot中自动配置优先级最高的连接池方案,它的出现是用来替代Apache早期的连接池产品------DBCP 1.x。
HikariCP同样是一个十分快速、简单、可靠的及十分轻量级的连接池,只有130KB,在GitHub上看到的是"光HikariCP"的名称,光就是说明它十分快。
下面从连接性能和查询性能上比较
结论 :
性能表现:hikariCP > druid > tomcat-jdbc > dbcp > c3p0。
根据几种数据源的对比 hikari 无疑性能最优秀的,但是因为是最新技术可能存在潜在的bug,所以我们要使用目前比较稳定的阿里的druid数据源;
5.4 druid数据源配置#
1)添加jar包
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.9</version>
</dependency>
2)修改工具类:
public class JDBCUtil {
// 私有构造 不让其他人创建对象
private JDBCUtil() {}
// 数据库连接四大属性
private static final String DRIVER = "com.mysql.jdbc.Driver";
private static final String URL = "jdbc:mysql://127.0.0.1:3306/hainiu_43_test05?useSSL=false";
private static final String USERNAME = "root";
private static final String PASSWORD = "root";
// 初始化druid连接池
private static DruidDataSource dataSource = null;
// 静态代码块 -- 初始化连接池
static {
// 创建连接池对象
dataSource = new DruidDataSource();
dataSource.setDriverClassName(DRIVER);
dataSource.setUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
}
/**
* 获取数据库连接
* @return
* @throws SQLException
*/
public static Connection getConnection() {
// 从连接池数据源 获取一条连接
DruidPooledConnection conn = null;
try {
conn = dataSource.getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
/**
* 关闭结果集、SQL处理对象和连接
* @param resultSet
* @param statement
* @param connection
* @throws SQLException
*/
public static void close(ResultSet rs, Statement stat, Connection conn) throws SQLException {
//--关闭连接
if(conn!=null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}finally {
conn=null;
}
}
if(rs!=null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}finally {
rs=null;
}
}
if(stat!=null) {
try {
stat.close();
} catch (SQLException e) {
e.printStackTrace();
}finally {
stat=null;
}
}
}
}
3)编写测试类
public class DruidTest {
public static void main(String[] args) {
// 获取连接
Connection conn = JDBCUtil.getConnection();
String sql = "insert into account(id, name, money) values (?,?,?)";
PreparedStatement statement = null;
try {
statement = conn.prepareStatement(sql);
// 设置参数
statement.setInt(1, 0);
statement.setString(2, "测试x");
statement.setDouble(3, 7777);
// 执行操作
int num = statement.executeUpdate();
System.out.println(num);
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
JDBCUtil.close(statement, conn);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}
最终优化:将配置信息放到配置文件中,读取配置文件传递参数。
druid.properties文件配置如下
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/hainiudb
username=root
password=12345678
#初始化连接数10个
initialSize=10
# 连接池中容许保持空闲状态的最小连接数量,低于这个数量将创建新的连接
minIdle=5
# 连接池在同一时间能够分配的最大活动连接的数量 20
maxActive=10
#我最多等待6s,6s的时间还没拿到,就放弃本次的索求
maxWait=6000
修改工具类:
public class JDBCUtil {
// 私有构造 不让其他人创建对象
private JDBCUtil() {}
// 初始化druid连接池
private static DruidDataSource dataSource = null;
// 静态代码块 -- 初始化连接池
static {
Properties properties = new Properties();
//使用ClassLoader加载配置文件,获取字节输入流
InputStream is = JDBCUtil.class.getClassLoader().getResourceAsStream("druid.properties");
properties.load(is)
// 创建连接池对象
dataSource = DruidDataSourceFactory.createDataSource(properties);
}
/**
* 获取数据库连接
* @return
* @throws SQLException
*/
public static Connection getConnection() {
// 从连接池数据源 获取一条连接
DruidPooledConnection conn = null;
try {
conn = dataSource.getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
/**
* 关闭结果集、SQL处理对象和连接
* @param resultSet
* @param statement
* @param connection
* @throws SQLException
*/
public static void close(ResultSet rs, Statement stat, Connection conn) throws SQLException {
//--关闭连接
if(conn!=null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}finally {
conn=null;
}
}
if(rs!=null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}finally {
rs=null;
}
}
if(stat!=null) {
try {
stat.close();
} catch (SQLException e) {
e.printStackTrace();
}finally {
stat=null;
}
}
}
}