10 jdbc
10.1 jdbc概述
JDBC(Java DataBase Connectivity,java数据库连接技术)是一种用于执行SQL语句的Java API。
JDBC是Java访问数据库的标准规范,可以为不同的关系型数据库提供统一访问,它由一组用Java语言编写的接口和类组成。
JDBC需要连接驱动,驱动是两个设备要进行通信,满足一定通信数据格式,数据格式由设备提供商规定,设备提供商为设备提供驱动软件,通过软件可以与该设备进行通信。
JDBC与数据库驱动的关系:接口与实现的关系。
其中:
DriverManager:用于注册驱动
Connection:表示与数据库创建的连接
Statement:操作数据库sql语句的对象
ResultSet:结果集或一张虚拟表
10.2 jdbc原理
Java提供访问数据库规范称为JDBC,而生产厂商提供规范的实现类称为驱动。
10.3 jdbc编程步骤
10.3.1 加载数据库驱动
Class.forName(“com.mysql.jdbc.Driver”);
创建 com.mysql.jdbc.Driver 这个类的对象供连接数据库使用
JAVA 规范中明确规定:所有的驱动程序必须在静态初始化代码块中将驱动注册到驱动程序管理器中。
10.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
10.3.3 获得一个 Statement 对象
Statement stmt = conn.createStatement();
10.3.4 通过 Statement 执行 Sql 语句
10.3.5 处理结果集
使用 Connection 对象获得一个 Statement, Statement 中的 executeQuery(String sql) 方法可以使用 select 语句查询,并且返回一个结果集 ResultSet 通过遍历这个结果集,可以获得 select语句的查寻结果,.
ResultSet 的 next()方法会操作一个游标从第一条记录的前面开始读取,直到最后一条记录。
executeUpdate(String sql) 方法用于执行 DDL 和 DML 语句, 比如可以update, delete 操作。
只有执行 select 语句才有结果集返回。
10.3.6 关闭数据库连接(释放资源)
rs.close(); //关闭结果集ResultSet
stmt.close();// 关闭Statement
con.close();// 关闭数据库连接Connection
//ResultSet Statement Connection 是依次依赖的。
注意:要按先 ResultSet 结果集,后 Statement,最后 Connection 的顺序关闭资源,
因为 Statement 和 ResultSet 是需要连接时才可以使用的,所以在使用结束之后有
可能其它的 Statement 还需要连接,所以不能先关闭 Connection。
10.4 jdbc编程实例
10.4.1 环境准备
加载mysql驱动时,要添加mysql 的驱动包
右键要添加驱动包的工程 → Properties → Java Build Path ,Libraries 选项卡
10.4.2 数据准备
沿用 hainiudb 数据库的 product 表 和 category 表
10.4.3 jdbc添加一行数据
需求:用java jdbc给category表添加一行图书类别
/**
* JDBCSearchDemo.java
* com.hainiu.demo
* Copyright (c) 2021, 海牛版权所有.
* @author 薪牛
*/
package com.hainiu.demo;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
/**
* 需求:用java jdbc给category表添加一行图书类别
* @author 薪牛
* @Date 2021年8月2日
*/
public class JDBCInsertDemo {
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 = "111111";
// 获取数据库连接
Connection conn = null;
Statement stmt = null;
try{
// 加载驱动
Class.forName(driver);
conn = (Connection) DriverManager.getConnection(url, user, password);
// 编写SQL语句
String sql = "insert into category(cid, cname) values (7, '图书');";
// 创建SQL语句的执行对象
stmt = conn.createStatement();
// 执行SQL语句并获取结果
int executeUpdate = stmt.executeUpdate(sql);
if(executeUpdate > 0){
System.out.println("插入成功");
}else{
System.out.println("插入失败");
}
}catch(Exception e){
e.printStackTrace();
}finally{
// 释放资源
try {
stmt.close();
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
10.4.4 jdbc修改数据
需求:用java jdbc 把category表中 图书 类别 改成 图书/电子书 类别
/**
* JDBCSearchDemo.java
* com.hainiu.demo
* Copyright (c) 2021, 海牛版权所有.
* @author 薪牛
*/
package com.hainiu.demo;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
/**
* 需求:用java jdbc 把category表中 图书 类别 改成 图书/电子书 类别
* @author 薪牛
* @Date 2021年8月2日
*/
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 = "111111";
// 获取数据库连接
Connection conn = null;
Statement stmt = null;
try{
// 加载驱动
Class.forName(driver);
conn = (Connection) DriverManager.getConnection(url, user, password);
// 编写SQL语句
String sql = "update category set cname='图书/电子书' where cname='图书';";
// 创建SQL语句的执行对象
stmt = conn.createStatement();
// 执行SQL语句并获取结果
int executeUpdate = stmt.executeUpdate(sql);
if(executeUpdate > 0){
System.out.println("修改成功");
}else{
System.out.println("修改失败");
}
}catch(Exception e){
e.printStackTrace();
}finally{
// 释放资源
try {
stmt.close();
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
10.4.5 jdbc删除数据
需求:用java jdbc 把category表中 图书/电子书 类别删除
/**
* JDBCSearchDemo.java
* com.hainiu.demo
* Copyright (c) 2021, 海牛版权所有.
* @author 薪牛
*/
package com.hainiu.demo;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
/**
* 需求:用java jdbc 把category表中 图书/电子书 类别删除
* @author 薪牛
* @Date 2021年8月2日
*/
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 = "111111";
// 获取数据库连接
Connection conn = null;
Statement stmt = null;
try{
// 加载驱动
Class.forName(driver);
conn = (Connection) DriverManager.getConnection(url, user, password);
// 编写SQL语句
String sql = "delete from category where cname='图书/电子书';";
// 创建SQL语句的执行对象
stmt = conn.createStatement();
// 执行SQL语句并获取结果
int executeUpdate = stmt.executeUpdate(sql);
if(executeUpdate > 0){
System.out.println("删除成功");
}else{
System.out.println("删除失败");
}
}catch(Exception e){
e.printStackTrace();
}finally{
// 释放资源
try {
stmt.close();
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
10.4.6 jdbc查询
需求:用java jdbc查询product表中价格>5000的商品, 并把多条商品信息封装成Product对象列表返回。
通过查询SQL : select pname, price from product where price>5000;
发现查询会两个字段,要把这两个字段的数据封装到对象中
首先要定义Product类
然后再将查询结果放到该对象中
最后再把对象放到列表中返回
1)定义Product类
/**
* Product.java
* com.hainiu.demo.entity
* Copyright (c) 2021, 海牛版权所有.
* @author 薪牛
*/
package com.hainiu.demo.entity;
/**
* 用于封装商品查询结果的类
* @author 薪牛
* @Date 2021年8月4日
*/
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)代码实现
/**
* JDBCSearchDemo.java
* com.hainiu.demo
* Copyright (c) 2021, 海牛版权所有.
* @author 薪牛
*/
package com.hainiu.demo;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import com.hainiu.demo.entity.Product;
/**
* 需求:用java jdbc查询product表中价格>5000的商品, 并把多条商品信息封装成Product对象列表返回。
*
* @author 薪牛
* @Date 2021年8月2日
*/
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 = "111111";
// 加载驱动
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;
}
}
查询结果:
10.4.7 封装ConnectionUtil工具用于获取连接
发现上面的代码除了业务逻辑外,就剩下获取连接的代码了
为了使程序员更专注于业务,现封装ConnectionUtil工具类来获取连接
/**
* ConnectionUtil.java
* com.hainiu.demo.util
* Copyright (c) 2021, 海牛版权所有.
* @author 薪牛
*/
package com.hainiu.demo.util;
import java.sql.Connection;
import java.sql.DriverManager;
/**
* 创建连接的工具
* @author 薪牛
* @Date 2021年8月2日
*/
public class 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 USER = "root";
private static final String PASSWORD = "111111";
/**
* 获取连接<br>
* 用完要释放连接,释放连接有两种方式:
* 1)finally 块关闭
* 2)采用try()内定义,使用完自动释放
*/
public static Connection getConnection() throws Exception{
// 加载驱动
Class.forName(DRIVER);
Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
return conn;
}
}
改造后代码如下:
/**
* JDBCSearchDemo.java
* com.hainiu.demo
* Copyright (c) 2021, 海牛版权所有.
* @author 薪牛
*/
package com.hainiu.demo;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import com.hainiu.demo.entity.Product;
import com.hainiu.demo.util.ConnectionUtil;
/**
* 需求:用java jdbc查询product表中价格>5000的商品, 并把多条商品信息封装成Product对象列表返回。
*
* @author 薪牛
* @Date 2021年8月2日
*/
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;
}
}
10.5 预处理对象
10.5.1 SQL注入问题
SQL注入:用户输入的内容作为了SQL语句语法的一部分,改变了原有SQL真正的意义。
比如:
/**
* JDBCSearchDemo.java
* com.hainiu.demo
* Copyright (c) 2021, 海牛版权所有.
* @author 薪牛
*/
package com.hainiu.demo;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import com.hainiu.demo.util.ConnectionUtil;
/**
* 需求:查询category表中 cname是 家用电器/电脑 的记录
* 由于 SQL注入,导致查询到了所有类别记录
* @author 薪牛
* @Date 2021年8月2日
*/
public class JDBCSearchDemo2 {
public static void main(String[] args) throws Exception {
try (
// 获取数据库连接
Connection conn = ConnectionUtil.getConnection();
) {
query(conn);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 查询
*/
private static void query(Connection conn) {
// String cname = "'家用电器/电脑'";
// sql注入
String cname = "'家用电器/电脑' or 1=1";
// 编写SQL语句
String sql = "select * from category where cname=" + cname;
try (
// 创建SQL语句的执行对象
Statement stmt = conn.createStatement();
// 执行SQL语句并获取结果
ResultSet rs = stmt.executeQuery(sql);) {
// 遍历数据
while (rs.next()) {
// 按照编号获取字段数据
System.out.println(rs.getInt(1) + "\t" + rs.getString(2));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
SQL注入查询结果:
SQL注入是很危险的,如果有个user表,登录时需要校验用户名和密码,那要是被SQL注入,系统就可以随意登录了。
如何解决?
10.5.2 预处理对象
PreparedStatement:
预编译对象,是Statement对象的子类。
特点:
性能高
会把sql语句先编译
能过滤掉用户输入的关键字。
PreparedStatement预处理对象,处理的每条sql语句中所有的实际参数,都必须使用占位符?替换。
使用预处理对象解决SQL注入问题
示例:
/**
* JDBCSearchDemo.java
* com.hainiu.demo
* Copyright (c) 2021, 海牛版权所有.
* @author 薪牛
*/
package com.hainiu.demo;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import com.hainiu.demo.util.ConnectionUtil;
/**
* 需求:查询category表中 cname是 家用电器/电脑 的记录
* 用 预处理对象解决SQL注入问题
* @author 薪牛
* @Date 2021年8月2日
*/
public class JDBCSearchDemo3 {
public static void main(String[] args) throws Exception {
try (
// 获取数据库连接
Connection conn = ConnectionUtil.getConnection();
) {
query(conn);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 查询
*/
private static void query(Connection conn) {
String cname = "家用电器/电脑";
// sql注入不好使
// String cname = "'家用电器/电脑' or 1=1";
// 编写SQL语句,用?做占位符
String sql = "select * from category where cname=?";
try (
// 创建SQL语句的预处理对象
PreparedStatement pstmt = conn.prepareStatement(sql);
) {
// 给占位符赋值, 占位符?的索引是从1开始
pstmt.setString(1, cname);
// 执行SQL语句并获取结果
ResultSet rs = pstmt.executeQuery();
// 遍历数据
while (rs.next()) {
// 按照编号获取字段数据
System.out.println(rs.getInt(1) + "\t" + rs.getString(2));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
11 JDBC事务
11.1 什么是事务
事务指的是逻辑上的一组操作,组成这组操作的各个单元要么全都成功,要么全都失败。
事务作用:保证在一个事务中多次操作要么全都成功,要么全都失败。
比如转账业务:
小强 给 小亮 转账 100 元, 小强-100, 小亮+100,只有两个update操作都成功时转账才能成功。
如果有一方update 失败, 转账都会失败。
为了 保证两个update操作都成功,需要把这两个操作放在一个事务中。
不是所有的存储引擎支持事务,MyISAM不支持事务;InnoDB支持事务。
11.2 事务的4个特性
原子性(Atomicity):
是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
一致性(Consistency):
事务前后数据的完整性必须保持一致。
隔离性(Isolation):
事务的隔离性是指多个用户并发访问数据库时,一个用户的事务不能被其它用户的事务所干扰,多个并发事务之间数据要相互隔离。
持久性(Durability):
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
11.3 mysql事务操作
sql语句 | 描述 |
---|---|
start transaction; | 开启事务(提交事务、回滚事务会关闭事务) |
commit; | 提交事务(操作生效) |
rollback; | 回滚事务(什么也没发生) |
准备数据
1)创建账号表
create table account(
id int primary key auto_increment,
`name` varchar(20),
money double
);
2)初始化数据
insert into account values (null,'小明',10000);
insert into account values (null,'小强',10000);
insert into account values (null,'小亮',10000);
效果 :
事务控制下的更新(提交)
-- 开启事务
START TRANSACTION;
-- 执行更新操作,次数是把更新数据写入内存
update account set money = money + 100 where `name` = '小明';
-- 提交事务,才把数据持久化到磁盘上
COMMIT;
效果 :
上面的事务操作其实和我们平时的操作是一样的,也就是说mysql在执行SQL语句的时候会帮你自动提交;
事务控制下的更新(回滚)
-- 开启事务
START TRANSACTION;
-- 执行更新操作,次数是把更新数据写入内存
update account set money = money + 100 where `name` = '小强';
-- 回滚 (删除内存中的数据,什么也没发生)
ROLLBACK;
效果 :
此处大家注意,钱并没有增加,这就是事务控制中的回滚功能;
11.4 JDBC事务操作
语法:
try{
// 设置开启事务
conn.setAutoCommit(false);
// jdbc操作1
...
// jdbc操作n
// 提交事务
conn.commit();
}catch(Exception e){
// 事务回滚
conn.rollback();
}
下面以转账问题为例:
需求:实现转账逻辑
版本1:
小强-100 和 小亮+100 不在一个事务中
/**
* JDBCSearchDemo.java
* com.hainiu.demo
* Copyright (c) 2021, 海牛版权所有.
* @author 薪牛
*/
package com.hainiu.demo;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import com.hainiu.demo.util.ConnectionUtil;
/**
* 需求:实现转账逻辑
*
* @author 薪牛
* @Date 2021年8月2日
*/
public class JDBCTransferAccountsDemo {
public static void main(String[] args) throws Exception {
try (
// 获取数据库连接
Connection conn = ConnectionUtil.getConnection();) {
String name1 = "小强";
String name2 = "小亮";
int money = 100;
transferAccounts(conn, name1, name2, money);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 转账方法
* @param conn
* @param name1 转出者
* @param name2 转入者
* @param money 转账金额
*/
private static void transferAccounts(Connection conn, String name1, String name2, int money) {
// 小强-100
String sql1 = "update account set money=money-? where name=?";
// 小亮+100
String sql2 = "update account set money=money+? where name=?";
try (
// 小强-100
PreparedStatement pstmt1 = conn.prepareStatement(sql1);
// 小亮+100
PreparedStatement pstmt2 = conn.prepareStatement(sql2);
) {
// 小强-100
pstmt1.setInt(1, money);
pstmt1.setString(2, name1);
// 执行SQL语句并获取结果
int executeUpdate1 = pstmt1.executeUpdate();
// 抛异常
int x = 1 / 0;
// 小亮+100
pstmt2.setInt(1, 100);
pstmt2.setString(2, name2);
// 执行SQL语句并获取结果
int executeUpdate2 = pstmt2.executeUpdate();
if (executeUpdate1 > 0 && executeUpdate2 > 0) {
System.out.println("转账成功");
}
} catch (Exception e) {
System.out.println("修改失败");
e.printStackTrace();
}
}
}
一旦其中一个失败了,就会造成收支不平衡(小强-100, 小亮没有+100)
如果要一致,就得加入事务
版本2:加入事务
/**
* JDBCSearchDemo.java
* com.hainiu.demo
* Copyright (c) 2021, 海牛版权所有.
* @author 薪牛
*/
package com.hainiu.demo;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import com.hainiu.demo.util.ConnectionUtil;
/**
* 需求:实现转账逻辑
*
* @author 薪牛
* @Date 2021年8月2日
*/
public class JDBCTransferAccountsDemo {
public static void main(String[] args) throws Exception {
try (
// 获取数据库连接
Connection conn = ConnectionUtil.getConnection();) {
String name1 = "小强";
String name2 = "小亮";
int money = 100;
transferAccounts(conn, name1, name2, money);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 转账方法
* @param conn
* @param name1 转出者
* @param name2 转入者
* @param money 转账金额
*/
private static void transferAccounts(Connection conn, String name1, String name2, int money) {
// 小强-100
String sql1 = "update account set money=money-? where name=?";
// 小亮+100
String sql2 = "update account set money=money+? where name=?";
try (
// 小强-100
PreparedStatement pstmt1 = conn.prepareStatement(sql1);
// 小亮+100
PreparedStatement pstmt2 = conn.prepareStatement(sql2);
) {
// 【开启事务】
conn.setAutoCommit(false);
// 小强-100
pstmt1.setInt(1, money);
pstmt1.setString(2, name1);
// 执行SQL语句并获取结果
int executeUpdate1 = pstmt1.executeUpdate();
// 抛异常
int x = 1 / 0;
// 小亮+100
pstmt2.setInt(1, 100);
pstmt2.setString(2, name2);
// 执行SQL语句并获取结果
int executeUpdate2 = pstmt2.executeUpdate();
// 【提交事务】
conn.commit();
if (executeUpdate1 > 0 && executeUpdate2 > 0) {
System.out.println("转账成功");
}
} catch (Exception e) {
try {
// 【事务回滚】
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
System.out.println("修改失败");
e.printStackTrace();
}
}
}
还原数据后执行
结果没变:
结论:
当 -100 和 +100 在一个事务中时
1)如果+100 或 -100更新失败,会回滚事务,相当于什么也没发生。
2)如果-100 和 +100都更新成功并提交事务,才会把数据持久化到磁盘,并永久生效。
11.5 事务的隔离级别
11.5.1 事务的**并发访问问题**
如果不考虑隔离性,事务存在3中并发访问问题。
1)脏读:一个事务读到了另一个事务未提交的数据.
2)不可重复读:一个事务读到了另一个事务已经提交(update)的数据。引发另一个事务,在事务中的多次查询结果不一致。
3)虚读 /幻读:一个事务读到了另一个事务已经提交(insert)的数据。导致另一个事务,在事务中多次查询的结果不一致。
11.5.2 事务并发问题的解决方案——隔离级别
数据库规范规定了4种隔离级别,分别用于描述两个事务并发的所有情况。
1.read uncommitted 读未提交,一个事务读到另一个事务没有提交的数据。
a)存在:3个问题(脏读、不可重复读、虚读)。
b)解决:0个问题
\2. read committed 读已提交,一个事务读到另一个事务已经提交的数据。
a)存在:2个问题(不可重复读、虚读)。
b)解决:1个问题(脏读)
\3. repeatable read :可重复读,在一个事务中读到的数据始终保持一致,无论另一个事务是否提交。
a)存在:1个问题(虚读)。
b)解决:2个问题(脏读、不可重复读)
\4. serializable 串行化,同时只能执行一个事务,相当于事务中的单线程。
a)存在:0个问题。
b)解决:3个问题(脏读、不可重复读、虚读)
安全和性能对比
n 安全性:serializable > repeatable read > read committed > read uncommitted
n 性能: serializable < repeatable read < read committed < read uncommitted
SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。
Read Uncommitted(读取未提交内容)
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。
Read Committed(读取提交内容)
这是大多数数据库系统的默认隔离级别(但不是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)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。
在MySQL中,实现了这四种隔离级别,分别有可能产生问题如下所示:
下面,将利用MySQL的客户端程序,分别测试几种隔离级别。测试数据库为test,表为tx;表结构:
id | int |
---|---|
num | int |
两个命令行客户端分别为A,B;不断改变A的隔离级别,在B端修改数据。
(一)、将A的隔离级别设置为read uncommitted(未提交读)
在B未更新数据之前:
B更新数据:
客户端B:
客户端A:
经过上面的实验可以得出结论,事务B更新了一条记录,但是没有提交,此时事务A可以查询出未提交记录。造成脏读现象。未提交读是最低的隔离级别。
(二)、将客户端A的事务隔离级别设置为read committed(已提交读)
在B未更新数据之前:
客户端A:
B更新数据:
客户端B:
客户端A:
经过上面的实验可以得出结论,已提交读隔离级别解决了脏读的问题,但是出现了不可重复读的问题,即事务A在两次查询的数据不一致,因为在两次查询之间事务B更新了一条数据。已提交读只允许读取已提交的记录,但不要求可重复读。
(三)、将A的隔离级别设置为repeatable read(可重复读)
在B未更新数据之前:
客户端A:
B更新数据:
客户端B:
客户端A:
B插入数据:
客户端B:
客户端A:
由以上的实验可以得出结论,可重复读隔离级别只允许读取已提交记录,而且在一个事务两次读取一个记录期间,其他事务部的更新该记录。但该事务不要求与其他事务可串行化。例如,当一个事务可以找到由一个已提交事务更新的记录,但是可能产生幻读问题(注意是可能,因为数据库对隔离级别的实现有所差别)。像以上的实验,就没有出现数据幻读的问题。
(四)、将A的隔离级别设置为 可串行化 (Serializable)
A端打开事务,B端插入一条记录
事务A端:
事务B端:
因为此时事务A的隔离级别设置为serializable,开始事务后,并没有提交,所以事务B只能等待。
事务A提交事务:
事务A端
事务B端
serializable完全锁定字段,若一个事务来查询同一份数据就必须等待,直到前一个事务完成并解除锁定为止 。是完整的隔离级别,会锁定对应的数据表格,因而会有效率的问题。
12 使用数据库连接池技术提升JDBC性能
12.1 为什么用连接池
如果每一次数据访问请求都需经历下面的过程:
建立数据库连接 --> 打开数据库 --> 存取数据 --> 关闭数据库连接
而连接并打开数据库是一件既消耗资源又费时的工作,那么频繁发生这种数据库操作时,系统的性能必然会急剧下降。
12.2 连接池原理
理解为存放多个连接的集合。
目的:解决建立数据库连接耗费资源和时间很多的问题,提高性能。
连接池的使用:
连接池初始化时,会创建多个连接放池子里
在使用连接时,从连接池中申请连接使用,使用完成之后,再将连接交还给连接池,以备后续重复利用
12.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数据源;
12.4 druid数据源配置
12.4.1 druid数据库连接池
Druid是Java语言中最好的数据库连接池。Druid能够提供强大的监控和扩展功能。
12.4.2 使用步骤
1)添加jar包
2)修改工具类:
/**
* JDBCUtil.java
* com.hainiu.demo.util
* Copyright (c) 2021, 海牛版权所有.
* @author 薪牛
*/
package com.hainiu.demo.util;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import com.alibaba.druid.pool.DruidDataSource;
/**
* 数据库连接工具,内部数据源为DruidDataSource
* @author 薪牛
* @Date 2021年8月4日
*/
public class JDBCUtil {
// 数据库连接四大属性
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 USER = "root";
private static final String PASSWORD = "111111";
// 创建Druid数据源对象
private static DruidDataSource dataSource = new DruidDataSource();
// 赋值
static{
dataSource.setDriverClassName(DRIVER);
dataSource.setUrl(URL);
dataSource.setUsername(USER);
dataSource.setPassword(PASSWORD);
}
// 编写数据库打开方法
public static Connection getConnection(){
System.out.println("[系统日志]开始获取数据库连接,请稍后!");
// 创建方法的返回值
Connection conn = null;
try {
conn = dataSource.getConnection();
System.out.println("[系统日志]已获取数据库连接!" + conn);
} catch (SQLException e) {
System.err.println("[系统日志]你URL或者用户名密码写错了!");
e.printStackTrace();
}
// 返回
return conn;
}
// 编写释放资源方法
public static void closeAll(Connection conn,PreparedStatement stat,ResultSet set){
// 顺序关闭 第一个关 set 第二个关 stat 第三个关 conn
try {
if(null != set){
set.close();
System.out.println("[系统日志]对象 " + set + " 已释放!");
}
if(null != stat){
stat.close();
System.out.println("[系统日志]对象 " + stat + " 已释放!");
}
if(null != conn){
conn.close();
System.out.println("[系统日志]对象 " + conn + " 已释放!");
}
} catch (SQLException e) {
System.err.println("[系统日志]关闭个连接都能报错?嗯???");
e.printStackTrace();
}
}
}
3)基于该工具类实现多次转账
/**
* JDBCSearchDemo.java
* com.hainiu.demo
* Copyright (c) 2021, 海牛版权所有.
* @author 薪牛
*/
package com.hainiu.demo;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import com.hainiu.demo.util.JDBCUtil;
/**
* 需求:实现转账逻辑
*
* @author 薪牛
* @Date 2021年8月2日
*/
public class JDBCTransferAccountsDemo2 {
public static void main(String[] args) throws Exception {
String name1 = "小强";
String name2 = "小亮";
int money = 100;
transferAccounts(name1, name2, money);
transferAccounts(name1, name2, money);
}
/**
* 转账方法
*
* @param name1
* 转出者
* @param name2
* 转入者
* @param money
* 转账金额
*/
private static void transferAccounts(String name1, String name2, int money) {
// 小强-100
String sql1 = "update account set money=money-? where name=?";
// 小亮+100
String sql2 = "update account set money=money+? where name=?";
Connection conn = JDBCUtil.getConnection();
try (
// 小强-100
PreparedStatement pstmt1 = conn.prepareStatement(sql1);
// 小亮+100
PreparedStatement pstmt2 = conn.prepareStatement(sql2);
) {
// 开启事务
conn.setAutoCommit(false);
// 小强-100
pstmt1.setInt(1, money);
pstmt1.setString(2, name1);
// 执行SQL语句并获取结果
int executeUpdate1 = pstmt1.executeUpdate();
// 小亮+100
pstmt2.setInt(1, 100);
pstmt2.setString(2, name2);
// 执行SQL语句并获取结果
int executeUpdate2 = pstmt2.executeUpdate();
// 提交事务
conn.commit();
if (executeUpdate1 > 0 && executeUpdate2 > 0) {
System.out.println("转账成功");
}
} catch (Exception e) {
// 事务回滚
try {
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
System.out.println("修改失败");
e.printStackTrace();
}finally{
JDBCUtil.closeAll(conn, null, null);
}
}
}
查看转账结果: