Java JDBC
Java事务
通俗的理解,事务是一组原子操作单元,从数据库角度说,就是一组SQL指令,要么全部执行成功,若因为某个原因其中
T条指令执行有错误,则撤销先前执行过的所有指令。更简答的说就是:要么全部执行成功,要么撤销不执行。
事务的ACID特性
- 原子性:表示事务执行过程中的任何失败都将导致事务所做的任何修改失效。
- 一致性:表示当事务执行失败时,所有被该事务影响的数据都应该恢复到事务执行前的状态
- 隔离性:表示事务执行过程中对数据的修改,在事务提交之前对其他事务不可见
- 持久性:表示已提交的数据在事务执行失败时,数据的状态都应该是正确的
事务主要是在对数据库的增删改操作中存在,查询影响的是隔离性。
案例
现在我们需要模拟一个场景:A银行账户给B银行账户转账,那么我们需要在A用户上扣钱,在B用户上加钱。所以我们需要去写两条SQL语句来完成这样的需求,但是我们要开启事务的支持,因为这两条SQL是在一条事务线上的,所以,要么同时执行成功,要么同时执行失败,满足事务的原子性和一致性。
首先,我们如果没开启对事务的支持,那么在A扣钱成功之后、B加钱成功之前如果发生一个异常,这里模拟为一个算术异常,这个异常会被catch语句块捕获,捕获后B用户加钱的SQL就不会被执行,那么这样A扣了钱,但是B却没有接收到钱,这便是破坏了事务的一致性与原子性。
Connection connection=null;
Statement statement=null;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?serverTimezone=GMT", "root", "123456");
connection.setAutoCommit(false);
statement=connection.createStatement();
//A账户扣钱
int num = statement.executeUpdate("update account set balance_A=balance_A-1000 where id=1");
System.out.println(num==1?"A扣钱成功":"A扣钱失败");
//模拟一个异常 这个异常会被捕获 这样A的钱扣了 B的钱却没加上
int i=1/0;
//B账户加钱
num=statement.executeUpdate("update account set balance_B=balance_B+1000 where id=1");
System.out.println(num==1?"转账成功":"转账失败");
//提交事务 在提交的时候才会去修改数据库
connection.commit();
} catch (Exception e) {
//事务回滚
try {
connection.rollback();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
e.printStackTrace();
}finally {
if (statement!=null) {
try {
statement.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if (connection!=null) {
try {
connection.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
在JDBC中是默认开启自动事务提交模式的,所谓自动提交模式就是,每条SQL都被当做一个事务,每执行完一条SQL,就是执行完一个事务那么自动提交这个事务对数据库做修改,我们这个业务的需求希望执行完A加钱SQL、B扣钱SQL语句后才算事务的结束,所以我们需要关闭自动事务提交模式,调用方法
connection.setAutoCommit(false);
设置参数false就是关系自动提交模式。然后在事务执行完成后,调用方法
connection.commit();
手动提交事务,表示这样该事务成功执行。
最后还有一个事务回滚的方法
connection.rollback();
如果出现了异常,那么就会回滚该事务,让数据库返回到事务执行之前的正常状态,通俗点也就是try里面写的对账户的SQL操作当它没执行过。
乐观锁与悲观锁
假设在多线程场景下,多个线程同时对一个数据库中的表进行操作,如果多个线程会去修改同一条数据。
乐观锁:
乐观锁不用去开启事务的支持,采取自动提交的策略。乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量。
乐观锁的实现:
-
CAS 实现:Java 中java.util.concurrent.atomic包下面的原子变量使用了乐观锁的一种 CAS 实现方式。
-
版本号控制:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会 +1。当线程 A 要更新数据时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
#影响记录条数只要大于或者等于1,版本号会加1 update user set username ='tom' where id = 1 and version = "当前数据库中的 version 值"
说明
乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。
悲观锁:
悲观锁要开启事务的支持,采用手动提交的策略。当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写“PCC”,又名“悲观锁”】。
悲观锁,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)
悲观锁主要分为共享锁和排他锁:
- 共享锁【shared locks】又称为读锁,简称 S 锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
- 排他锁【exclusive locks】又称为写锁,简称 X 锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁。获取排他锁的事务可以对数据行读取和修改。
for update
for update 是一种行级锁,又叫排它锁。对于上面转账的案例,我们将转账拆分成两个程序,A转账给B,更新account表,修改A的余额;B接收钱,更新account表,修改B的余额。我们需要修改account表,此时我们要对该表加上排他锁,避免并发操作带来数据的不一致性。
A程序:
public static void main(String[] args) {
Connection connection=null;
Statement statement=null;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?serverTimezone=GMT", "root", "123456");
connection.setAutoCommit(false);
statement=connection.createStatement();
//A账户扣钱
//for update 加上写锁
statement.executeQuery("select * from account where id=1 for update ");
//提交事务 在提交的时候才会去修改数据库
connection.commit();
} catch (Exception e) {
//事务回滚
try {
connection.rollback();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
e.printStackTrace();
}finally {
if (statement!=null) {
try {
statement.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if (connection!=null) {
try {
connection.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}
B程序:
public static void main(String[] args) {
Connection connection=null;
Statement statement=null;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?serverTimezone=GMT", "root", "123456");
connection.setAutoCommit(false);
statement=connection.createStatement();
//B账户加钱
int num=statement.executeUpdate("update account set balance_B=balance_B+1000 where id=1");
System.out.println(num==1?"转账成功":"转账失败");
//提交事务
connection.commit();
} catch (Exception e) {
//事务回滚
try {
connection.rollback();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
e.printStackTrace();
}finally {
if (statement!=null) {
try {
statement.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if (connection!=null) {
try {
connection.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}
我们在程序A的connection.commit();这一行设置一个断点。Debug A程序,此时断点处事务并未提交,因此,写锁并未释放。然后,我们run B程序,准备对account表中的同一行做修改。
可以发现B程序一直阻塞住,并没有执行成功。
现在我们释放A程序中的断点,即提交事务A。再次查看B程序
可以看到转账成功,因为事务A提交,释放了写锁,那么其他的进程就可以读取该行数据,所以显示转账成功。
PreparedStatement防止SQL注入
SQL注入:
指的就是通过在SQL中加入数据库的关键字,导致最终SQL执行的结果是错误的
select * from user where username="张三1" and password="1231";
select * from user where 1 = 1 or password="";
PreparedStatement
Statement对象没有预编译过程,直接执行SQL。
select * from user where username=张三1 and password=1231;
PreparedStatement是Statement的子类,使用这个类对于的对象可以防止SQL注入,因为这个对象在执行SQL时候
分为3个步骤:
-
先预编译SQL,搭建SQL语句框架,但是并没有赋值
先用占位符代替数据
select * from user where username=? and password=? ;
//获取数据库操作对象并且预编译SQL preparedStatement=connection.prepareStatement("select * from user where username=? and password=?");
-
给SQL中注入具体的数据
preparedStatement.setString(1,"张三1"); preparedStatement.setString(2,"1231");
即?=张三1 ?=1231
-
执行SQL
resultSet=preparedStatement.executeQuery();
封装JDBC工具类
- 构造方法私有化
- 所有的方法都要是静态方法
import java.sql.*;
/**
* 封装JDBC工具类
*/
public class JDBCUtils {
//构造方法私有化
private JDBCUtils()
{
}
//静态代码块做类加载加载驱动
static {
try {
//注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* 用于获取数据库对象
*/
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection("jdbc:mysql://localhost:3306/test" +
"?serverTimezone=GMT&characterEncoding=utf-8", "root", "123456");
}
/**
* 用于关闭资源
*/
public static void getClose(ResultSet resultSet, Statement statement,Connection connection){
if (resultSet!=null) {
try {
resultSet.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if (statement!=null) {
try {
statement.close();
} catch (SQLException throwables) {
}
}
if (connection!=null) {
try {
connection.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}
测试:
public static void main(String[] args) {
Connection connection =null;
PreparedStatement preparedStatement =null;
ResultSet resultSet =null;
try {
//获取数据库连接 驱动加载在静态代码块中已经执行
connection = JDBCUtils.getConnection();
//获取数据库操作对象
preparedStatement = connection.prepareStatement("select * from user");
//执行SQL
resultSet = preparedStatement.executeQuery();
if (resultSet!=null)
{
while (resultSet.next())
{
int uid = resultSet.getInt("uid");
String username = resultSet.getString("username");
String password = resultSet.getString("password");
System.out.println(uid+" "+username+" "+password);
}
}
} catch (Exception throwables) {
throwables.printStackTrace();
}finally {
JDBCUtils.getClose(resultSet,preparedStatement,connection);
}
}