DBMS对DB的保护分为以下几类:
①完整性控制(约束、触发器)
②安全性控制(访问控制)
③并发控制(事务与并发控制)
④DB的备份与恢复
第一节 数据库的完整性
数据在DB的正确性与相容性。
Data的完整性约束是为了防止DB中存在不符合语义的数据。
那些加在DB数据之上的语义约束就是数据库的完整性约束,而DBMS检查Data是否满足约束的机制称为完整性检查。
完整性约束作为DB关系模式定义的一部分,通过CREATE TABLE和ALTER TABLE来定义,定义好之后服务器随时进行检测。
一 完整性约束作用的对象
列、元组和表。
(1)列级约束
①数据类型
②数据格式
③取值范围
④空值约束(是否允许为NULL)
(2)元组约束
各字段之间的相互约束;例如:活动的开始日期不能大于结束日期。
(3)表级约束
若干元组之间、关系之间的约束;例如:成绩单中学生的ID必须是学生基础信息表中存在的学生ID。
二 定义与实现完整性约束
关系模型中的三类完整性约束:
①实体完整性约束
②参照完整性约束
③用户定义完整性约束
除了③中的触发器外,其他基本都是在定义表时完成。
1 实体完整性约束
通过主键或者候选键实现。
(1)主键约束
①每个表只有一个主键(可由多列构成)
②主键值必须唯一且不能为NULL
③复合主键不能包含不必要的列
④一个列名只能在主键中出现一次(废话)
⑤主键的实现方式1:想要设置为主键的属性后加关键字"PRIMARY KEY";这属于列级完整性约束,仅适合单列主键。
⑥主键的实现方式1:在所有属性后加关键字"PRIMARY KEY(col1[,col2]...)";这属于表级完整性约束,适合单列主键和复合主键。
⑦系统自动为主键创建主键索引,默认名时PRIMARY;可重新指定名字。
(2)候选键约束
与主键类似,可由一列或多列构成。
候选主键在创建时用关键字"UNIQUE"标识。
①每个表可以由多个候选键
②系统自动为候选键创建UNIQUE索引
2 参照性约束
通过外键声明来实现。
方法①在某列的属性定义后加reference_definition语法项。
reference_definition语法项:
REFERENCES tbl_name(col1[, col2]...)
[ON DELETE reference_option]
[ON UPDATE reference_option]
其中col1的语法是:
col_name[(length)] [ASC | DESC]
reference_option语法是:
RESTRICT | CASCADE | SET NULL | NO ACTION
ON DELETE和ON UPDATE分别对应当外键所应用的值在被参照表中删除和更新时,外键这里需要做的操作。
RESTRICT:当有外键引用的值被删除或更新时,系统拒绝删除或更新。(需要先斩断外键与被删除或更新值之间的关联才能不受限制)
CASCADE:级联删除或更新;当有外键引用的值被删除或更新时,引用它的外键所在行也会自动被删除或更新。
SET NULL:当有外键引用的值被删除或更新时,引用它的外键值将被设置为NULL。(需要外键允许为NULL)
NO ACTION:不采取任何措施;与RESTRICT效果一致。(这里要特别注意)
方法②:所有列定义后添加
FOREIGN KEY(col1[, col2]...)
REFERENCES tbl_name(col1[, col2]...)
[ON DELETE reference_option]
[ON UPDATE reference_option]
外键可以参照自身所在的表,即自参照表。
外键的列数与被参照的列数需要相同。
3 用户定义完整性
①非空约束:NOT NULL/NULL
②CHECK约束:对列或表约束;语法:
CHECK(expr)
expr是SQL表达式。
③触发器
三 命名完整性约束
对约束进行命名。
在各种完整性约束的定义前加上"CONSTRAINT"和约束的名字即可:
CONSTRAINT [name]
若不命名则系统自动取名。
只能针对基于表的完整性约束命名。
四 更新完整性约束
语法:
ALTER TABLE 与表有关的
①完整性约束不能直接修改,若要修改实际上是先删除后添加同名约束。
②ALTER TABLE语句可独立删除约束而不删除表本身;删除表则会删除与表相关的全部约束。
第二节 触发器
Trigger是用用户定义在关系表上的一类事件驱动的数据库对象;也是一种保证数据完整性的方法。
触发器一旦定义,无需用户调用,任何对表的修改均由DB Server自动激活相应的触发器。
主要作用实现主键和外键不能保证的复杂的参照完整性和数据一致性。
一 创建触发器
语法:
CREATE TRIGGER tri_name tri_time tri_event
ON tbl_name
FOR EACH ROW
tri_body
①tri_name:触发器名,当前DB内唯一。
②tri_time:可使用的值为BEFORE和AFTER;表示在激活它的语句之前或之后触发。
用于验证新数据用BEFORE;
用于执行善后用AFTER。
③tri_event:指定触发事件;可使用的值有:
INSERT、UPDATE和DELETE
④tbl_name:关联的表名,不能是临时表或者视图;同一个表不能同时拥有相同tri_time和tri_event的两个触发器。
⑤FOR EACH ROW:针对每一行。
⑥tri_body:动作的主体;若为多条SQL语句,则使用BEGIN...END复合语句结构。
因每个表每个事件在每个触发事件上只允许定义一个触发器;因此,每个表最多支持6个触发器;分别是:
BEFORE INSTER
BEFORE UPDATE
BEFORE DELETE
AFTER INSERT
AFTER UPDATE
AFTER DELETE
例如:
CREATE TRIGGER tri_01 AFTER INSERT
ON customers FOR EACH ROW
SET @str = "插入了一个客户信息!";
测试:
INSERT INTO customers VALUES(NULL,'郑八','F','广州',"YUEXIU")
二 删除触发器
语法:
DROP TRIGGER [IF EXISTS] tri_name
三 使用触发器
1 INSERT触发器
①在INSERT触发器代码内可引用一个名为NEW(不区分大小写)的虚拟表,来访问被插入行的数据。(BEFORE和AFTER都可用。)
②对于AUTO_INCREMENT的列NEW在执行前(BEFORE)的值总是0;在AFTER中则是实际自增后的值。
例如:
CREATE TRIGGER tri_01 AFTER INSERT
ON customers FOR EACH ROW
SET @str = NEW.cust_id;
测试:
INSERT INTO customers VALUES(NULL,'陈九','M','SHANGHAI','XuHui');
2 DELETE触发器
①在DELETE触发器内可以引用一个名为OLD(不区分大小写)的虚拟表,来访问被删除行的数据。
②OLD表中的值全部是只读,无法修改、删除。
3 UPDATE触发器
①在UPDATE触发器内既可以引用名为OLD的虚拟表,又可以引用名为NEW的虚拟表。
②BEFORE中NEW中的值是新值;注意需要执行的用户又权限执行才行。
③OLD表中的值是只读的。
例如:
CREATE TRIGGER tri_03 BEFORE UPDATE
ON cutomers FOR EACH ROW
SET NEW.cust_address = OLD.cust_name
更新时将表最终的cust_address设置为cust_name的值。
测试:
UPDATE customers SET cust_address = "BEIJNG" WHERE cust_name = "陈九";
第三节 安全性与访问权限
一 用户账户管理
MySQL中的用户账户及相关信息都存储在一个名为mysql的数据库中,这个数据库里有一个user表,包含了所有用户账号和密码信息,并使用一个名为user的列来存储用户的登陆名。
1 创建用户账号
语法:
CREATE USER 'username'@'hostname' [IDENTIFIED BY [PASSWORD]] 'password'[,'username'@'hostname' [IDENTIFIED BY [PASSWORD]] 'password']...;
①hostname:表示主机名,即用户连接MySQL时使用的主机的名字;若美哟指定,则默认会是"%";表示一组主机。
②IDENTIFIED BY子句可选,用户指定用户的密码;若不使用此子句,则创建的用户无密码。
③PASSWORD:可选,用于指定散列(加密)口令;如果希望数据库中存储明文密码则省略此关键字。若需要存储加密后的密码则使用该关键字并指定密码加密后的散列值。
④password:用户的密码;可明文可散列。
例如:
mysql> SELECT PASSWORD('123');
xxxxxxxx #散列后的密码密文
mysql> CREATE USER
'zhangsan'@'localhost' IDENTIFIED BY '123456', #密码明文存储
'lisi'@'localhost' IDENTIFIED BY PASSWORD 'xxxxxxxx'; #密码散列存储
相同用户名但不同主机名是允许的。
若不指定口令则可以不使用口令就能登陆MySQL。
2 删除用户
语法:
DROP USER user1[,user2]...;
若username中不知道主机名则默认认为是"%";但若不存在'username'@'%'则会被认为用户不存在。
3 修改用户账户
语法:
SET PASSWORD [FOR 'user1'@'hostname'] = {PASSWORD('new_password') | 'encrypted_password'};
例如:
mysql> SELECT PASSWORD('123');
xxxxxxxxx #密码123的散列值
mysql> SET PASSWORD FOR 'zhangsan'@'localhost' = 'xxxxxxxxx';
#或者
mysql> SET PASSWORD FOR 'zhangsan'@'localhost' = PASSWORD('123');
若省略FOR 'user1'@'hostname';则表示修改当前用户。
二 账号权限管理
新创建的用户只能登陆MySQL,基本无其他权限。
可使用SHOW GRANTS FOR语句查看用户的权限清单。
1 权限授予
语法:
GRANT priv_type[(col_list)][,priv_type(col_list)]...
ON [object_type] priv_level
TO user_specification[,user_specification]...
[WITH GRANT OPTION]
①priv_type:用于指定权限名称;例如:SELECT、UPDATE、DELETE等。
②col_list:指定权限授予给表中的哪些列。
③ON子句:权限授予的对象和级别;例如:可在ON后给出亚奥授予权限的数据库名或者表名。
④object_type:权限要授予的对象类型,包括表、函数和存储过程等;即TABLE、FUNCTION和PROCEDURE等。
⑤priv_level:权限级别。
*.*
和*
代表当前DB中所有表/视图。
dbname.*
代表dbname数据库中的所有表/视图。
dbname.tbl_name
代表dbname数据库中tbl_name表/视图。
dbname.routine_name
代表dbname数据库中的某个存储过程或存储函数。
⑥TO子句:
TO 'username'@'hostname' [IDENTIFIED BY [PASSWORD] 'password']
实际上是授权时可以对已存在用户授权,也可以在授权时创建用户;若同时指定了用户名和密码的情况下,若用户是已存在用户则会更新该用户的密码。
例如:
GRANT SELECT,UPDATE
ON mysql_test.customers
TO 'zhangsan'@'localhost' IDENTIFIED BY '123',
'lisi'@'localhost',
'wangwu'@'localhost' IDENTIFIED BY PASSWORD 'xxxxxxxx'; #加密后的米阿门散列
或者:
GRANT SELECT(cust_id,cust_name) ON mysql_test.customers TO 'zhangsan'@'localhost';
2 权限转移
通过GRANT中的WITH子句实现。
WITH GRANT OPTION:表示TO子句中的用户都具有将自己所拥有的权限授予其他用户的权利。
3 权限的撤销
语法:
REVOKE
priv_type[(col_list)][,priv_type[col_list]]...
ON [object_type] priv_level
FROM 'username'@'hostname' [IDENTIFIED BY [PASSWORD] 'password'][,'username'@'hostname' [IDENTIFIED BY [PASSWORD] 'password']]...;
第四节 事务与并发控制
多用户,同一或多个程序同时存取;提供控制,防止互相干扰;这种机制称为"并发控制"。
事务时为了保证数据的一致性二产生的一个概念和基本手段。
MySQL中只有InnoDB引擎的数据库或者表才支持事务。
一 事务的概念
事务是用户定义的一个数据操作序列,这些操作可以作为一个完整的工作单元,要么全部执行,要么全部不执行;是一个不可分割的工作单元。
一个部分可以是一条/一组SQL语句或整个程序。
程序是静止的,事务是动态的是程序的执行而不是程序本身;同一程序的多个独立执行可以同时进行。而每一步执行则是一个不同的事务。
要让DBMS知道哪些动作属于一个事务,而不是单纯的DB操作或程序。必须要显式地告诉DBS,这可以通过标记事务的开始和结束来实现。
事务的定义语句:
①BEGIN TRANSACTION:开始
②COMMIT:提交(更新回写到磁盘)
③ROLLBACK:回滚
以①开始;以②③结束。
二 事务的特征
ACID特征:
①原子性Atomicity:事务包含的一组操作是原子不可分的;要么全做,要么全不做。
②一致Consistency:由一个状态转到另一个状态。
③隔离性Isolation:彼此之间互不干扰。
④持续性Durability:也叫永久性,提交对DB中的数据改变是永久性的。
例如:银行账户A \Rightarrow S金额 \Rightarrow B账户
思考过程:
read(A); # 读取A账户
A = A - S; # A账户的总金额减少S
write(A); # 修改后的A账户总金额写入A账户
read(B); # 读取B账户
B = B + S; # B账户的总金额增加S
write(B); # 修改后的B账户总金额写入B账户
结果:
BEGIN TRANSCATION
read(A);
A = A - S;
write(A);
if(A < 0) ROLLBACK; # A透支时拒绝转账,执行回滚;数据库中相关数据恢复到本事务初始时的状态。
else{
read(B);
B = B + S;
write(B); # 书本中没有这一句
COMMIT; # 表示操作全部完成,数据库处于新的状态。
}
三 并发操作问题
事务是并发控制的基本单位。
为了保证事务的隔离性和一致性,DBMS需要对并发正确调度。
并发操作问题典型的三种类型:
(1)更新丢失
事务T_1和T_2同时读入同一数据加以修改;其中一个事务的提交结果导致另一个事务的修改丢失(被覆盖);导致数据不一致。
(2)不可重复读
事务和
;当
读Data后,
执行更新,这使得
无法再现前一次读取的结果;这又分为三种情况:
①读
改
再次读取时得到与前一次不同的结果。(不可重复读)
②按条件读一些Data
删除了部分记录
再次按相同条件读取时某些记录消失。(不可重复读)
③按条件读一些Data
插入了部分记录
再次按相同条件读取时多出一些记录。(幻读)
(3)读'脏'数据
一个事务读取了另一个事务修改但还未提交的数据。 在这种情况下,如果前一个事务发生回滚,后一个事务读取到的数据就会无效,导致数据不一致。被读取的未提交的数据就是'脏'数据。
以上就是并发操作破坏了事务的隔离性可能带来的后果。并发控制机制就是解决这些问题的方法。主要方法有:
封锁、时间戳、乐观控制法和多版本并发控制等;其中封锁时大多数DB采用的基本方法。
四 封锁
最常用的并发控制技术;基本思想:需要时,事务通过向系统请求对它所希望操作的数据对象(比如DB中的记录)加锁,以确保它不被非预期地改变。
1 锁
实质上是允许或阻止一个事务对一个数据对象的存取特权。一个事务对一个对象加锁的结果是将别的事务"封锁"在对象之外。
基本的锁类型有:
①排他锁(Exclusive Lock,X锁,又叫写锁):加锁后其他事务既不能对该数据进行读写,也不能对该数据添加其他任何锁。主要用于写操作。
②共享锁(Shared Lock,S锁,又叫读锁):当事务对数据加上读锁后,其他事务只能对该数据进行读取也就是可以加S锁,但不能做任何修改操作,也就是不能添加X锁。主要用于读操作。
2 用封锁进行并发控制
工作原理:
①事务T对数据D加X锁:所有其他的事务对D的锁请求(S锁或者X锁)都必须等到T释放X锁。
②事务T对数据D加S锁:别的事务可以对D请求S锁;但对D请求X锁必须等到T释放S锁。(隐含:其它啊事务都释放了对该数据的S锁。)
③事务执行DB操作都需要请求相应的锁;一般由DBMS在执行时自动请求。
④事务一直占用获得的锁直到结束(COMMIT或者ROLLBACK)时释放。
3 封锁的粒度
通常以粒度描述封锁的数据单元大小。DBMS可以决定不同的粒度的锁,粒度可以是从最底层的数据元素到最高层的整个数据库,粒度越细,并发性就越大,但软件的复杂性和系统的开销也越大,使得系统整体性能降低。
但粒度越粗,则并发性就越小,虽然软件的复杂性会和开销会变小;但是同样使得系统整体性能降低。
因此,大多数高性能系统都会选择折中的锁粒度。至于哪一层粒度最合适则与应用环境下事务量、数据量及数据的易变性特征等都紧密相关。
4 封锁的级别
又称一致性级别或隔离度。
与封锁期限有关。
各种锁的类型与其封锁期限组合形成不同的封锁级别。
(1)0级封锁
被封锁的事务不重写其他非0级封锁事务的未提交更新数据;实际价值不大。(甚至很多教程都默认只有3级封锁,不包含它)
(2)1级封锁
被封锁事务既不重写但可以读未提交的更新数据;为了防止丢失更新。
仅写加X锁直到事务结束(COMMIT或ROLLBACK),读不加锁。
读未提交
丢失更新:不加锁修改之后被其它事务更新了 写:加X锁->修改数据R->成功或失败->释放X锁 读:不加锁,导致读到别的事务未提交数据,且多次读结果不一致
(3)2级封锁
被封锁事务既不重写也不读未提交的更新数据;除了防止丢失更新,还可以防止读'脏'数据。(书中的描述是模糊且与3级封锁描述一致,不用看了。)
读数据前加S锁,读操作完成后就释放S锁。(瞬间S锁)
读已提交
写:同一级封锁协议 读:加S锁->读数据R->读完释放S锁,不会读到别的事务未提交的数据,但多次读会结果还是会不一致 注:在数据R加了X锁之后,不能再加其它锁(其它事务的X锁和S锁);在数据R加了S锁之后,其它锁还能加S锁,但不能加X锁
(4)3级封锁
被封锁的事务不读未提交的更新数据,不写未提交的数据。显然:防止丢失更新、防止读'脏'数据和防止不可重复读。(书中的描述是模糊且与2级封锁描述一致,不用看了。)
读数据前加S锁,事务结束后才释放S锁。
可重复读
写:同一级封锁协议 读:加S锁->读数据R->再读数据R->事务完成->释放S锁,多次读结果一致
可串行化:
可重复读与幻读的区别是:可重复读是更改表中行级数据,而幻读是增加表中行级数据,可串行化使得所有的事务必须串行化执行,解决了一切并发问题,但会造成大量的等待、阻塞甚至死锁,使系统性能降低。
5 活锁与死锁
封锁会带来一个重要的问题:可能引起"活锁"或"死锁"。
活锁:事务可能因为优先级低而永远等待下去。
活锁问题的解决与调度算法有关;最简单的办法是先来先服务。
死锁:两个以上事务循环等待被同组另外事务锁住的数据单元的情形。
在任何一个多任务程序设计系统中,死锁总是潜在存在的。
预防死锁的几种方法:
(1)一次性锁请求
每一事务在处理时一次性地提出所有锁请求,仅当这些请求全部满足时事务处理才进行,否则就等待;这样做不会死锁但是会使系统并行性降低,事务等待时间加长,使系统性能下降。
(2)锁请求排序
让每个数据单元以线性顺序排序,然后要求每一事务都按此顺序提出锁请求。同样会降低系统性能。
(3)序列化处理
通过应用设计未每一数据单元建立一个"主人"程序,对没定数据单元的所有请求都发送给"主人",而"主人"以单道形式运行。这样系统可以多道运行。系统性能和完整性会受到影响。
(4)资源剥夺
每当事务因锁请求不能满足而受阻时,强行令两个冲突的事务中的一个ROLLBACK,释放所有锁,以后再运行。
对待死锁也可以不防止而是让其发生并随时检测,一旦发现再进行解除处理。使用图论的方法检测,并以这个在执行的事务为终点。
6 可串行性
一组事务的一个调度就是它们的基本操作的一种排序。
如果两个事务T_1和T_2中,T_1的所有操作在T_2的所有操作之前或者之后,则这种关系就是指该调度是并行的。
若一调度等价于某一串行高度,及它们产生的结果一样则说调度是可串行化的。
一组事务的串行调度并不是唯一的。
7 两段封锁法
封锁是一种并发控制技术,而可串行性是并发控制(调度)的正确性标准。
两段封锁法(Two-Phase Locking,TPL)是一种简单而有效的保障调度,是可串行性的一种方法。
两段锁协议规定:在任何一个事务中,所有加锁操作都必须在所有释放锁之前。
因此,可以将事务划分为两段:
(1)发展(Growing)或加锁阶段
此间,对任一Data进行任何操作前,事务都需要获得该对象的一个相应的锁。
(2)收缩(Sharking)或释放阶段
一旦事务释放一个锁,则表明它进入了此阶段,以后它不能再请求任何另外的锁。
定理6.1:遵循两段锁协议可使事务的任何并发调度都是可串行化的。
第五节 备份与恢复
破坏DB的因素:
①硬件故障
②软件故障
③病毒
④人为误操作
⑤自然灾害
⑥资料窃取
MySQL的备份由很多方法,但本书只介绍其中最基本的一种方式。
下面介绍使用SQL语句备份和恢复DB中表数据的方法;需要注意的是,这种方法只能导出或导入数据内容,而不包括表结构,如果表结构文件损坏,则需要先设法恢复原本的表结构之后才能使用这里介绍的方法完成的备份进行恢复。(MySQL是有同时备份表结构个数据的方法的,只是非本书讨论的重点。)
1 使用SELECT ... INTO OUTFILE ...语句备份数据
将表中的所有行写入到一个文件中;(书中写的语法经不起推敲的)
SELECT {* [FROM db_name [WHERE where_conditions]] | tbl_name_list FROM db_name [WHERE where_conditions]} {INTO OUTFILE 'file_name' export_options | INTO DUMPFILE 'file_name'}
其中,export_options格式如下:
[FIELDS
[TERMINATED BY 'str'] #字段间的分割符号
[[OPTIONALLY] ENCLOSED BY 'char'] #包裹字符类型值得符号;OPTIONALLY针对所有类型值而非仅仅字符型
[ESCAPED BY 'char'] #转移字符
]
[LINES TERMINATED BY 'str'] #数据行的结束标识
①在文件中导出的数据行会以一定的形式存储,其中空值用'\N'表示。
②Fields和Lines者自居分别决定数据在备份文件中的存储格式。如果不指定则默认值是:
FIELDS
TERMINATED BY '\t'
ENCLOSED BY ''
ESCAPED BY '\\'
LINES TERMINATED BY '\n'
③如果使用了FIELDS子句,那么TERMINATED/ENCLOSED/ESCAPED子句至少使用一个。
④DUMPFILE:不用指定存储格式,所有数据行都会紧挨着放置,值与行之间无任何标识。
2 使用LOAD DATA INFILE ... INTO语句恢复数据
语法:
LOAD DATA INFILE 'file_name'
INTO TABLE tbl_name
[FIELDS
[TERMINATED BY 'str'] #字段间的分割符号
[[OPTIONALLY] ENCLOSED BY 'char'] #包裹字符类型值得符号;OPTIONALLY针对所有类型值而非仅仅字符型
[ESCAPED BY 'char'] #转移字符
]
[LINES
[STARTING BY 'str'] #指定前缀;导入数据时忽略前缀及其之前的内容,且若某行不包含该前缀则跳过该行。
[TERMINATED BY 'str'] #数据行的结束标识
]
例如:
备份
SELECT * FROM mysql_test.customers
INTO OUTFILE 'C:\Backups\mydata.txt'
FIELDS TERMINATED BY ','
OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '?';
恢复
LOAD DATA INFILE 'C:\Backups\mydata.txt'
INTO TABLE mysql_test.customers
FIELDS TERMINATED BY ','
OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '?';
恢复时使用的FIELDS子句和LINES子句的内容需要和备份时保持一致;否则可能出现数据识别错误。
注意:在多个用户同时使用MySQL数据库的场景中,为了得到一个一致的备份,需要在指定的表上使用LOCK TABLES tbl_name READ语句做一个读锁定(别人只能读),以防止在备份过程中表被其它用户更新。
当恢复时,则需要使用LOCK TABLES tbl_name WRITE语句做一个写锁定(别人不能读写),以避免数据冲突。
操作完成后使用UNLOCK TABLES语句对表进行解锁。