數據庫相關知識
事務
1. 什麼是事務?
爲了完成某個業務而對數據庫進行一系列操作,這些操作要麼全部成功,要麼全部失敗。
2. 事務的四個特性(ACID)?
- 原子性(Atomicity)
- 事務所涉及的各個操作要麼全部成功,要麼全部失敗
- 一致性(Consistency)
- 事務結束之後,不能夠有非法的數據寫入到數據庫
- 隔離性(lsolation)
- 多個非事務可以同時進行,能一定程度上保證互不影響
- 持久性(Durability)
- 事務完成之後,數據一定會存放在數據庫裏面
3. 隔離級別?
-
讀未提交
- 一個事務可以讀取另外一個事務尚未提交的數據,可能會產生
髒讀
,不可重複讀取
,幻影讀取
問題
- 一個事務可以讀取另外一個事務尚未提交的數據,可能會產生
-
讀已提交
- 一個事務只能讀取另外一個事務已經提交的數據。該隔離級別解決了
髒讀
問題,但是有可能會產生不可重複讀取
和幻影讀取
問題
- 一個事務只能讀取另外一個事務已經提交的數據。該隔離級別解決了
-
可重複讀取
- 在同一個事務當中,多次讀取同一份數據,結果是一致的。該隔離級別解決了
髒讀
,不可重複讀取
,但是仍然有可能產生幻影讀取
問題
- 在同一個事務當中,多次讀取同一份數據,結果是一致的。該隔離級別解決了
-
序列化(串行化)
- 讓事務一個一個串行執行,解決了
髒讀
,不可重複讀取
和幻影讀取
問題
從上往下,隔離級別越來越高;
隔離級別越高,性能越低
- 讓事務一個一個串行執行,解決了
視圖
1. 什麼是視圖?
在已有表或者視圖上創建的虛擬表(存在內存中的)
2. 創建視圖
create view 視圖名(字段列表) as select 語句;
# 如
create view v_emp as select * from t_emp;
create view v_emp2(ename,salary) as select * from t_emp;
--視圖也可以是基於多張表
create view v_teacher_course(cname,tname,level) as select
c.name,t.name,t.level
from t_course c join t_teacher t on c.teacher_id = t.id;
3. 刪除視圖
drop view 視圖名;
4. 視圖的優點
-
簡化:
可以將一些複雜的SQL(比如多張表的join查詢)的查詢結果作爲一個視圖,調用者不用關心這個複雜的查詢怎麼寫。
-
安全:
調用者只能查詢或者修改他們所見到的數據
-
邏輯獨立性:
可以屏蔽真實表結構變化帶來的影響
約束
1. 什麼是約束
約束是一種限制,它通過對錶的行或者列的數據做出限制,確保數據的完整性和一致性
2. 約束的種類
-
主鍵約束
- 要求主鍵值必須唯一且非空
- 一張表最多隻有一個主鍵約束
-
唯一性約束(unique)
-
唯一性約束不允許重複值,但是允許有多個null
-
一個表中可以有多個unique字段
-
create table t_dept( id int primary key, dept_name varchar(50) unique, loc varchar(100) ); create table t_dept( id int primary key, dept_name varchar(50), loc varchar(100), unique(dept_name) );
-
-
外鍵約束
-
示例:
外鍵約束
create table t_dept( id int primary key, dept_name varchar(50), loc varchar(100) ); create table t_staff( id int primary key auto_increment, sname varchar(50), age int, dept_id int, foreign key(dept_id) references t_dept(id) ); # 添加數據時,先添加主表中的數據 insert into t_dept values(100,'HR','location01'); insert into t_staff values(null,'張三',22,100); # 刪除數據時,要先刪除從表中的數據 delete from t_staff where id = 1; delete from t_dept where id = 100
-
-
非空約束(not null)
存儲過程
1. 什麼是數據庫的存儲過程?
存放在數據庫端的一系列SQL語句,用於完成某個業務功能。
2. 如何創建存儲過程
create procedure 存儲過程名(in/out/inout 參數 參數類型)
參數有三種類型:
in
:(默認的)輸入參數,該參數值在調用時需要指定,在存儲過程當中該參數值不能返回out
:輸出參數,該參數值可以被返回。inout
:輸入輸出參數。
3. 調用存儲過程
call 存儲過程名;
示例:
# 執行存儲過程
delimiter //
create procedure getEmp()
begin
select * from t_emp;
end
//
# 創建完存儲過程要將分號改回來
delimiter ;
# 調用存儲過程
call getEmp;
--------------------------
# 執行存儲過程(帶參數in)
delimiter //
create procedure getEmp2(in sid int)
begin
select * from t_emp where id = sid;
end
//
# 創建完存儲過程要將分號改回來
delimiter ;
# 調用存儲過程
call getEmp2(1);
--------------------------
# 執行存儲過程(帶參數out)是可以拿到返回值的
delimiter //
create procedure getEmp3(out max_value decimal(8,2))
begin
select max(salary) into max_value from t_emp;
end
//
# 創建完存儲過程要將分號改回來
delimiter ;
# 調用存儲過程
call getEmp3(@mv);
select @mv;
因爲在控制檯,“;”表示結束,這樣存儲過程就不完整了,所以用"delimiter //“將結束符號改爲”//"
delimiter;
表示創建完存儲過程要將分號改回來,跟以前一樣SQL命令以";"結尾
java操作存儲過程
package util;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DBUtil {
public static Connection getConnection() throws Exception{
Connection conn = null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/test","root","123456");
}catch(Exception e) {
e.printStackTrace();
throw e;
}
return conn;
}
/**
* 關閉連接
* @param conn
*/
public static void close(Connection conn) {
if(conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws Exception {
// 測試連接是否成功
System.out.println(getConnection());
}
}
package util;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Types;
/**
* 如何調用存儲過程
*/
public class ProcedureTest {
public static void main(String[] args) throws Exception {
test1();
}
/**
* 調用不帶參的存儲過程
* @throws Exception
*/
public static void test1() throws Exception{
Connection conn = null;
try {
conn = DBUtil.getConnection();
CallableStatement cs = conn.prepareCall("{call getEmp}");
ResultSet rs = cs.executeQuery();
while(rs.next()) {
int id = rs.getInt("id");
String ename = rs.getString("ename");
System.out.println("id:" + id + " ename:" + ename);
}
} catch (Exception e) {
e.printStackTrace();
throw e;
}finally {
DBUtil.close(conn);
}
}
/**
* 調用帶參的存儲過程
* @throws Exception
*/
public static void test2() throws Exception{
Connection conn = null;
try {
conn = DBUtil.getConnection();
// 一個參數一個問號
CallableStatement cs = conn.prepareCall("{call getEmp2(?)}");
cs.setInt(1, 1);
ResultSet rs = cs.executeQuery();
while(rs.next()) {
int id = rs.getInt("id");
String ename = rs.getString("ename");
System.out.println("id:" + id + " ename:" + ename);
}
} catch (Exception e) {
e.printStackTrace();
throw e;
}finally {
DBUtil.close(conn);
}
}
/**
* 調用帶參(out參數的)的存儲過程(能獲取返回值的)
* @throws Exception
*/
public static void test3() throws Exception{
Connection conn = null;
try {
conn = DBUtil.getConnection();
// 一個參數一個問號
CallableStatement cs = conn.prepareCall("{call getEmp3(?)}");
cs.registerOutParameter(1, Types.DECIMAL);
cs.execute();
System.out.println(cs.getBigDecimal(1));
} catch (Exception e) {
e.printStackTrace();
throw e;
}finally {
DBUtil.close(conn);
}
}
}
4. 存儲過程的優點:
- 減少應用程序和數據庫之間交互的次數,提升性能
- 將多個應用程序相同的邏輯集中寫在存儲過程裏面,可以共享一部分業務邏輯
5. 存儲過程的缺點
- 存儲過程依賴於特定的數據庫(不同數據庫存儲過程語法不同,不能移植)
- 存儲過程對於特別複雜的業務也不好寫。
6. 用存儲過程命令生成100萬條數據
prep_data.sql如下;
drop table if exists t_user;
create table t_user(
id int not null,
ename varchar(50),
email varchar(50)
);
drop procedure proc_insert;
DELIMITER //
CREATE PROCEDURE proc_insert()
BEGIN
DECLARE i INT DEFAULT 0;
SET autocommit = 0;
WHILE i<1000000 DO
insert into t_user values(i,concat('emp',i),concat('emp',i,'@126.com'));
SET i = i+1;
IF i%2000 = 0 THEN
COMMIT;
END IF;
END WHILE;
END; //
DELIMITER ;
call proc_insert;
數據庫的樂觀鎖和悲觀鎖是什麼?
數據庫管理系統(DBMS)中的併發控制的任務是確保在多個事務同時存取數據庫中同一數據時不破壞事務的隔離性和統一性以及數據庫的統一性。
樂觀併發控制(樂觀鎖)和悲觀併發控制(悲觀鎖)是併發控制主要採用的技術手段。
悲觀鎖:假定會發生併發衝突,屏蔽一切可能違反數據完整性的操作
悲觀鎖是一種利用數據庫內部機制提供的鎖的方式,也就是對更新的數據加鎖,這樣在併發期間一旦有一個事務持有了數據庫記錄的鎖,其他的線程將不能再對數據進行更新了,這就是悲觀鎖的實現方式。
MySQL InnoDB中使用悲觀鎖:
要使用悲觀鎖,我們必須關閉mysql數據庫的自動提交屬性,因爲MySQL默認使用autocommit模式,也就是說,當你執行一個更新操作後,MySQL會立刻將結果進行提交。 set autocommit=0;
//0.開始事務
begin;/begin work;/start transaction; (三者選一就可以)
//1.查詢出商品信息
select status from t_goods where id=1 for update;
//2.根據商品信息生成訂單
insert into t_orders (id,goods_id) values (null,1);
//3.修改商品status爲2
update t_goods set status=2;
//4.提交事務
commit;/commit work;
上面的查詢語句中,我們使用了 select…for update
的方式,這樣就通過開啓排他鎖的方式實現了悲觀鎖。此時在t_goods表中,id爲1的 那條數據就被我們鎖定了,其它的事務必須等本次事務提交之後才能執行。這樣我們可以保證當前的數據不會被其它事務修改。
上面我們提到,使用 select…for update
會把數據給鎖住,不過我們需要注意一些鎖的級別,MySQL InnoDB默認行級鎖。行級鎖都是基於索引的,如果一條SQL語句用不到索引是不會使用行級鎖的,會使用表級鎖把整張表鎖住,這點需要注意。
優點與不足:
悲觀併發控制實際上是“先取鎖再訪問”的保守策略,爲數據處理的安全提供了保證。但是在效率方面,處理加鎖的機制會讓數據庫產生額外的開銷,還有增加產生死鎖的機會;另外,在只讀型事務處理中由於不會產生衝突,也沒必要使用鎖,這樣做只能增加系統負載;還有會降低了並行性,一個事務如果鎖定了某行數據,其他事務就必須等待該事務處理完纔可以處理那行數
樂觀鎖:假設不會發生併發衝突,只在提交操作時檢查是否違反數據完整性。
樂觀鎖是一種不會阻塞其他線程併發的控制,它不會使用數據庫的鎖進行實現,它的設計裏面由於不阻塞其他線程,所以並不會引起線程頻繁掛起和恢復,這樣便能夠提高併發能力,所以也有人把它稱爲非阻塞鎖。一般的實現樂觀鎖的方式就是記錄數據版本。
數據版本,爲數據增加的一個版本標識。當讀取數據時,將版本標識的值一同讀出,數據每更新一次,同時對版本標識進行更新。當我們提交更新的時候,判斷數據庫表對應記錄的當前版本信息與第一次取出來的版本標識進行比對,如果數據庫表當前版本號與第一次取出來的版本標識值相等,則予以更新,否則認爲是過期數據。
實現數據版本有兩種方式,第一種是使用版本號,第二種是使用時間戳。
使用版本號實現樂觀鎖:
使用版本號時,可以在數據初始化時指定一個版本號,每次對數據的更新操作都對版本號執行+1操作。並判斷當前版本號是不是該數據的最新的版本號。
1.查詢出商品信息
select (status,status,version) from t_goods where id=#{id}
2.根據商品信息生成訂單
3.修改商品status爲2
update t_goods
set status=2,version=version+1
where id=#{id} and version=#{version};
優點與不足:
樂觀併發控制相信事務之間的數據競爭(data race)的概率是比較小的,因此儘可能直接做下去,直到提交的時候纔去鎖定,所以不會產生任何鎖和死鎖。但如果直接簡單這麼做,還是有可能會遇到不可預期的結果,例如兩個事務都讀取了數據庫的某一行,經過修改以後寫回數據庫,這時就遇到了問題。