5.数据库注入问题
数据库的注入问题是一个安全性的问题。我们通过一个登录案例来给大家说明
数据库的注入问题:产生的根本原因是什么呢?
数据库注入问题产生的根本原因是因为我们把 用户输入的字符串当成了SQL语句里面的关键字来解析,改变了SQL语句的格式
本来:select * from user where username = ? and password = ?
改变:select * from user where username = ? and password = ? or 1=1
5.1 PrepareStatement
package com.cskaoyan.login;
import com.cskaoyan.utils.JDBCUtils;
import java.sql.*;
public class LoginMain2 {
// 登录的方法
public static Boolean login(String username, String password) throws SQLException {
// 获取数据库连接
Connection connection = JDBCUtils.getConnection();
// 获取PrepareStatement对象 对SQL语句进行预编译
PreparedStatement preparedStatement = connection.prepareStatement("select * from user where username = ? and password = ?");
// 赋值
preparedStatement.setString(1,username);
preparedStatement.setString(2,password);
// 执行
ResultSet resultSet = preparedStatement.executeQuery();
// 判断
if (resultSet.next()) {
System.out.println("查询到了用户…");
JDBCUtils.closeSources(connection,preparedStatement,resultSet);
return true;
}else {
System.out.println("没有查询到用户…");
JDBCUtils.closeSources(connection,preparedStatement,resultSet);
return false;
}
}
}
-
PreparedStatement preparedStatement = connection.prepareStatement(String sql);
这个api是对SQL语句进行预编译
-
preparedStatement.setString(int Index,String value);
这个api是对SQL语句的参数进行赋值
-
preparedStatement.executeQuery()
orpreparedStatement.executeUpdate()
这个api是去执行sql语句
总结一下:我们的statement在去执行SQL语句的时候,和数据库通信了一次,我们的PrepareStatement在去执行sql语句的时候,和数据库通信了两次。
哪两次呢?一次是去把sql语句交给MySQL服务器进行预编译,还有一次是去给MySQL服务器传递参数,让MySQL服务器能够去执行SQL语句。
6. 批处理
批处理就是我们可以通过statement和PrepareStatement一次去执行多条sql语句。
需要加上配置:rewriteBatchedStatements=true
6.1 常规
// 常规手段
public static void normalBatch() throws SQLException {
// 获取连接
Connection connection = JDBCUtils.getConnection();
// 获取statement对象
Statement statement = connection.createStatement();
for (int i = 0; i < 10000; i++) {
statement.executeUpdate("insert into user values (null,'changfeng','changfeng',null)");
}
// 关闭资源
JDBCUtils.closeSources(connection,statement,null);
}
6.2 statement
// statement
public static void statementBatch() throws SQLException {
// 获取连接
Connection connection = JDBCUtils.getConnection();
// 获取statement对象
Statement statement = connection.createStatement();
// 进行批处理
for (int i = 0; i < 10000; i++) {
statement.addBatch("insert into user values (null,'tianming','tianming',null)");
}
// 执行SQL语句
statement.executeBatch();
// 关闭资源
JDBCUtils.closeSources(connection,statement,null);
}
6.3 PrepareStatement
// prepareStatement
public static void prepareStatementBatch() throws SQLException {
// 获取连接
Connection connection = JDBCUtils.getConnection();
// 获取PrepareStatement对象
PreparedStatement preparedStatement = connection.prepareStatement("insert into user values (null,?,?,null)");
// 执行批处理
for (int i = 0; i < 10000; i++) {
preparedStatement.setString(1,"gaoyuanyuan");
preparedStatement.setString(2,"gaoyuanyuan");
preparedStatement.addBatch();
}
// 执行SQL语句
preparedStatement.executeBatch();
// 关闭资源
JDBCUtils.closeSources(connection,preparedStatement,null);
}
总结:通过对比他们的执行效率发现,PrepareStatement是最快的,statement进行批处理比常规的方式要稍微快一点。
常规方式 | Statement | PrepareStatement | |
---|---|---|---|
通信次数 | 10000 | 1 | 2 |
编译次数 | 10000 | 10000 | 1 |
执行次数 | 10000 | 10000 | 10000 |
statement对比PrepareStatement有什么优缺点呢?
- 缺点:批处理效率更低,有数据库注入问题
- 优点:在进行批处理的时候,不限于SQL语句的模板,使用起来更加灵活
注意
默认情况下mysql的批处理仍然是一条一条的,在配置文件的url后面添加上rewriteBatchedStatements=true才可以
7. 事务
7.1 介绍
事务是指逻辑上的一组操作,组成这个操作的各个单元,要么都成功,要么就都不成功。
例如转账这个案例:张三给李四通过银行转账10000元
- 给张三的账户扣钱(10000)
- 给李四的账户加钱(10000)
package com.cskaoyan.transfer;
import com.cskaoyan.utils.JDBCUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class TransferDemo {
public static void main(String[] args) throws SQLException {
transfer("风华哥","景天",100);
}
// 转账方法 工资:100元,
// 风华哥给景天转账100元
public static void transfer(String fromName, String toName, Integer money) {
// 给风华哥扣钱
Connection connection = JDBCUtils.getConnection();
try {
// 开启事务
// 其实开启事务这个说法是不太精确的,我们MySQL去处理SQL语句的时候,会自动把每一条SQL语句都封装成一个事务来处理,并且这个事务是自动提交的
connection.setAutoCommit(false);
PreparedStatement preparedStatement1 = connection.prepareStatement("update account set money = money - ? where name = ?");
// 赋值
preparedStatement1.setInt(1, money);
preparedStatement1.setString(2, fromName);
// 影响的行数
int affectedRows = preparedStatement1.executeUpdate();
if (affectedRows < 1) {
System.out.println("扣钱失败!!");
return;
}
System.out.println("扣钱成功!!!");
int i = 1 / 0;
// 给景天加钱
PreparedStatement prepareStatement2 = connection.prepareStatement("update account set money = money + ? where name = ?");
prepareStatement2.setInt(1, money);
prepareStatement2.setString(2, toName);
int affectedRows2 = prepareStatement2.executeUpdate();
if (affectedRows2 > 0) {
System.out.println("加钱成功~ 转账成功");
connection.commit();
} else {
System.out.println("加钱失败~ 扣钱成功");
connection.rollback();
}
}catch (Exception ex) {
ex.printStackTrace();
try {
connection.rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
补充:我们去操作数据库各个单元的connection对象,必须是同一个,如果不是同一个,那么就做不到同时提交,同时回滚,也就是做不到事务的控制。
那么在分布式的系统中,我们往往是做不到同一个connection对象的,那么如何解决这个问题呢?那就要使用分布式事务来控制
1. connection.setAutoCommit(false); // 开启事务
2. connection.rollback(); // 回滚事务
3. connection.commit(); // 提交事务
7.2 事务的四个特性-ACID
7.2.1 Atomicity 原子性
原子性是指事务是一个不可分割的最小的工作单元,事务里面的各个操作,要么就都成功,要么就都不成功。
7.2.2 Consistency 一致性
事务是指事务必须使数据库从一个一致性的状态到另外一个一致性的状态。
7.2.3 Isolation 隔离性
事务的隔离性指的是各个事务之间的操作不能互相影响,事务与事务的操作要互相隔离。
7.2.4 Druability 持久性
持久性指的是事务的操作一旦被提交,对数据库的改变是永久性的。
7.3 事务的隔离级别
事务的隔离级别是数据库定义的隔离级别。
-
Serializable:串行化
串行化这种隔离级别是指我们的数据库在去处理事务的时候,是一个一个事务来处理的,例如现在有A、B、C三个事务同时开启了,那么数据库在去处理这些事务的时候,会按照顺序,一个一个排队来处理
没有数据安全性的问题,但是效率很低
-
Repeatable read:可重复读(MySQL数据库默认的事务的隔离级别)
我们在一个事务里面,反复读取同一个数据,读取到的结果是一致的。
-
Read committed:读已提交
-
Read uncommitted:读未提交
各个隔离级别的作用:
读未提交(read uncommitted):不管事务是否提交了内容,在其他事务中都会同步修改
读已提交(read committed):其他事务可以看到自身已经提交的事务,但是看不到未提交的事务
可重复读(repeatable read):不受其他事务的影响,安全性较高
串行化(serialization):在处理事务的过程中,必须按照一个事务一个事务来处理,虽然安全性高,但是效率低下
数据的安全性问题有哪些呢?
- 脏读:一个事务读取到了另外一个事务还未提交的数据
- 不可重复读:在同一个事务内,我们去读取同一个数据,前后的结果不一致。一个事务读取到了另一个事务已经提交的数据。
- 虚幻读:我们在一个事务内,我们去读取某些数据,有的时候能读取到,有的时候不能读取到。如果我们要避免虚幻读的问题,那么我们不能读取到别的事务已经插入的数据。
-- 修改数据库隔离级别
set global/session transaction isolation level read uncommitted
-- 查看数据库隔离级别
select @@transaction_isolation;
select @@tx_isolation;
7.3.1 读未提交
-
在一个事务中,可以读取到另一个事务还没有提交的数据,有脏读的问题
-
在一个事务中,可以读取到另外一个事务已经提交的数据,有不可重复读的问题
-
在一个事务中,可以读取到另外一个事务插入的数据,有虚幻读的问题
7.3.2 读已提交
我们发现,读未提交这种事务的隔离级别 没有脏读的问题,但是有 不可重复读以及虚幻读的问题
7.3.3 可重复读
Repeatable read 这种事务的隔离级别是我们MySQL默认的事务的隔离级别
我们发现,MySQL里面的可重复读没有脏读的问题,没有不可重复读的问题,也没有虚幻读的问题。
说明一下:其实可能在别的数据库产品里面,可重复读这种数据库的隔离级别有虚幻读的问题,这里是因为我们MySQL的存储引擎InnoDB给我们解决了虚幻读的问题。如何解决的呢?多版本控制(MVCC,不要求,有兴趣可以了解一下)
7.3.4 串行化
串行化是一个事务执行完了以后再去执行另外一个事务,它没有脏读、不可重复读、虚幻读的问题,但是效率很低。
总结:
脏读 | 不可重复读 | 虚幻读 | |
---|---|---|---|
读未提交 | √ | √ | √ |
读已提交 | × | √ | √ |
可重复读 | × | × | × |
串行化 | × | × | × |
说明:在MySQL里面,可重复读没有虚幻读的问题。默认在MySQL里面,可重复读这个事务隔离级别是安全且高效的。