2021年7月29日------JDBC

JDBC

  • 一、JDBC概述
  • 二、实现jdbc
    • 1.statement
      • 功能讲解
      • JDBCUtils工具类
      • 登录操作
    • 2.sql注入攻击
      • 1).sql注入攻击
      • 2).PreparedStatement原理
    • 3.PreparedStatement简介
      • 1.PreparedStatement
    • 4.JDBC进行批处理
      • 第一种方式:
      • 第二种方式:
  • 连接池
    • 开源连接池:
      • DBCP
      • C3p0
    • 事务
      • 概念
      • 操作
      • 通过JDBC控制事务
      • 事务的四大特性(ACID)
      • 设计隔离性
      • 数据库的隔离级别
      • 选择隔离级别的原则
    • 数据库中的锁机制
      • a.共享锁、排他锁
      • 死锁:
      • 行级锁、表级锁:

一、JDBC概述

1.驱动

为了能让程序员利用java程序操作数据库,数据库厂商提供了一套jar包,通过导入这个jar包就可以直接调用其中的方法操作数据库,这个jar包称之为驱动。

2.JDBC简介:
JDBC全称为:Java DataBase Connectivity(java数据库连接),它主要由接口组成。
组成JDBC的2个包:
java.sql包 javax.sql包
开发JDBC应用需要以上2个包的支持外,还需要导入相应JDBC的数据库实现(即数据库驱动)。
不仅需要jdbc接口,还需要驱动这个实现,驱动中就是对jdbc接口的一些实现。

二、实现jdbc

1.statement

o 注册数据库驱动
o 获取数据库连接
o 创建传输器
o 传输sql并返回结果
o 遍历结果
o 关闭资源

package com.pcy.jdbc;import java.sql.*;public class jdbcDemo {public static void main(String[] args) {Connection conn=null;Statement statement=null;ResultSet resultSet=null;try {//1.注册数据库驱动//Driver 里的静态代码块注册一次数据库驱动,自己手写注册数据库驱动一共两次
// 第一种方式           DriverManager.registerDriver(new Driver());//使用反射来加载类只会注册数据库驱动一次Class.forName("com.mysql.jdbc.Driver");//2.获取数据库连接
//            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?user=root&password=123456");conn=DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb","root","123456");//3.创建传输器statement = conn.createStatement();//4.传输sql语句并且返回结果,获取行标resultSet = statement.executeQuery("select * from em");//5.遍历结果while (resultSet.next()){//获取每行的数据int id = resultSet.getInt(1);//根据列的下标来获取内容String name = resultSet.getString(2);String s = resultSet.getString("gender");//根据列的下标来获取内容System.out.println(id+":"+name+";性别:"+s);}} catch (SQLException | ClassNotFoundException throwables) {throwables.printStackTrace();}finally {if (resultSet != null){//6.关闭资源try {resultSet.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {resultSet=null;}}if (resultSet != null){//关闭资源try {statement.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {statement=null;}}if (conn != null){//关闭资源try {resultSet.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {resultSet=null;}try {statement.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {statement=null;}try {conn.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {conn=null;}}}}
}

功能讲解

1.DriverManager
Jdbc程序中的DriverManager用于加载驱动,并创建与数据库的链接,这个API的常用方法:
DriverManager.registerDriver(new Driver())
DriverManager.getConnection(url, user, password)
注意:在实际开发中并不推荐采用registerDriver方法注册驱动。
原因有二:
1.查看Driver的源代码可以看到,如果采用此种方式,会导致驱动程序注册两次,也就是在内存中会有两个Driver对象。
2.程序依赖mysql的api,脱离mysql的jar包,程序将无法编译,将来程序切换底层数据库将会非常麻烦。
推荐方式:Class.forName(“com.mysql.jdbc.Driver”);
o采用此种方式不会导致驱动对象在内存中重复出现,并且采用此种方式,程序仅仅只需要一个字符串,不需要依赖具体的驱动,使程序的灵活性更高。
o同样,在开发中也不建议采用具体的驱动类型指向getConnection方法返回的connection对象。

2.数据库URL

URL用于标识数据库的位置,程序员通过URL地址告诉JDBC程序连接哪个数据库,URL的写法为:
jdbc:mysql://localhost:3306/test?参数名=参数值

3.常用数据库URL地址的写法:

Oracle写法:jdbc:oracle:thin:@localhost:1521:sid
SqlServer—jdbc:microsoft:sqlserver://localhost:1433; DatabaseName=sid
MySql—jdbc:mysql://localhost:3306/sid
Mysql的url地址的简写形式: jdbc:mysql:///sid
常用属性:useUnicode=true&characterEncoding=UTF-8

4.Connection

oJdbc程序中的Connection,它用于代表数据库的链接,Connection是数据库编程中最重要的一个对象,客户端与数据库所有交互都是通过connection对象完成的,这个对象的常用方法:

createStatement():创建向数据库发送sql的statement对象。
prepareStatement(sql) :创建向数据库发送预编译sql的PrepareSatement对象。
prepareCall(sql):创建执行存储过程的callableStatement对象。
setAutoCommit(boolean autoCommit):设置事务是否自动提交。
commit() :在链接上提交事务。
rollback() :在此链接上回滚事务。

5.Statement
oJdbc程序中的Statement对象用于向数据库发送SQL语句,
Statement对象常用方法:

executeQuery(String sql) :用于向数据发送查询语句。
executeUpdate(String sql):用于向数据库发送insert、update或delete语句
execute(String sql):用于向数据库发送任意sql语句
addBatch(String sql) :把多条sql语句放到一个批处理中。
executeBatch():向数据库发送一批sql语句执行。

6.ResultSet
Jdbc程序中的ResultSet用于代表Sql语句的执行结果。Resultset封装执行结果时,采用的类似于表格的方式。ResultSet 对象维护了一个指向表格数据行的游标,初始的时候,游标在第一行之前,
调用ResultSet.next() 方法,可以使游标指向具体的数据行,进行调用方法获取该行的数据。
ResultSet既然用于封装执行结果的,所以该对象提供的都是用于获取数据的get方法:

== 提问:数据库中列的类型是varchar,获取该列的数据调用什么方法?Int类型呢?bigInt类型呢?Boolean类型?==

7.ResultSet中的api
ResultSet还提供了对结果集进行滚动的方法:

next():移动到下一行
Previous():移动到前一行
absolute(int row):移动到指定行
beforeFirst():移动resultSet的最前面。
afterLast() :移动到resultSet的最后面

8.为什么要关闭资源?
在安装数据库的时候,设置过最大连接数量,如果用了不还连接,别人就无法使用了。

o rs对象中可能包含很大的一个数据,对象保存在内存中,这样就十分占用内存。需要将他关闭。
最晚创建的对象,最先关闭。
o Jdbc程序运行完后,切记要释放程序在运行过程中,创建的那些与数据库进行交互的对象,这些对象通常是ResultSet, Statement和Connection对象。
o 特别是Connection对象,它是非常稀有的资源,用完后必须马上释放,如果Connection不能及时、正确的关闭,极易导致系统宕机。Connection的使用原则是尽量晚创建,尽量早的释放。
o 为确保资源释放代码能运行,资源释放代码也一定要放在finally语句中。

释放资源:
o 在关闭过程中可能会出现异常,为了能够关闭资源,需要将资源在finally中关闭。
o 如果在finally中关闭资源则需要将conn,stat,rs三个对象定义成全局的变量。
o 在conn,stat,rs三个变量出现异常的时候可能会关闭不成功,我们需要将他们在finally中置为null。conn,stat,rs这三个对象是引用,将引用置为null,它引用的对象就会被JVM回收,也能保证资源的释放。

增加一条数据

package com.pcy.jdbc;import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;public class jdbcDemo2 {public static void main(String[] args) {Connection conn=null;Statement state=null;//注册数据库驱动try {Class.forName("com.mysql.jdbc.Driver");//连接数据库conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "root", "123456");//创建传输器state = conn.createStatement();//传入mysql并且返回影响的行数int i = state.executeUpdate("insert into emp value(8,'pan',2)");if (i>0){System.out.println("插入成功:"+i);}else {System.out.println("插入失败");}} catch (ClassNotFoundException e) {e.printStackTrace();} catch (SQLException throwables) {throwables.printStackTrace();} finally {if (state!=null){try {state.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {state =null;}}if (conn!=null){try {conn.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {conn =null;}}}}
}

JDBCUtils工具类

注意:这里的database.properties必须写在src目录下,否则不识别

package com.pcy.jdbc.utils;import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;public class JDBCUtils {//私有化构造方法private JDBCUtils(){}//创建Properties类对象//静态属性(对象)---全局统一---只需要创建一次对象就可以读取配置内容private static Properties p=new Properties();//静态代码块---保证先读取配置文件内容static {//JDBCUtils.class---获取当前类的字节码对象//getClassLoader()---获取类加载器//getResource("src目录下的文件名称")---获取当前工程的src目录//getPath()---把URL类型转换成String类型try {p.load(new FileInputStream(new File(JDBCUtils.class.getClassLoader().getResource("database.properties").getPath())));} catch (IOException e) {e.printStackTrace();}}//定义静态方法---注册驱动以及获取数据库连接public static Connection getConnection() throws ClassNotFoundException, SQLException {//注册数据库驱动//获取配置文件的driver键对应值Class.forName(p.getProperty("driver"));//获取数据库连接///通过配置文件的键对来获取对应值return DriverManager.getConnection(p.getProperty("url"),p.getProperty("username"),p.getProperty("password"));}//定义静态方法---关闭资源public static void close(Connection conn, Statement stat, ResultSet rs){if(rs!=null)try {rs.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {rs=null;}if(stat!=null)try {stat.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {stat=null;}if(conn!=null)try {conn.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {conn=null;}}
}

database.properties

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mydb
username=root
password=123456

工具类的作用

在程序中,大量代码重复导致代码复用性低,工作效率低下,不美观。为了提供代码复用性,我们将创建连接和关闭连接提取到工具类中,方便以后调用,提升工作效率,增强代码复用性。

登录操作

public class LoginJdbcDemo {public static void main(String[] args) {//获取控制台输入的用户名和密码Scanner sc=new Scanner(System.in);System.out.println("请输入用户名:");int id = sc.nextInt();System.out.println("请输入密码:");//这里输入nextLine就会自动结束String name=sc.next();login(id,name);}public static void login(int id,String name){Connection conn=null;Statement state=null;ResultSet rs=null;try {//创建连接conn = JDBCUtils.getConnection();//创建一个传输器state = conn.createStatement();//传入sql语句rs = (ResultSet) state.executeQuery("select * from dept where id='" +id+ "'and name='" +name+ "'");if (rs.next()){System.out.println("查询成功");}else {System.out.println("用户不存在");}} catch (ClassNotFoundException e) {e.printStackTrace();} catch (SQLException throwables) {throwables.printStackTrace();}finally {JDBCUtils.close(conn,state,rs);}}
}

2.sql注入攻击

1).sql注入攻击

在网页中输入’#,或者’ or '1=1 就能够直接登录。

select * from user where name =  'name' and password = 'password';
select * from user where name='name'#' and password='password'

由于执行的sql语句是在后台拼接出来的,其中有一部分内容是由用户从客户端传入,所以当用户传入数据中包含sql关键字时,就有可能通过这些关键字改变sql语句的语义,从而执行一些特殊的操作,这样的攻击方式就叫做sql注入攻击。

2).PreparedStatement原理

PreparedStatement利用预编译的机制将sql语句的主干和参数分别传输给数据库服务器,从而使数据库可以分辨出哪些是sql语句的主干,哪些是参数,这样一来即使参数中带了sql的关键字,数据库服务器也仅仅将他当做参数值使用,关键字不会起作用,从而从原理上防止了sql注入的问题。

3.PreparedStatement简介

1.PreparedStatement

o PreperedStatement是Statement的孩子,它的实例对象可以通过调用Connection.preparedStatement()方法获得,相对于Statement对象而言:

o PreperedStatement可以避免SQL注入的问题。

o Statement会使数据库频繁编译SQL,可能造成数据库缓冲区溢出。PreparedStatement 可对SQL进行预编译,从而提高数据库的执行效率。

o 并且PreperedStatement对于sql中的参数,允许使用占位符的形式进行替换,简化sql语句的编写。

package com.pcy.jdbc;import com.pcy.jdbc.utils.JDBCUtils;import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;public class PrepareStatementDemo {public static void main(String[] args) {Connection conn=null;PreparedStatement ps=null;ResultSet rs=null;try {conn= JDBCUtils.getConnection();ps = conn.prepareStatement("select * from  dept where id=? and name=?");ps.setInt(1,1);ps.setString(2,"财务部");rs = ps.executeQuery();if (rs.next()){System.out.println("登录成功");}} catch (ClassNotFoundException e) {e.printStackTrace();} catch (SQLException throwables) {throwables.printStackTrace();}finally {JDBCUtils.close(conn,ps,rs);}}
}

4.JDBC进行批处理

使用业务场景:
当需要向数据库发送一批SQL语句执行时,应避免向数据库一条条的发送执行,而应采用JDBC的批处理机制,以提升执行效率。
o实现批处理有两种方式,

第一种方式:

Statement.addBatch(sql)
执行批处理SQL语句
executeBatch()方法:执行批处理命令
clearBatch()方法:清除批处理命令

public class BatchDemo {public static void main(String[] args) {Connection conn=null;Statement state=null;ResultSet rs=null;try {conn=JDBCUtils.getConnection();/*(1)如果是批量新增或修改操作,SQL语句要是insert into 或 update 语句(2)如果是批量查询操作,SQL是select语句,但是不能调用batchUpdate方法*/String sql1="insert into dept value(6,'小卖部'),(7,'部长')";String sql2="update dept set name ='部长' where id=1";state = conn.createStatement();
//            state.addBatch(sql1);
//            state.addBatch(sql2);state.addBatch(sql1);state.addBatch(sql2);state.executeBatch();} catch (ClassNotFoundException e) {e.printStackTrace();} catch (SQLException throwables) {throwables.printStackTrace();}finally {//执行关闭JDBCUtils.close(conn,state,rs);}}
}

o采用Statement.addBatch(sql)方式实现批处理:

优点:可以执行多条不同结构的sql语句
缺点:没有使用预编译机制,效率低下,如果要执行多条结构相同仅仅参数不同的语句时,仍然需要多次写sql语句的主干。

第二种方式:

package com.pcy.jdbc;import com.pcy.jdbc.utils.JDBCUtils;import java.sql.*;public class BatchDemo1 {public static void main(String[] args) {Connection conn=null;PreparedStatement ps=null;try {conn=JDBCUtils.getConnection();ps = conn.prepareStatement("select * from dept where id=?");for (int i=0;i<1000;i++){ps.setString(1,""+i);if (i%10==0){ps.executeBatch();ps.clearBatch();}ps.executeBatch();}} catch (ClassNotFoundException e) {e.printStackTrace();} catch (SQLException throwables) {throwables.printStackTrace();}finally {//执行关闭JDBCUtils.close(conn,ps,null);}}
}

PreparedStatement批处理:
o采用ps.addBatch(sql)方式实现批处理:
优点:有预编译机制,效率比较高.执行多条结构相同,参数不同的sql时,不需要重复写sql的主干
缺点:只能执行主干相同参数不同的sql,没有办法在一个批中加入结构不同的sql

连接池

为什么使用连接池
缺点:
不使用连接池的时候
用户每次请求都需要向数据库获得链接,而数据库创建连接通常需要消耗相对较大的资源,创建时间也较长。假设网站一天10万访问量,数据库服务器就需要创建10万次连接,极大的浪费数据库的资源,并且极易造成数据库服务器内存溢出、宕机。
o它是将那些已连接的数据库连接存放在一个容器里(连接池),这样以后别人要连接数据库的时候,将不会重新建立数据库连接,会直接从连接池里取出可用的连接,用户使用完毕后,连接又重新还回到连接池中。
o注意:连接池里的连接将会一直保存在内存里,即使你没用也是一样。所以这个时候你得权衡一下连接池的连接数量了。

开源连接池:

现在很多WEB服务器(Weblogic, WebSphere, Tomcat)都提供了DataSoruce的实现,即连接池的实现。通常我们把DataSource的实现,按其英文含义称之为数据源,数据源中都包含了数据库连接池的实现。
也有一些开源组织提供了数据源的独立实现:
DBCP 数据库连接池
C3P0 数据库连接池
实际应用时不需要编写连接数据库代码,直接从数据源获得数据库的连接。程序员编程时也应尽量使用这些数据源的实现,以提升程序的数据库访问性能。

DBCP

DBCP 是 Apache 软件基金组织下的开源连接池实现,使用DBCP数据源,应用程序应在系统中增加如下两个 jar 文件:
Commons-dbcp.jar:连接池的实现
Commons-pool.jar:连接池实现的依赖库
#初始化连接
initialSize=10

#最大连接数量
maxActive=50

#最大空闲连接
maxIdle=20

#最小空闲连接
minIdle=5

#超时等待时间以毫秒为单位 6000毫秒/1000等于60秒
maxWait=60000

package com.pcy.jdbc;import org.apache.commons.dbcp.BasicDataSourceFactory;import javax.sql.DataSource;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;public class DbcpDemo {public static void main(String[] args) {Connection conn=null;PreparedStatement ps=null;ResultSet rs=null;Properties p=new Properties();try {p.load(new FileInputStream(new File(DbcpDemo.class.getClassLoader().getResource("dbcp.properties").getPath())));//创建工厂对象BasicDataSourceFactory factory=new BasicDataSourceFactory();DataSource dataSource = factory.createDataSource(p);conn = dataSource.getConnection();ps = conn.prepareStatement("select * from dept");rs = ps.executeQuery();if (rs.next()){System.out.println(rs.getString(1));System.out.println(rs.getString(2));}} catch (IOException e) {e.printStackTrace();} catch (Exception e) {e.printStackTrace();}finally {if(rs!=null)try {rs.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {rs=null;}if(ps!=null)try {ps.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {ps=null;}if(conn!=null)try {//归还连接conn.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {conn=null;}}}
}

dbcp.properties

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mydb
username=root
password=123456

C3p0

注意:配置文件必须是c3p0.properties

package com.pcy.jdbc;import com.mchange.v2.c3p0.ComboPooledDataSource;
import com.mchange.v2.c3p0.impl.C3P0ImplUtils;
import com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool;
import com.mchange.v2.c3p0.jboss.C3P0PooledDataSource;import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;public class C3p0Demo {public static void main(String[] args) {Connection conn=null;PreparedStatement ps=null;ResultSet rs=null;//new一个连接池对象ComboPooledDataSource source = new ComboPooledDataSource();try {conn = source.getConnection();ps = conn.prepareStatement("select * from dept");rs = ps.executeQuery();if (rs.next()){System.out.println(rs.getString(1));System.out.println(rs.getString(2));}} catch (SQLException throwables) {throwables.printStackTrace();}finally {if(rs!=null)try {rs.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {rs=null;}if(ps!=null)try {ps.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {ps=null;}if(conn!=null)try {conn.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {conn=null;}}}
}

c3p0.properties

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mydb
username=root
password=123456

事务

概念

就是一个操作可以分为多个单元,这些单元要么全部成功要么全部失败

操作

1.mysql客户端可以使用事务

START TRANSACTION;
UPDATE USER SET money=money+100 WHERE NAME='a';
UPDATE USER SET money=money-100 WHERE NAME='b';# COMMIT;# 提交事务
ROLLBACK;# 回滚事务,执行会使事务全部失败。撤销回原有的内容
  • 在开启事务之后,事务提交或者回滚都是位于一次事务中,这些sql语句同时成功或者失败
  • 数据库默认会为每一条sql语句执行事务

通过JDBC控制事务

conn.setAutoCommit(boolean false);设置是否提交,true是自动提交,如果是false则不会自动提交,则后面的sql语句都是在一次事务中
conn.commit();提交事务
conn.rollback();回滚事务
conn.setSavepo int()设置保存点,保存点可以在事务回滚的时候使用
import com.mchange.v2.c3p0.ComboPooledDataSource;import java.sql.*;public class transDemo {public static void main(String[] args) {Connection conn = null;PreparedStatement ps = null;Savepoint sp=null;ComboPooledDataSource source = new ComboPooledDataSource();try {conn = source.getConnection();//开启事务,默认的布尔值为true代表自动提交每一条sql语句conn.setAutoCommit(false);//不会自动提交每一条sql语句当前语句以及以后的sql语句都在一次事务中ps = conn.prepareStatement("update user set money=money-100 where name=?");ps.setString(1, "a");//第一个?给值aps.executeUpdate();//添加保存点sp = conn.setSavepoint();ps = conn.prepareStatement("update user set money=money+100 where name=?");ps.setString(1, "b");//第一个?给值aps.executeUpdate();conn.commit();} catch (SQLException e) {e.printStackTrace();try {if (conn != null) {//判断保存对象是否为空if(sp!=null){conn.rollback(sp);//继续提交到滚点之前的代码conn.commit();}else {//没有保存点就全部回滚conn.rollback();}conn.rollback();}} catch (SQLException throwables) {throwables.printStackTrace();}} finally {if (ps == null) {try {ps.close();} catch (SQLException throwables) {throwables.printStackTrace();} finally {ps = null;}}if (conn == null) {try {conn.close();} catch (SQLException throwables) {throwables.printStackTrace();} finally {conn = null;}}}}
}

事务的四大特性(ACID)

原子性(Atomicity)
事务本身是一个不可分割的单位,在一个事务中可以分为多个单元。

一致性(Consistency)
在事务中可以分为多个单元,这些单元要么全部成功,要么全部失败

隔离性(Isolation)
事务存在线程安全问题,隔离性是为了解决线程安全问题提出的一个特性

持久性(Durability)
一个事务一旦提交,他对数据库中数据的改变就应该是永久性的。当事务提交之后,过程不可逆,任何操作都无法改变。

以上特性中有三个数据库服务器默认提供的(原子性、一致性、持久性)

隔离性需要程序员来指定,隔离性本质是为了解决事务线程安全问题,但是导致线程安全问题的原因不一致,我们针对线程安全问题的隔离性就不一样。

设计隔离性

两个线程同时读取数据是否会出现线程安全问题?

不会

一个事务读同时另一个事务写是否出现线程安全问题?

有可能导致线程安全问题

两个事务同时写是否出现线程安全问题?

一定会出现线程安全问题
在一个线程修改一个线程查询的情况下可能产生的问题?

  • 不可重复读:一个事务读取到另一个已经提交的事务的结果,导致事务提交前后数据读取不一致。
  • 脏读:.一个事务读取到另一个事务未提交的数据,造成数据混乱产生的问题
  • 虚读、幻读:一个事务读取到另一个事务已经提交的数据。只在读写整表数据时产生。虚读/幻读并不是每次都会产生,有可能会发生,也有可能不发生。

如果一个线程修改 一个线程查询 可能会产生 脏读 不可重复读 虚读/幻读问题 而这些问题有些场景下是问题 有些场景下不是问题。而想要防止的问题越多,对数据库性能的影响就越大,所以到底要将数据库设置到何种状态来防止哪类问题不应该在数据库中写死,而应该提供相应的选项让数据库的使用者根据不同的场景灵活的选择 == 本质上是对 数据的可靠性 和 数据库的效率 之间的选择。==

提升数据的隔离级别可以提升数据的安全性,保证数据的正确但是由于提升隔离级别底层是通过加锁来实现的,所以安全性越高带来的问题就是数据操作的效率越低。

所以最终数据库设计者在设计数据库时,对于并发的查询没有做隔离,对于并发的修改做了严格的隔离,对于并发的读和写提供了相应的选项允许数据库的使用者选择,来根据需求防止不同的问题,这些选项就称之为数据库的隔离级别。

数据库的隔离级别

隔离级别是基于客户端来讨论的,不同的客户端在和服务器交互式可以有不同的隔离级别,客户端处在什么隔离级别就具有什么隔离级别的问题。

如何设置隔离级别?
set global/session transaction islation level 隔离级别
global:给数据库服务器来设置隔离级别,需要把当前窗口关闭,以后所有的窗口去连接数据库服务器隔离级别都能生效。
session:给当前窗口设置隔离级别,只在当前窗口生效,关闭窗口下次生效的还是数据库的隔离级别。
select @@tx_isolation:查看当前的隔离级别
数据库中的锁:
数据库隔离性的实现是由加锁来实现的。数据库中锁有很多的分类,比如查询时可以添加共享锁,更新时可以加上排他锁。甚至还有表级锁和行级锁。

隔离的四个级别

read uncommitted读未提交 不做任何隔离。可能产生脏读 不可重复读 虚读/幻读问题.性能最好
read committed读已提交 一个事务可以读取到另一个事务已经提交的数据。可以防止脏读,但可能存在不可重复读 虚读/幻读 问题。性能较好。
repeatable read可重复读取 在查询整表数据时,一个事务可以读取到另一个事务已经提交的数据。可以防止脏读 不可重复读问题,但可能存在虚读/幻读问题。mysql默认采用此隔离级别。性能一般。
serializable序列化 通过锁进行严格隔离,对同一个数据的访问要串行化进行。可以防止脏读 不可重复读 虚读/幻读 问题。但数据库处于串行化状态,效率极其低下。性能最差。

选择隔离级别的原则

从可靠性角度:
serializable > repeatable read -> read committed -> read uncommitted
从性能角度:
read uncommitted -> read committed -> repeatable read ->serializable

数据库中的锁机制

a.共享锁、排他锁

  • 在serializable级别之下,查询添加共享锁

  • 在非serializable级别之下,查询不添加锁

  • 在任意隔离级别下,更新添加排他锁、

  • 共享锁和共享锁可以共存

  • 共享锁和排他锁不能共存

  • 排他锁和排他锁不能共存
    正是利用了这种锁机制,数据库保证了并发的读不隔离,并发的写一定隔离,并发的读写在某一方或多方为Serializable的级别时,实现串行化,保证完全可靠。

死锁:

多个客户端都是Serializable的级别下,先查询再修改,可能会进入互相等待状态,其实就是发生了死锁,mysql会检测到死锁,自动退出一方以打断死锁。
1.操作过程:
两个窗口都是 serializable
两个窗口分别执行一次查询操作。各自创建一个共享锁。
两个窗口分别执行一次修改操作,会再各自创建一个排他锁,两个锁相互等待对方结束事务,构成死锁。
2.死锁的解决:
a.检测死锁,并修改代码,避免死锁。
b.杀死死锁中任意一个,让死锁代码开始执行,从而解决死锁的问题。

行级锁、表级锁:

数据库的锁根据锁定的粒度可以分为行级锁和表级锁。行级锁锁一行。表级锁锁整表。数据库自动根据操作的数据决定加哪种粒度的锁。

2021年7月29日------JDBC

JDBC

  • 一、JDBC概述
  • 二、实现jdbc
    • 1.statement
      • 功能讲解
      • JDBCUtils工具类
      • 登录操作
    • 2.sql注入攻击
      • 1).sql注入攻击
      • 2).PreparedStatement原理
    • 3.PreparedStatement简介
      • 1.PreparedStatement
    • 4.JDBC进行批处理
      • 第一种方式:
      • 第二种方式:
  • 连接池
    • 开源连接池:
      • DBCP
      • C3p0
    • 事务
      • 概念
      • 操作
      • 通过JDBC控制事务
      • 事务的四大特性(ACID)
      • 设计隔离性
      • 数据库的隔离级别
      • 选择隔离级别的原则
    • 数据库中的锁机制
      • a.共享锁、排他锁
      • 死锁:
      • 行级锁、表级锁:

一、JDBC概述

1.驱动

为了能让程序员利用java程序操作数据库,数据库厂商提供了一套jar包,通过导入这个jar包就可以直接调用其中的方法操作数据库,这个jar包称之为驱动。

2.JDBC简介:
JDBC全称为:Java DataBase Connectivity(java数据库连接),它主要由接口组成。
组成JDBC的2个包:
java.sql包 javax.sql包
开发JDBC应用需要以上2个包的支持外,还需要导入相应JDBC的数据库实现(即数据库驱动)。
不仅需要jdbc接口,还需要驱动这个实现,驱动中就是对jdbc接口的一些实现。

二、实现jdbc

1.statement

o 注册数据库驱动
o 获取数据库连接
o 创建传输器
o 传输sql并返回结果
o 遍历结果
o 关闭资源

package com.pcy.jdbc;import java.sql.*;public class jdbcDemo {public static void main(String[] args) {Connection conn=null;Statement statement=null;ResultSet resultSet=null;try {//1.注册数据库驱动//Driver 里的静态代码块注册一次数据库驱动,自己手写注册数据库驱动一共两次
// 第一种方式           DriverManager.registerDriver(new Driver());//使用反射来加载类只会注册数据库驱动一次Class.forName("com.mysql.jdbc.Driver");//2.获取数据库连接
//            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?user=root&password=123456");conn=DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb","root","123456");//3.创建传输器statement = conn.createStatement();//4.传输sql语句并且返回结果,获取行标resultSet = statement.executeQuery("select * from em");//5.遍历结果while (resultSet.next()){//获取每行的数据int id = resultSet.getInt(1);//根据列的下标来获取内容String name = resultSet.getString(2);String s = resultSet.getString("gender");//根据列的下标来获取内容System.out.println(id+":"+name+";性别:"+s);}} catch (SQLException | ClassNotFoundException throwables) {throwables.printStackTrace();}finally {if (resultSet != null){//6.关闭资源try {resultSet.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {resultSet=null;}}if (resultSet != null){//关闭资源try {statement.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {statement=null;}}if (conn != null){//关闭资源try {resultSet.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {resultSet=null;}try {statement.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {statement=null;}try {conn.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {conn=null;}}}}
}

功能讲解

1.DriverManager
Jdbc程序中的DriverManager用于加载驱动,并创建与数据库的链接,这个API的常用方法:
DriverManager.registerDriver(new Driver())
DriverManager.getConnection(url, user, password)
注意:在实际开发中并不推荐采用registerDriver方法注册驱动。
原因有二:
1.查看Driver的源代码可以看到,如果采用此种方式,会导致驱动程序注册两次,也就是在内存中会有两个Driver对象。
2.程序依赖mysql的api,脱离mysql的jar包,程序将无法编译,将来程序切换底层数据库将会非常麻烦。
推荐方式:Class.forName(“com.mysql.jdbc.Driver”);
o采用此种方式不会导致驱动对象在内存中重复出现,并且采用此种方式,程序仅仅只需要一个字符串,不需要依赖具体的驱动,使程序的灵活性更高。
o同样,在开发中也不建议采用具体的驱动类型指向getConnection方法返回的connection对象。

2.数据库URL

URL用于标识数据库的位置,程序员通过URL地址告诉JDBC程序连接哪个数据库,URL的写法为:
jdbc:mysql://localhost:3306/test?参数名=参数值

3.常用数据库URL地址的写法:

Oracle写法:jdbc:oracle:thin:@localhost:1521:sid
SqlServer—jdbc:microsoft:sqlserver://localhost:1433; DatabaseName=sid
MySql—jdbc:mysql://localhost:3306/sid
Mysql的url地址的简写形式: jdbc:mysql:///sid
常用属性:useUnicode=true&characterEncoding=UTF-8

4.Connection

oJdbc程序中的Connection,它用于代表数据库的链接,Connection是数据库编程中最重要的一个对象,客户端与数据库所有交互都是通过connection对象完成的,这个对象的常用方法:

createStatement():创建向数据库发送sql的statement对象。
prepareStatement(sql) :创建向数据库发送预编译sql的PrepareSatement对象。
prepareCall(sql):创建执行存储过程的callableStatement对象。
setAutoCommit(boolean autoCommit):设置事务是否自动提交。
commit() :在链接上提交事务。
rollback() :在此链接上回滚事务。

5.Statement
oJdbc程序中的Statement对象用于向数据库发送SQL语句,
Statement对象常用方法:

executeQuery(String sql) :用于向数据发送查询语句。
executeUpdate(String sql):用于向数据库发送insert、update或delete语句
execute(String sql):用于向数据库发送任意sql语句
addBatch(String sql) :把多条sql语句放到一个批处理中。
executeBatch():向数据库发送一批sql语句执行。

6.ResultSet
Jdbc程序中的ResultSet用于代表Sql语句的执行结果。Resultset封装执行结果时,采用的类似于表格的方式。ResultSet 对象维护了一个指向表格数据行的游标,初始的时候,游标在第一行之前,
调用ResultSet.next() 方法,可以使游标指向具体的数据行,进行调用方法获取该行的数据。
ResultSet既然用于封装执行结果的,所以该对象提供的都是用于获取数据的get方法:

== 提问:数据库中列的类型是varchar,获取该列的数据调用什么方法?Int类型呢?bigInt类型呢?Boolean类型?==

7.ResultSet中的api
ResultSet还提供了对结果集进行滚动的方法:

next():移动到下一行
Previous():移动到前一行
absolute(int row):移动到指定行
beforeFirst():移动resultSet的最前面。
afterLast() :移动到resultSet的最后面

8.为什么要关闭资源?
在安装数据库的时候,设置过最大连接数量,如果用了不还连接,别人就无法使用了。

o rs对象中可能包含很大的一个数据,对象保存在内存中,这样就十分占用内存。需要将他关闭。
最晚创建的对象,最先关闭。
o Jdbc程序运行完后,切记要释放程序在运行过程中,创建的那些与数据库进行交互的对象,这些对象通常是ResultSet, Statement和Connection对象。
o 特别是Connection对象,它是非常稀有的资源,用完后必须马上释放,如果Connection不能及时、正确的关闭,极易导致系统宕机。Connection的使用原则是尽量晚创建,尽量早的释放。
o 为确保资源释放代码能运行,资源释放代码也一定要放在finally语句中。

释放资源:
o 在关闭过程中可能会出现异常,为了能够关闭资源,需要将资源在finally中关闭。
o 如果在finally中关闭资源则需要将conn,stat,rs三个对象定义成全局的变量。
o 在conn,stat,rs三个变量出现异常的时候可能会关闭不成功,我们需要将他们在finally中置为null。conn,stat,rs这三个对象是引用,将引用置为null,它引用的对象就会被JVM回收,也能保证资源的释放。

增加一条数据

package com.pcy.jdbc;import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;public class jdbcDemo2 {public static void main(String[] args) {Connection conn=null;Statement state=null;//注册数据库驱动try {Class.forName("com.mysql.jdbc.Driver");//连接数据库conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "root", "123456");//创建传输器state = conn.createStatement();//传入mysql并且返回影响的行数int i = state.executeUpdate("insert into emp value(8,'pan',2)");if (i>0){System.out.println("插入成功:"+i);}else {System.out.println("插入失败");}} catch (ClassNotFoundException e) {e.printStackTrace();} catch (SQLException throwables) {throwables.printStackTrace();} finally {if (state!=null){try {state.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {state =null;}}if (conn!=null){try {conn.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {conn =null;}}}}
}

JDBCUtils工具类

注意:这里的database.properties必须写在src目录下,否则不识别

package com.pcy.jdbc.utils;import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;public class JDBCUtils {//私有化构造方法private JDBCUtils(){}//创建Properties类对象//静态属性(对象)---全局统一---只需要创建一次对象就可以读取配置内容private static Properties p=new Properties();//静态代码块---保证先读取配置文件内容static {//JDBCUtils.class---获取当前类的字节码对象//getClassLoader()---获取类加载器//getResource("src目录下的文件名称")---获取当前工程的src目录//getPath()---把URL类型转换成String类型try {p.load(new FileInputStream(new File(JDBCUtils.class.getClassLoader().getResource("database.properties").getPath())));} catch (IOException e) {e.printStackTrace();}}//定义静态方法---注册驱动以及获取数据库连接public static Connection getConnection() throws ClassNotFoundException, SQLException {//注册数据库驱动//获取配置文件的driver键对应值Class.forName(p.getProperty("driver"));//获取数据库连接///通过配置文件的键对来获取对应值return DriverManager.getConnection(p.getProperty("url"),p.getProperty("username"),p.getProperty("password"));}//定义静态方法---关闭资源public static void close(Connection conn, Statement stat, ResultSet rs){if(rs!=null)try {rs.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {rs=null;}if(stat!=null)try {stat.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {stat=null;}if(conn!=null)try {conn.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {conn=null;}}
}

database.properties

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mydb
username=root
password=123456

工具类的作用

在程序中,大量代码重复导致代码复用性低,工作效率低下,不美观。为了提供代码复用性,我们将创建连接和关闭连接提取到工具类中,方便以后调用,提升工作效率,增强代码复用性。

登录操作

public class LoginJdbcDemo {public static void main(String[] args) {//获取控制台输入的用户名和密码Scanner sc=new Scanner(System.in);System.out.println("请输入用户名:");int id = sc.nextInt();System.out.println("请输入密码:");//这里输入nextLine就会自动结束String name=sc.next();login(id,name);}public static void login(int id,String name){Connection conn=null;Statement state=null;ResultSet rs=null;try {//创建连接conn = JDBCUtils.getConnection();//创建一个传输器state = conn.createStatement();//传入sql语句rs = (ResultSet) state.executeQuery("select * from dept where id='" +id+ "'and name='" +name+ "'");if (rs.next()){System.out.println("查询成功");}else {System.out.println("用户不存在");}} catch (ClassNotFoundException e) {e.printStackTrace();} catch (SQLException throwables) {throwables.printStackTrace();}finally {JDBCUtils.close(conn,state,rs);}}
}

2.sql注入攻击

1).sql注入攻击

在网页中输入’#,或者’ or '1=1 就能够直接登录。

select * from user where name =  'name' and password = 'password';
select * from user where name='name'#' and password='password'

由于执行的sql语句是在后台拼接出来的,其中有一部分内容是由用户从客户端传入,所以当用户传入数据中包含sql关键字时,就有可能通过这些关键字改变sql语句的语义,从而执行一些特殊的操作,这样的攻击方式就叫做sql注入攻击。

2).PreparedStatement原理

PreparedStatement利用预编译的机制将sql语句的主干和参数分别传输给数据库服务器,从而使数据库可以分辨出哪些是sql语句的主干,哪些是参数,这样一来即使参数中带了sql的关键字,数据库服务器也仅仅将他当做参数值使用,关键字不会起作用,从而从原理上防止了sql注入的问题。

3.PreparedStatement简介

1.PreparedStatement

o PreperedStatement是Statement的孩子,它的实例对象可以通过调用Connection.preparedStatement()方法获得,相对于Statement对象而言:

o PreperedStatement可以避免SQL注入的问题。

o Statement会使数据库频繁编译SQL,可能造成数据库缓冲区溢出。PreparedStatement 可对SQL进行预编译,从而提高数据库的执行效率。

o 并且PreperedStatement对于sql中的参数,允许使用占位符的形式进行替换,简化sql语句的编写。

package com.pcy.jdbc;import com.pcy.jdbc.utils.JDBCUtils;import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;public class PrepareStatementDemo {public static void main(String[] args) {Connection conn=null;PreparedStatement ps=null;ResultSet rs=null;try {conn= JDBCUtils.getConnection();ps = conn.prepareStatement("select * from  dept where id=? and name=?");ps.setInt(1,1);ps.setString(2,"财务部");rs = ps.executeQuery();if (rs.next()){System.out.println("登录成功");}} catch (ClassNotFoundException e) {e.printStackTrace();} catch (SQLException throwables) {throwables.printStackTrace();}finally {JDBCUtils.close(conn,ps,rs);}}
}

4.JDBC进行批处理

使用业务场景:
当需要向数据库发送一批SQL语句执行时,应避免向数据库一条条的发送执行,而应采用JDBC的批处理机制,以提升执行效率。
o实现批处理有两种方式,

第一种方式:

Statement.addBatch(sql)
执行批处理SQL语句
executeBatch()方法:执行批处理命令
clearBatch()方法:清除批处理命令

public class BatchDemo {public static void main(String[] args) {Connection conn=null;Statement state=null;ResultSet rs=null;try {conn=JDBCUtils.getConnection();/*(1)如果是批量新增或修改操作,SQL语句要是insert into 或 update 语句(2)如果是批量查询操作,SQL是select语句,但是不能调用batchUpdate方法*/String sql1="insert into dept value(6,'小卖部'),(7,'部长')";String sql2="update dept set name ='部长' where id=1";state = conn.createStatement();
//            state.addBatch(sql1);
//            state.addBatch(sql2);state.addBatch(sql1);state.addBatch(sql2);state.executeBatch();} catch (ClassNotFoundException e) {e.printStackTrace();} catch (SQLException throwables) {throwables.printStackTrace();}finally {//执行关闭JDBCUtils.close(conn,state,rs);}}
}

o采用Statement.addBatch(sql)方式实现批处理:

优点:可以执行多条不同结构的sql语句
缺点:没有使用预编译机制,效率低下,如果要执行多条结构相同仅仅参数不同的语句时,仍然需要多次写sql语句的主干。

第二种方式:

package com.pcy.jdbc;import com.pcy.jdbc.utils.JDBCUtils;import java.sql.*;public class BatchDemo1 {public static void main(String[] args) {Connection conn=null;PreparedStatement ps=null;try {conn=JDBCUtils.getConnection();ps = conn.prepareStatement("select * from dept where id=?");for (int i=0;i<1000;i++){ps.setString(1,""+i);if (i%10==0){ps.executeBatch();ps.clearBatch();}ps.executeBatch();}} catch (ClassNotFoundException e) {e.printStackTrace();} catch (SQLException throwables) {throwables.printStackTrace();}finally {//执行关闭JDBCUtils.close(conn,ps,null);}}
}

PreparedStatement批处理:
o采用ps.addBatch(sql)方式实现批处理:
优点:有预编译机制,效率比较高.执行多条结构相同,参数不同的sql时,不需要重复写sql的主干
缺点:只能执行主干相同参数不同的sql,没有办法在一个批中加入结构不同的sql

连接池

为什么使用连接池
缺点:
不使用连接池的时候
用户每次请求都需要向数据库获得链接,而数据库创建连接通常需要消耗相对较大的资源,创建时间也较长。假设网站一天10万访问量,数据库服务器就需要创建10万次连接,极大的浪费数据库的资源,并且极易造成数据库服务器内存溢出、宕机。
o它是将那些已连接的数据库连接存放在一个容器里(连接池),这样以后别人要连接数据库的时候,将不会重新建立数据库连接,会直接从连接池里取出可用的连接,用户使用完毕后,连接又重新还回到连接池中。
o注意:连接池里的连接将会一直保存在内存里,即使你没用也是一样。所以这个时候你得权衡一下连接池的连接数量了。

开源连接池:

现在很多WEB服务器(Weblogic, WebSphere, Tomcat)都提供了DataSoruce的实现,即连接池的实现。通常我们把DataSource的实现,按其英文含义称之为数据源,数据源中都包含了数据库连接池的实现。
也有一些开源组织提供了数据源的独立实现:
DBCP 数据库连接池
C3P0 数据库连接池
实际应用时不需要编写连接数据库代码,直接从数据源获得数据库的连接。程序员编程时也应尽量使用这些数据源的实现,以提升程序的数据库访问性能。

DBCP

DBCP 是 Apache 软件基金组织下的开源连接池实现,使用DBCP数据源,应用程序应在系统中增加如下两个 jar 文件:
Commons-dbcp.jar:连接池的实现
Commons-pool.jar:连接池实现的依赖库
#初始化连接
initialSize=10

#最大连接数量
maxActive=50

#最大空闲连接
maxIdle=20

#最小空闲连接
minIdle=5

#超时等待时间以毫秒为单位 6000毫秒/1000等于60秒
maxWait=60000

package com.pcy.jdbc;import org.apache.commons.dbcp.BasicDataSourceFactory;import javax.sql.DataSource;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;public class DbcpDemo {public static void main(String[] args) {Connection conn=null;PreparedStatement ps=null;ResultSet rs=null;Properties p=new Properties();try {p.load(new FileInputStream(new File(DbcpDemo.class.getClassLoader().getResource("dbcp.properties").getPath())));//创建工厂对象BasicDataSourceFactory factory=new BasicDataSourceFactory();DataSource dataSource = factory.createDataSource(p);conn = dataSource.getConnection();ps = conn.prepareStatement("select * from dept");rs = ps.executeQuery();if (rs.next()){System.out.println(rs.getString(1));System.out.println(rs.getString(2));}} catch (IOException e) {e.printStackTrace();} catch (Exception e) {e.printStackTrace();}finally {if(rs!=null)try {rs.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {rs=null;}if(ps!=null)try {ps.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {ps=null;}if(conn!=null)try {//归还连接conn.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {conn=null;}}}
}

dbcp.properties

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mydb
username=root
password=123456

C3p0

注意:配置文件必须是c3p0.properties

package com.pcy.jdbc;import com.mchange.v2.c3p0.ComboPooledDataSource;
import com.mchange.v2.c3p0.impl.C3P0ImplUtils;
import com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool;
import com.mchange.v2.c3p0.jboss.C3P0PooledDataSource;import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;public class C3p0Demo {public static void main(String[] args) {Connection conn=null;PreparedStatement ps=null;ResultSet rs=null;//new一个连接池对象ComboPooledDataSource source = new ComboPooledDataSource();try {conn = source.getConnection();ps = conn.prepareStatement("select * from dept");rs = ps.executeQuery();if (rs.next()){System.out.println(rs.getString(1));System.out.println(rs.getString(2));}} catch (SQLException throwables) {throwables.printStackTrace();}finally {if(rs!=null)try {rs.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {rs=null;}if(ps!=null)try {ps.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {ps=null;}if(conn!=null)try {conn.close();} catch (SQLException throwables) {throwables.printStackTrace();}finally {conn=null;}}}
}

c3p0.properties

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mydb
username=root
password=123456

事务

概念

就是一个操作可以分为多个单元,这些单元要么全部成功要么全部失败

操作

1.mysql客户端可以使用事务

START TRANSACTION;
UPDATE USER SET money=money+100 WHERE NAME='a';
UPDATE USER SET money=money-100 WHERE NAME='b';# COMMIT;# 提交事务
ROLLBACK;# 回滚事务,执行会使事务全部失败。撤销回原有的内容
  • 在开启事务之后,事务提交或者回滚都是位于一次事务中,这些sql语句同时成功或者失败
  • 数据库默认会为每一条sql语句执行事务

通过JDBC控制事务

conn.setAutoCommit(boolean false);设置是否提交,true是自动提交,如果是false则不会自动提交,则后面的sql语句都是在一次事务中
conn.commit();提交事务
conn.rollback();回滚事务
conn.setSavepo int()设置保存点,保存点可以在事务回滚的时候使用
import com.mchange.v2.c3p0.ComboPooledDataSource;import java.sql.*;public class transDemo {public static void main(String[] args) {Connection conn = null;PreparedStatement ps = null;Savepoint sp=null;ComboPooledDataSource source = new ComboPooledDataSource();try {conn = source.getConnection();//开启事务,默认的布尔值为true代表自动提交每一条sql语句conn.setAutoCommit(false);//不会自动提交每一条sql语句当前语句以及以后的sql语句都在一次事务中ps = conn.prepareStatement("update user set money=money-100 where name=?");ps.setString(1, "a");//第一个?给值aps.executeUpdate();//添加保存点sp = conn.setSavepoint();ps = conn.prepareStatement("update user set money=money+100 where name=?");ps.setString(1, "b");//第一个?给值aps.executeUpdate();conn.commit();} catch (SQLException e) {e.printStackTrace();try {if (conn != null) {//判断保存对象是否为空if(sp!=null){conn.rollback(sp);//继续提交到滚点之前的代码conn.commit();}else {//没有保存点就全部回滚conn.rollback();}conn.rollback();}} catch (SQLException throwables) {throwables.printStackTrace();}} finally {if (ps == null) {try {ps.close();} catch (SQLException throwables) {throwables.printStackTrace();} finally {ps = null;}}if (conn == null) {try {conn.close();} catch (SQLException throwables) {throwables.printStackTrace();} finally {conn = null;}}}}
}

事务的四大特性(ACID)

原子性(Atomicity)
事务本身是一个不可分割的单位,在一个事务中可以分为多个单元。

一致性(Consistency)
在事务中可以分为多个单元,这些单元要么全部成功,要么全部失败

隔离性(Isolation)
事务存在线程安全问题,隔离性是为了解决线程安全问题提出的一个特性

持久性(Durability)
一个事务一旦提交,他对数据库中数据的改变就应该是永久性的。当事务提交之后,过程不可逆,任何操作都无法改变。

以上特性中有三个数据库服务器默认提供的(原子性、一致性、持久性)

隔离性需要程序员来指定,隔离性本质是为了解决事务线程安全问题,但是导致线程安全问题的原因不一致,我们针对线程安全问题的隔离性就不一样。

设计隔离性

两个线程同时读取数据是否会出现线程安全问题?

不会

一个事务读同时另一个事务写是否出现线程安全问题?

有可能导致线程安全问题

两个事务同时写是否出现线程安全问题?

一定会出现线程安全问题
在一个线程修改一个线程查询的情况下可能产生的问题?

  • 不可重复读:一个事务读取到另一个已经提交的事务的结果,导致事务提交前后数据读取不一致。
  • 脏读:.一个事务读取到另一个事务未提交的数据,造成数据混乱产生的问题
  • 虚读、幻读:一个事务读取到另一个事务已经提交的数据。只在读写整表数据时产生。虚读/幻读并不是每次都会产生,有可能会发生,也有可能不发生。

如果一个线程修改 一个线程查询 可能会产生 脏读 不可重复读 虚读/幻读问题 而这些问题有些场景下是问题 有些场景下不是问题。而想要防止的问题越多,对数据库性能的影响就越大,所以到底要将数据库设置到何种状态来防止哪类问题不应该在数据库中写死,而应该提供相应的选项让数据库的使用者根据不同的场景灵活的选择 == 本质上是对 数据的可靠性 和 数据库的效率 之间的选择。==

提升数据的隔离级别可以提升数据的安全性,保证数据的正确但是由于提升隔离级别底层是通过加锁来实现的,所以安全性越高带来的问题就是数据操作的效率越低。

所以最终数据库设计者在设计数据库时,对于并发的查询没有做隔离,对于并发的修改做了严格的隔离,对于并发的读和写提供了相应的选项允许数据库的使用者选择,来根据需求防止不同的问题,这些选项就称之为数据库的隔离级别。

数据库的隔离级别

隔离级别是基于客户端来讨论的,不同的客户端在和服务器交互式可以有不同的隔离级别,客户端处在什么隔离级别就具有什么隔离级别的问题。

如何设置隔离级别?
set global/session transaction islation level 隔离级别
global:给数据库服务器来设置隔离级别,需要把当前窗口关闭,以后所有的窗口去连接数据库服务器隔离级别都能生效。
session:给当前窗口设置隔离级别,只在当前窗口生效,关闭窗口下次生效的还是数据库的隔离级别。
select @@tx_isolation:查看当前的隔离级别
数据库中的锁:
数据库隔离性的实现是由加锁来实现的。数据库中锁有很多的分类,比如查询时可以添加共享锁,更新时可以加上排他锁。甚至还有表级锁和行级锁。

隔离的四个级别

read uncommitted读未提交 不做任何隔离。可能产生脏读 不可重复读 虚读/幻读问题.性能最好
read committed读已提交 一个事务可以读取到另一个事务已经提交的数据。可以防止脏读,但可能存在不可重复读 虚读/幻读 问题。性能较好。
repeatable read可重复读取 在查询整表数据时,一个事务可以读取到另一个事务已经提交的数据。可以防止脏读 不可重复读问题,但可能存在虚读/幻读问题。mysql默认采用此隔离级别。性能一般。
serializable序列化 通过锁进行严格隔离,对同一个数据的访问要串行化进行。可以防止脏读 不可重复读 虚读/幻读 问题。但数据库处于串行化状态,效率极其低下。性能最差。

选择隔离级别的原则

从可靠性角度:
serializable > repeatable read -> read committed -> read uncommitted
从性能角度:
read uncommitted -> read committed -> repeatable read ->serializable

数据库中的锁机制

a.共享锁、排他锁

  • 在serializable级别之下,查询添加共享锁

  • 在非serializable级别之下,查询不添加锁

  • 在任意隔离级别下,更新添加排他锁、

  • 共享锁和共享锁可以共存

  • 共享锁和排他锁不能共存

  • 排他锁和排他锁不能共存
    正是利用了这种锁机制,数据库保证了并发的读不隔离,并发的写一定隔离,并发的读写在某一方或多方为Serializable的级别时,实现串行化,保证完全可靠。

死锁:

多个客户端都是Serializable的级别下,先查询再修改,可能会进入互相等待状态,其实就是发生了死锁,mysql会检测到死锁,自动退出一方以打断死锁。
1.操作过程:
两个窗口都是 serializable
两个窗口分别执行一次查询操作。各自创建一个共享锁。
两个窗口分别执行一次修改操作,会再各自创建一个排他锁,两个锁相互等待对方结束事务,构成死锁。
2.死锁的解决:
a.检测死锁,并修改代码,避免死锁。
b.杀死死锁中任意一个,让死锁代码开始执行,从而解决死锁的问题。

行级锁、表级锁:

数据库的锁根据锁定的粒度可以分为行级锁和表级锁。行级锁锁一行。表级锁锁整表。数据库自动根据操作的数据决定加哪种粒度的锁。