目录
事务
事务的四大特性(简称ACID):
- 原子性(Atomicity):事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。
- 回滚可以用日志来实现,日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。
- 一致性(Consistency):数据库在事务执行前后都保持一致性状态。
- 在一致性状态下,所有事物对一个数据的读取结果是相同的。
- 隔离性:一个事务所做的修改在最终提交以前,对其他事务是不可见的。
- 持久性:一旦事务提交,则其所做的修改将会永远保存到数据库中,即使系统发生崩溃,事务执行的结果也不能丢失。
- 可以通过数据库备份和恢复来实现,在系统发生崩溃时,使用备份的数据库进行数据恢复。
事务的隔离性
1、如果不考虑事务的隔离性会产生的问题:
(1)脏读:脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。例如:用户A向用户B转账100元,对应SQL命令如下:
update account set money=money+100 where name=’B’; (此时A通知B)
update account set money=money - 100 where name=’A’;
当只执行第一条SQL时,A通知B查看账户,B发现确实钱已到账(此时即发生了脏读),而之后无论第二条SQL是否执行,只要该事务不提交,则所有操作都将回滚,那么当B以后再次查看账户时就会发现钱其实并没有转。
(2)不可重复读:不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
例如事务T1在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据就得到了不同的结果,发送了不可重复读。
不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。
在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。但在另一些情况下就有可能发生问题,例如对于同一个数据A和B依次查询就可能不同,A和B就可能打起来了……
(3)虚读:幻读是事务非独立执行时发生的一种现象。例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。
幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。
2、MySQL数据库的四种隔离级别:
① Serializable (串行化):可避免脏读、不可重复读、幻读的发生。
② Repeatable read (可重复读):可避免脏读、不可重复读的发生。
③ Read committed (读已提交):可避免脏读的发生。
④ Read uncommitted (读未提交):最低级别,任何情况都无法保证。
以上四种隔离级别最高的是Serializable级别,最低的是Read uncommitted级别,当然级别越高,执行效率就越低。像Serializable这样的级别,就是以锁表的方式(类似于Java多线程中的锁)使得其他的线程只能在锁外等待,所以平时选用何种隔离级别应该根据实际情况。在MySQL数据库中默认的隔离级别为Repeatable read (可重复读)。
索引
索引定义
数据库索引好比是一本书前面的目录,能加快数据库的查询速度。索引是对数据库表中一个或多个列(例如,employee 表的姓氏 (lname) 列)的值进行排序的结构。
建立索引的优缺点
优点:
1.大大加快数据的检索速度;
2.创建唯一性索引,保证数据库表中每一行数据的唯一性;
3.加速表和表之间的连接;
4.在使用分组和排序子句进行数据检索时,可以显著减少查询中分组和排序的时间。
缺点:
1.索引需要占用数据表以外的物理存储空间
2.创建索引和维护索引要花费一定的时间
3.当对表进行更新操作时,索引需要被重建,这样降低了数据的维护速度。
索引类型
根据数据库的功能,可以在数据库设计器中创建索引。按照存储方式分为:聚集与非聚集索引;按照维护与管理索引角度分为:唯一索引、组合索引和系统自动创建的索引。
(1)唯一索引: UNIQUE 例如:create unique index stusno on student(sno);
索引列的值必须唯一,但允许有空值。
(2)主键索引: primary key
特殊的唯一索引,不允许有空值;一个表只能有一个主键。
(3)组合索引:多个字段组合作为索引;
(4)聚集索引与非聚集索引:
- 聚集索引
- 表中行的物理顺序与键值的逻辑(索引)顺序相同。一个表只能包含一个聚集索引。
- 聚集索引应该建在那些经常需要order by,group by,按范围取值的列上。因为数据本身就是按照聚集索引的顺序存储的。不应该建在需要频繁修改的列上,因为聚集索引的每次改动都以为这表中数据的物理数据的一次重新排序。聚集索引适合建立在大数据量但是小数目不同值的列上。
- 非聚集索引
- 非聚集索引和表里面数据的物理地址顺序无关。
- 非聚集索引的查询方式和聚集索引的查询方式不一样。
- 聚集索引找到符合条件的目标即获得该目标行的所有数据,因为直接找到是它的物理地址。
- 非聚集索引
- 如果查询的字段是非聚集索引的一部分,直接返回数据(索引本身包含的就有相应数据);
- 如果查的数据包含非索引数据,那么通过非聚集索引找到目标之后,目标体会有一个目标数据聚集索引的key,会通过这个key再通过聚集索引找到完整的目标数据。
索引的实现方式
(1)树
- 几种树结构(B树,B+树,B*树)
- 平衡树:二叉树的查找的时间复杂度是O(log2N),其查找效率与深度有关,而普通的二叉树可能由于内部节点排列问题退化成链表,这样查找效率就会很低。因此平衡二叉树是更好的选择,因为它保持平衡,即通过旋转调整结构保持最小的深度。其查找的时间复杂度也是O(log2N)。
- 实际上,数据库中索引的结构也并非AVL树或更优秀的红黑树,尽管它的查询的时间复杂度很低。原因如下:
- 索引存在于索引文件中,是存在于磁盘中的。索引通常很大,因此无法一次将全部索引加载到内存当中,每次只能从磁盘中读取一个磁盘页的数据到内存中。而这个磁盘的读取的速度较内存中的读取速度而言是差了好几个级别。
- 我们说的平衡二叉树结构,指的是逻辑结构上的平衡二叉树,其物理实现是数组。但是在逻辑结构上相近的节点在物理结构上可能会差很远。因此,每次读取的磁盘页的数据中有许多是用不上的。因此,查找过程中要进行许多次的磁盘读取操作。
- 而适合作为索引的结构应该是尽可能少的执行磁盘IO操作,因为执行磁盘IO操作非常的耗时。因此,平衡二叉树并不适合作为索引结构。
- B树
- 平衡二叉树没能充分利用磁盘预读功能,而B树是为了充分利用磁盘预读功能来而创建的一种数据结构,B树是为了作为索引才被发明出来的的。
- 局部性原理与磁盘预读:由于存储介质的特性,磁盘本身存取就比主存慢很多,再加上机械运动耗费,磁盘的存取速度往往是主存的几百分分之一,因此为了提高效率,要尽量减少磁盘I/O。为了达到这个目的,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。这样做的理论依据是计算机科学中著名的局部性原理:
- 当一个数据被用到时,其附近的数据也通常会马上被使用。
- 程序运行期间所需要的数据通常比较集中。
- 由于磁盘顺序读取的效率很高(不需要寻道时间,只需很少的旋转时间),因此对于具有局部性的程序来说,预读可以提高I/O效率。
-
B树的每个节点可以存储多个关键字,它将节点大小设置为磁盘页的大小,充分利用了磁盘预读的功能。每次读取磁盘页时就会读取一整个节点。也正因每个节点存储着非常多个关键字,树的深度就会非常的小。进而要执行的磁盘读取操作次数就会非常少,更多的是在内存中对读取进来的数据进行查找。
-
B+树
-
比B树更适合作为索引的结构是B+树。MySQL中也是使用B+树作为索引。它是B树的变种,因此是基于B树来改进的。B树:有序数组+平衡多叉树;B+树:有序数组链表+平衡多叉树。
-
B+树的关键字全部存放在叶子节点中,非叶子节点用来做索引,而叶子节点中有一个指针指向一下个叶子节点。做这个优化的目的是为了提高区间访问的性能。而正是这个特性决定了B+树更适合用来存储外部数据。
-
比较:
-
B树:
比如说,我们要查找关键字范围在3到7的关键字,在找到第一个符合条件的数字3后,访问完第一个关键字所在的块后,得遍历这个B树,获取下一个块,直到遇到一个不符合条件的关键字。遍历的过程是比较复杂的。
B+树:
相比之下,B+树的基于范围的查询简洁很多。由于叶子节点有指向下一个叶子节点的指针,因此从块1到块2的访问,通过块1指向块2的指针即可。从块2到块3也是通过一个指针即可。
总结采用B+树的原因:
- 搜索效率高:是一种平衡树;
- 提高了磁盘IO性能;
- 元素遍历效率高。只要遍历叶子节点就可以实现整棵树的遍历。
(2)散列索引
通过散列函数来定位的一种索引,很少有单独使用散列索引的,反而是散列文件组织用的比较多。
散列文件组织就是根据一个键通过散列计算把对应的记录都放到同一个槽中,这样的话相同的键值对应的记录就一定是放在同一个文件里了,也就减少了文件读取的次数,提高了效率。
散列索引就是根据对应键的散列码来找到最终的索引项的技术,其实和B树就差不多了,也就是一种索引之上的二级辅助索引,我理解散列索引都是二级或更高级的稀疏索引,否则桶就太多了,效率也不会很高。
(3)位图索引
位图索引是一种针对多个字段的简单查询设计一种特殊的索引,适用范围比较小,只适用于字段值固定并且值的种类很少的情况,比如性别,只能有男和女,或者级别,状态等等,并且只有在同时对多个这样的字段查询时才能体现出位图的优势。
位图的基本思想就是对每一个条件都用0或者1来表示,如有5条记录,性别分别是男,女,男,男,女,那么如果使用位图索引就会建立两个位图,对应男的10110和对应女的01001,这样做有什么好处呢,就是如果同时对多个这种类型的字段进行and或or查询时,可以使用按位与和按位或来直接得到结果了。
(4)比较:
B+树最常用,性能也不差,用于范围查询和单值查询都可以。特别是范围查询,非得用B+树这种顺序的才可以了。HASH的如果只是对单值查询的话速度会比B+树快一点,但是ORACLE好像不支持HASH索引,只支持HASH表空间。位图的使用情况很局限,只有很少的情况才能用,一定要确定真正适合使用这种索引才用(值的类型很少并且需要复合查询),否则建立一大堆位图就一点意义都没有了。
MySQL索引实现
参考链接:https://blog.csdn.net/suifeng3051/article/details/52669644
MyISAM索引实现
MyISAM引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址。下图是MyISAM索引的原理图:
这里设表一共有三列,假设我们以Col1为主键,则上图是一个MyISAM表的主索引(Primary key)示意。可以看出MyISAM的索引文件仅仅保存数据记录的地址。在MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复。如果我们在Col2上建立一个辅助索引,则此索引的结构如下图所示:
同样也是一颗B+Tree,data域保存数据记录的地址。因此,MyISAM中索引检索的算法为首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录。 MyISAM的索引方式也叫做“非聚集”的,之所以这么称呼是为了与InnoDB的聚集索引区分。
InnoDB索引实现
虽然InnoDB也使用B+Tree作为索引结构,但具体实现方式却与MyISAM截然不同。
第一个重大区别是InnoDB的数据文件本身就是索引文件。从上文知道,MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。而在InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。
上图是InnoDB主索引(同时也是数据文件)的示意图,可以看到叶节点包含了完整的数据记录。这种索引叫做聚集索引。因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形。
第二个与MyISAM索引的不同是InnoDB的辅助索引data域存储相应记录主键的值而不是地址。换句话说,InnoDB的所有辅助索引都引用主键作为data域。例如,下图为定义在Col3上的一个辅助索引:
这里以英文字符的ASCII码作为比较准则。聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。
了解不同存储引擎的索引实现方式对于正确使用和优化索引都非常有帮助,例如知道了InnoDB的索引实现后,就很容易明白为什么不建议使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。再例如,用非单调的字段作为主键在InnoDB中不是个好主意,因为InnoDB数据文件本身是一颗B+Tree,非单调的主键会造成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。
两种索引引擎比较
- Innodb引擎
- Innodb引擎提供了对数据库ACID事务的支持,并且实现了SQL标准的四种隔离级别。
- 该引擎还提供了行级锁和外键约束,它的设计目标是处理大容量数据库系统,它本身其实就是基于MySQL后台的完整数据库系统 。
- MySQL运行时Innodb会在内存中建立缓冲池,用于缓冲数据和索引。
- 但是该引擎不支持FULLTEXT类型的索引,而且它没有保存表的行数,当SELECT COUNT(*) FROM TABLE时需要扫描全表。当需要使用数据库事务时,该引擎当然是首选。
- 由于锁的粒度更小,写操作不会锁定全表,所以在并发较高时,使用Innodb引擎会提升效率。但是使用行级锁也不是绝对的,如果在执行一个SQL语句时MySQL不能确定要扫描的范围,InnoDB表同样会锁全表。
- MyIASM引擎
- MyIASM是MySQL默认的引擎,但是它没有提供对数据库事务的支持,也不支持行级锁和外键,因此当INSERT(插入)或UPDATE(更新)数据时即写操作需要锁定整个表,效率便会低一些。
- 不过和Innodb不同,MyIASM中存储了表的行数,于是SELECT COUNT(*) FROM TABLE时只需要直接读取已经保存好的值而不需要进行全表扫描。如果表的读操作远远多于写操作且不需要数据库事务的支持,那么MyIASM也是很好的选择。
索引使用策略及优化
MySQL的优化主要分为结构优化(Scheme optimization)和查询优化(Query optimization)。
联合索引及最左前缀原理
(1)联合索引(复合索引)
联合索引可以为多个字段创建一个索引。比如,我们在(a,b,c)字段上创建一个联合索引,则索引记录会首先按照A字段排序,然后再按照B字段排序然后再是C字段,因此,联合索引的特点就是:第一个字段一定是有序的;当第一个字段值相等的时候,第二个字段又是有序的,比如下表中当A=2时所有B的值是有序排列的,依次类推,当同一个B值得所有C字段是有序排列的:
| A | B | C |
| 1 | 2 | 3 |
| 1 | 4 | 2 |
| 1 | 1 | 4 |
| 2 | 3 | 5 |
| 2 | 4 | 4 |
| 2 | 4 | 6 |
| 2 | 5 | 5 |
其实联合索引的查找就跟查字典是一样的,先根据第一个字母查,然后再根据第二个字母查,或者只根据第一个字母查,但是不能跳过第一个字母从第二个字母开始查。这就是所谓的最左前缀原理。
(2)最左前缀原理
我们在(a,b,c)
字段上建了一个联合索引,这个索引是先按a 再按b 再按c进行排列的,所以:
以下的查询方式都可以用到索引:
select * from table where a=1;
select * from table where a=1 and b=2;
select * from table where a=1 and b=2 and c=3;
上面三个查询按照 (a ), (a,b ),(a,b,c )
的顺序都可以利用到索引,这就是最左前缀匹配。
如果查询语句是:
select * from table where a=1 and c=3; 那么只会用到索引a。
如果查询语句是:
select * from table where b=2 and c=3; 因为没有用到最左前缀a,所以这个查询是用不到索引的。
如果用到了最左前缀而只是颠倒了顺序,也是可以用到索引的,因为mysql查询优化器会判断纠正这条sql语句该以什么样的顺序执行效率最高,最后才生成真正的执行计划。但我们还是最好按照索引顺序来查询,这样查询优化器就不用重新编译了。
前缀索引
前缀索引就是用列的前缀代替整个列作为索引key,当前缀长度合适时,可以做到既使得前缀索引的选择性接近全列索引,同时因为索引key变短而减少了索引文件的大小和维护开销。但是前缀索引也有它的坏处:MySQL 不能在 ORDER BY 或 GROUP BY 中使用前缀索引,也不能把它们用作覆盖索引(Covering Index)。
一般来说以下情况可以使用前缀索引:
(1)字符串列(varchar,char,text等),需要进行全字段匹配或者前匹配。也就是=‘xxx’ 或者 like ‘xxx%’
(2)字符串本身可能比较长,而且前几个字符就开始不相同。比如我们对中国人的姓名使用前缀索引就没啥意义,因为中国人名字都很短,另外对收件地址使用前缀索引也不是很实用,因为一方面收件地址一般都是以XX省开头,也就是说前几个字符都是差不多的,而且收件地址进行检索一般都是like ’%xxx%’,不会用到前匹配。相反对外国人的姓名可以使用前缀索引,因为其字符较长,而且前几个字符的选择性比较高。同样电子邮件也是一个可以使用前缀索引的字段。
(3)前一半字符的索引选择性就已经接近于全字段的索引选择性。如果整个字段的长度为20,索引选择性为0.9,而我们对前10个字符建立前缀索引其选择性也只有0.5,那么我们需要继续加大前缀字符的长度,但是这个时候前缀索引的优势已经不明显,没有太大的建前缀索引的必要了。
索引优化策略
- 最左前缀匹配原则
- 主键外键一定要建索引
- 对 where,on,group by,order by 中出现的列使用索引
- 尽量选择区分度高的列作为索引,区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就是0
- 对较小的数据列使用索引,这样会使索引文件更小,同时内存中也可以装载更多的索引键
- 索引列不能参与计算,保持列“干净”,SQL语句where中如果有functionName(colname)或者某些运算,则MYSQL无法使用基于colName的索引。使用索引需要直接查询某个字段。索引失效的原因是索引是针对原值建的二叉树,将列值计算后,原来的二叉树就用不上了;为了解决索引列上计算引起的索引失效问题,将计算放到索引列外的表达式上。
lastactive为索引列:
SELECT count(id) FROM user WHERE unix_timstamp()-lastactive < 180 索引失效
SELECT count(id) FROM user WHERE lastactive > unix_timstamp() - 180 索引有效
- 为较长的字符串使用前缀索引
- 尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可
- 不要过多创建索引, 权衡索引个数与DML之间关系,DML也就是插入、删除数据操作。这里需要权衡一个问题,建立索引的目的是为了提高查询效率的,但建立的索引过多,会影响插入、删除数据的速度,因为我们修改的表数据,索引也需要进行调整重建
- 对于like查询,”%”不要放在前面。
SELECT * FROM houdunwang WHERE uname LIKE '后盾%' -- 走索引
SELECT * FROM houdunwang WHERE uname LIKE "%后盾%" -- 不走索引
- 查询where条件数据类型不匹配也无法使用索引
- 字符串与数字比较不使用索引
CREATE TABLE a(achar(10));
EXPLAIN SELECT * FROM a WHERE a="1" – 走索引
EXPLAIN SELECT * FROM a WHERE a=1 – 不走索引
- 正则表达式不使用索引,这应该很好理解,所以这是为什么在SQL中很难看到regexp关键字的原因。
存储过程
存储过程(Stored Procedure)是在大型数据库系统中,一组为了完成特定功能的SQL 语句集,存储在数据库中,经过第一次编译后再次调用不需要再次编译,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。存储过程是数据库中的一个重要对象。
优点:
a.存储过程只在创造时进行编译,以后每次执行存储过程都不需再重新编译,而一般 SQL 语句每执行一次就编译一次,所以使用存储过程可提高数据库执行速度。
b.当对数据库进行复杂操作时(如对多个表进行 Update,Insert,Query,Delete 时),可将此复杂操作用存储过程封装起来与数据库提供的事务处理结合一起使用。这些操作,如果用程序来完成,就变成了一条条的 SQL 语句,可能要多次连接数据库。而换成存储,只需要连接一次数据库就可以了。
c.存储过程可以重复使用,可减少数据库开发人员的工作量。
d.安全性高,可设定只有某此用户才具有对指定存储过程的使用权。
缺点
a.调试麻烦
b.移植问题,数据库端代码当然是与数据库相关的。但是如果是做工程型项目,基本不存在移植问题。
c.重新编译问题,因为后端代码是运行前编译的,如果带有引用关系的对象发生改变时,受影响的存储过程、包将需要重新编译(不过也可以设置成运行时刻自动编译)。
d.如果在一个程序系统中大量的使用存储过程,到程序交付使用的时候随着用户需求的增加会导致数据结构的变化,接着就是系统的相关问题了,最后如果用户想维护该系统可以说是很难很难、而且代价是空前的。维护起来更加麻烦!
数据库三范式
第一范式:属性不可拆分
第二范式:非主属性完全依赖于码
第三范式:非主属性既不完全依赖于码,也不传递依赖于码
数据库锁
数据库锁出现的原因是为了处理并发问题,因为数据库是一个多用户共享的资源,当出现并发的时候,就会导致出现各种各样奇怪的问题,就像程序代码一样,出现多线程并发的时候,如果不做特殊控制的话,就会出现意外的事情,比如“脏“数据、修改丢失等问题。所以数据库并发需要使用事务来控制,事务并发问题需要数据库锁来控制,所以数据库锁是跟并发控制和事务联系在一起的。
(1)悲观锁、乐观锁:
- 并发控制一般采用三种方法,分别是乐观锁和悲观锁以及时间戳。
- 乐观锁认为一个用户读数据的时候,别人不会去写自己所读的数据;
- 乐观锁比较简单,不做任何控制,这只是一部分人对于并发所持有的一种态度而已。
- 悲观锁觉得自己读数据库的时候,别人可能刚好在写自己刚读的数据;
- 在读取数据的时候,为了不让别人修改自己读取的数据,就会先对自己读取的数据加锁,只有自己把数据读完了,才允许别人修改那部分数据,
- 时间戳就是不加锁,通过时间戳来控制并发出现的问题。
- 时间戳就是在数据库表中单独加一列时间戳,比如“TimeStamp”,每次读出来的时候,把该字段也读出来,当写回去的时候,把该字段加1,提交之前 ,跟数据库的该字段比较一次,如果比数据库的值大的话,就允许保存,否则不允许保存,这种处理方法虽然不使用数据库系统提供的锁机制,但是这种方法可以大大提高数据库处理的并发量,因为这种方法可以避免了长事务中的数据库加锁开销(操作员A 和操作员B操作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系 统整体性能表现。
- 以上悲观锁所说的加“锁”,其实分为几种锁,分别是:排它锁和共享锁,其中排它锁又称为写锁,共享锁又称为读锁。(ps.可以参考此文 “http://blog.sina.com.cn/s/blog_548bd2090100ir7k.html”)
-
共享锁和排它锁是具体的锁,是数据库机制上的锁,存在以下关系:
上图表示可以共存的锁,如,第二行表示,一个事务T1给某数据加了X锁,则事务T2就不能再给那数据加X锁了,同时也不能再加S锁了,只有到T1事务提交完成之后,才可以。默认来说,当sql脚本修改更新某条记录的时候,会给该条记录加X锁,读的话加的是S锁。
(2)行级锁和表级锁
行级锁是一种排他锁,防止其他事务修改此行;在使用以下语句时,Oracle会自动应用行级锁:
INSERT、UPDATE、DELETE、SELECT … FOR UPDATE [OF columns] [WAIT n | NOWAIT];
SELECT … FOR UPDATE语句允许用户一次锁定多条记录进行更新
使用COMMIT或ROLLBACK语句释放锁
表级锁又分为5类:
行共享 (ROW SHARE) – 禁止排他锁定表
行排他(ROW EXCLUSIVE) – 禁止使用排他锁和共享锁
共享锁(SHARE) - 锁定表,对记录只读不写,多个用户可以同时在同一个表上应用此锁
共享行排他(SHARE ROW EXCLUSIVE) – 比共享锁更多的限制,禁止使用共享锁及更高的锁
排他(EXCLUSIVE) – 限制最强的表锁,仅允许其他用户查询该表的行。禁止修改和锁定表。
分布式数据库
定义
分布式主要是为了提供可扩展性以及高可用性,业务中使用分布式的场景主要有分布式存储以及分布式计算。
分布式存储中可以将数据分片到多个节点上,不仅可以提高性能(可扩展性),同时也可以使用多个节点对同一份数据进行备份。
至于分布式计算,就是将一个大的计算任务分解成小任务分配到多台节点上去执行,再汇总每个小任务的执行结果得到最终结果。MapReduce 是分布式计算的最好例子。
分库分表
分库分表前的问题
任何问题都是太大或者太小的问题,我们这里面对的数据量太大的问题。
- 用户请求量太大
- 因为单服务器TPS,内存,IO都是有限的。
- 解决方法:分散请求到多个服务器上; 其实用户请求和执行一个sql查询是本质是一样的,都是请求一个资源,只是用户请求还会经过网关,路由,http服务器等。
- 单库太大
- 单个数据库处理能力有限;
- 单库所在服务器上磁盘空间不足;
- 单库上操作的IO瓶颈;
- 解决方法:切分成更多更小的库。
- 单表太大
- CRUD都成问题;
- 索引膨胀,查询超时;
- 解决方法:切分成多个数据集更小的表。
分库分表的方式方法
(1)单个库太大,这时如果是因为表多而数据多,使用垂直切分,根据业务切分成不同的库。
(2)如果是因为单张表的数据量太大,这时要用水平切分,即把表的数据按某种规则切分成多张表,甚至多个库上的多张表。
(3) 分库分表的顺序应该是先垂直分,后水平分。 因为垂直分更简单,更符合我们处理现实世界问题的方式。
- 垂直切分
- 垂直分表
- 也就是“大表拆小表”,基于列字段进行的。一般是表中的字段较多,将不常用的, 数据较大,长度较长(比如text类型字段)的拆分到“扩展表“。 一般是针对那种几百列的大表,也避免查询时,数据量太大造成的“跨页”问题。
- 垂直分库
- 垂直分库针对的是一个系统中的不同业务进行拆分,比如用户User一个库,商品Producet一个库,订单Order一个库。 切分后,要放在多个服务器上,而不是一个服务器上。为什么? 我们想象一下,一个购物网站对外提供服务,会有用户,商品,订单等的CRUD。没拆分之前, 全部都是落到单一的库上的,这会让数据库的单库处理能力成为瓶颈。按垂直分库后,如果还是放在一个数据库服务器上, 随着用户量增大,这会让单个数据库的处理能力成为瓶颈,还有单个服务器的磁盘空间,内存,tps等非常吃紧。 所以我们要拆分到多个服务器上,这样上面的问题都解决了,以后也不会面对单机资源问题。
- 数据库业务层面的拆分,和服务的“治理”,“降级”机制类似,也能对不同业务的数据分别的进行管理,维护,监控,扩展等。 数据库往往最容易成为应用系统的瓶颈,而数据库本身属于“有状态”的,相对于Web和应用服务器来讲,是比较难实现“横向扩展”的。 数据库的连接资源比较宝贵且单机处理能力也有限,在高并发场景下,垂直分库一定程度上能够突破IO、连接数及单机硬件资源的瓶颈。
- 垂直分表
- 水平拆分
- 水平分表
- 针对数据量巨大的单张表(比如订单表),按照某种规则(RANGE,HASH取模等),切分到多张表里面去。 但是这些表还是在同一个库中,所以库级别的数据库操作还是有IO瓶颈。不建议采用。
- 水平分库分表
- 将单张表的数据切分到多个服务器上去,每个服务器具有相应的库与表,只是表中数据集合不同。水平分库分表能够有效的缓解单机和单库的性能瓶颈和压力,突破IO、连接数、硬件资源等的瓶颈。
- 水平分表
分布式事务
指事务的操作位于不同的节点上,需要保证事务的 AICD 特性。
产生原因
-
数据库分库分表;
-
SOA 架构,比如一个电商网站将订单业务和库存业务分离出来放到不同的节点上。
应用场景
-
下单:减少库存、更新订单状态。库存和订单不在不同一个数据库,因此涉及分布式事务。
-
支付:买家账户扣款、卖家账户入账。买家和卖家账户信息不在同一个数据库,因此涉及分布式事务。
解决方案
1. 两阶段提交协议:二阶段提交(Two-phaseCommit)是指,在计算机网络以及数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法(Algorithm)。通常,二阶段提交也被称为是一种协议(Protocol))。在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。
- 准备阶段:事务协调者(事务管理器)给每个参与者(资源管理器)发送Prepare消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志,但不提交,到达一种“万事俱备,只欠东风”的状态。
- 协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。
- 参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)
- 各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个”同意”消息;如果参与者节点的事务操作实际执行失败,则它返回一个”中止”消息。
- 提交阶段:如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)
- 如果任一参与者节点在第一阶段返回的响应消息为”中止”,或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:
- 协调者节点向所有参与者节点发出”回滚操作(rollback)”的请求。
- 参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。
- 参与者节点向协调者节点发送”回滚完成”消息。
- 协调者节点受到所有参与者节点反馈的”回滚完成”消息后,取消事务。
- 当协调者节点从所有参与者节点获得的相应消息都为”同意”时:
- 协调者节点向所有参与者节点发出”正式提交(commit)”的请求。
- 参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
- 参与者节点向协调者节点发送”完成”消息。
- 协调者节点受到所有参与者节点反馈的”完成”消息后,完成事务。
- 如果任一参与者节点在第一阶段返回的响应消息为”中止”,或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:
- 二阶段的缺点:
- 同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
- 单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
- 数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。
- 二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
2.三阶段提交协议(Three-phase commit protocol)
与两阶段提交不同的是,三阶段提交有两个改动点。
(1)引入超时机制。同时在协调者和参与者中都引入超时机制。
(2)在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。
也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit
、PreCommit
、DoCommit
三个阶段。
- CanCommit阶段
- 事务询问 协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
- 响应反馈 参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No。
- PreCommit阶段
协调者根据参与者的反应情况来决定是否可以记性事务的PreCommit操作。根据响应情况,有以下两种可能。
- 假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
- 发送中断请求 协调者向所有参与者发送abort请求。
- 中断事务 参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断
- 假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。
- 发送预提交请求 协调者向参与者发送PreCommit请求,并进入Prepared阶段。
- 事务预提交 参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
- 响应反馈 如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
- doCommit阶段
该阶段进行真正的事务提交,也可以分为以下两种情况。
- 执行提交
- 发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
- 事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
- 响应反馈 事务提交完之后,向协调者发送Ack响应。
- 完成事务 协调者接收到所有参与者的ack响应之后,完成事务。
- 中断事务 协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。
- 发送中断请求 协调者向所有参与者发送abort请求
- 事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
- 反馈结果 参与者完成事务回滚之后,向协调者发送ACK消息。
- 中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。
3.2PC与3PC的区别
相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。
4.消息中间件:消息中间件也可称作消息系统 (MQ),它本质上是一个暂存转发消息的一个中间件。在分布式应用当中,我们可以把一个业务操作转换成一个消息,比如支付宝的余额转入余额宝操作,支付宝系统执行减少余额操作之后向消息系统发送一个消息,余额宝系统订阅这条消息然后进行增加余额宝操作。
- 消息的可靠性
- 消息的发送端的可靠性:发送端完成操作后一定能将消息成功发送到消息系统;
- 在本地数据建一张消息表,将消息数据与业务数据保存在同一数据库实例里,这样就可以利用本地数据库的事务机制。事务提交成功后,将消息表中的消息转移到消息中间件,若转移消息成功则删除消息表中的数据,否则继续重传。
- 消息的接收端的可靠性:接收端仅且能够从消息中间件成功消费一次消息。
- 保证接收端处理消息的业务逻辑具有幂等性:只要具有幂等性,那么消费多少次消息,最后处理的结果都是一样的。保证消息具有唯一编号,并使用一张日志表来记录已经消费的消息编号。
- 消息的发送端的可靠性:发送端完成操作后一定能将消息成功发送到消息系统;
负载均衡的算法与实现
算法
- 轮询:轮询算法把每个请求轮流发送到每个服务器上。下图中,一共有 6 个客户端产生了 6 个请求,这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。最后,(1, 3, 5) 的请求会被发送到服务器 1,(2, 4, 6) 的请求会被发送到服务器 2。该算法比较适合每个服务器的性能差不多的场景,如果有性能存在差异的情况下,那么性能较差的服务器可能无法承担多大的负载。
- 加权轮询:加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定的权值。上图中,服务器 1 被赋予的权值为 5,服务器 2 被赋予的权值为 1,那么 (1, 2, 3, 4, 5) 请求会被发送到服务器 1,(6) 请求会被发送到服务器 2。
- 最少连接:由于每个请求的连接时间不一样,使用轮询或者加权轮询算法的话,可能会让一台服务器当前连接数多大,而另一台服务器的连接过小,造成负载不均衡。例如上图中,(1, 3, 5) 请求会被发送到服务器 1,但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1;(2, 4, 6) 请求被发送到服务器 2,只有 (2) 的连接断开。该系统继续运行时,服务器 2 会承担多大的负载。最少连接算法就是将请求发送给当前最少连接数的服务器上。例如下图中,服务器 1 当前连接数最小,那么新到来的请求 6 就会被发送到服务器 1 上。
- 加权最小连接:在最小连接的基础上,根据服务器的性能为每台服务器分配权重,根据权重计算出每台服务器能处理的连接数。
- 随机算法:把请求随机发送到服务器上。和轮询算法类似,该算法比较适合服务器性能差不多的场景。
实现
- DNS 解析:使用 DNS 作为负载均衡器,根据负载情况返回不同服务器的 IP 地址。大型网站基本使用了这种方式最为第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。
- 修改 MAC 地址:使用 LVS(Linux Virtual Server)这种链路层负载均衡器,根据负载情况修改请求的 MAC 地址。
- 修改 IP 地址:在网络层修改请求的目的 IP 地址。
- HTTP 重定向:HTTP 重定向负载均衡服务器收到 HTTP 请求之后会返回服务器的地址,并将该地址写入 HTTP 重定向响应中返回给浏览器,浏览器收到后需要再次发送请求。
- 反向代理:正向代理:发生在客户端,是由用户主动发起的。比如翻墙,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端。反向代理:发生在服务器端,用户不知道代理的存在。
redis数据库介绍
Redis
是一种基于 键值对 的 NoSQL
数据库。与很多键值对数据库不同,Redis
提供了丰富的 值数据存储结构,包括 string
(字符串)、hash
(哈希)、list
(列表)、set
(集合)、zset
(有序集合)、bitmap
(位图)等等。
1. Redis的特性
-
速度快,最快可达到
10w QPS
(基于 内存,C
语言,单线程 架构); -
基于 键值对 (
key/value
) 的数据结构服务器。全称Remote Dictionary Server
。包括string
(字符串)、hash
(哈希)、list
(列表)、set
(集合)、zset
(有序集合)、bitmap
(位图)。同时在 字符串 的基础上演变出 位图(BitMaps
)和HyperLogLog
两种数据结构。3.2
版本中加入GEO
(地理信息位置)。 -
丰富的功能。例如:键过期(缓存),发布订阅(消息队列),
Lua
脚本(自己实现Redis
命令),事务,流水线(Pipeline
,用于减少网络开销)。 -
简单稳定。无外部库依赖,单线程模型。
-
客户端语言多。
-
持久化(支持两种 持久化 方式
RDB
和AOF
)。 -
主从复制(分布式的基础)。
-
高可用(
Redis Sentinel
),分布式(Redis Cluster
)和 水平扩容。
2. Redis的应用场景
- 缓存:合理的使用 缓存 能够明显加快访问的速度,同时降低数据源的压力。这也是
Redis
最常用的功能。Redis
提供了 键值过期时间(EXPIRE key seconds
)设置,并且也提供了灵活控制 最大内存 和 内存溢出 后的 淘汰策略。 - 排行榜:每个网站都有自己的排行榜,例如按照 热度排名 的排行榜,发布时间 的排行榜,答题排行榜 等等。
Redis
提供了 列表(list
)和 有序集合(zset
)数据结构,合理的使用这些数据结构,可以很方便的构建各种排行榜系统。 - 计数器:计数器 在网站应用中非常重要。例如:点赞数加
1
,浏览数 加1
。还有常用的 限流操作,限制每个用户每秒 访问系统的次数 等等。Redis
支持 计数功能(INCR key
),而且计数的 性能 也非常好,计数的同时也可以设置 超时时间,这样就可以 实现限流。 - 社交网络:赞/踩,粉丝,共同好友/喜好,推送,下拉刷新等是社交网站必备的功能。由于社交网站 访问量通常比较大,而且 传统的数据库 不太适合保存这类数据,
Redis
提供的 数据结构 可以相对比较容易实现这些功能。 - 消息队列:
Redis
提供的 发布订阅(PUB/SUB
)和 阻塞队列 的功能,虽然和专业的消息队列比,还不够强大,但对于一般的消息队列功能基本满足。
3.Redis几种数据结构
Redis存储的value支持多种数据类型,包括string(字符串)、list(链表)、hash(哈希)、set(无序集合)、zset(有序集合)。下面简单介绍一些这些数据类型的特点:
- string类型:string数据类型是二进制安全的,可以把图片、css文件、视频文件等保存在string中,为了提供网站的运行速度,可以用string类型缓存一些静态文件,如:图片、css文件等。string类型支持增量操作,可用作统计计算,如统计网站访问次数。
- list类型:list数据类型指key对应的value是一个双向链表结构,所以list类型提供链表支持的所有操作。list类型在互联网应用中非常有用,例如存放微博中“我的关注列表”或者论坛中所有的回帖ID。
另外,使用list还可以实现消息队列功能,减轻数据库的压力。消息队列类似于现实生活中的排队,每次有消息到达时就把消息放进队列尾部,取出消息时就从队列头部取出。要用list实现消息队列,先用rpush命令把消息放进队列尾部,然后使用lpop命令把消息从队列头部取出。 - set类型:set数据类型是一种无序集合。优点是快速查找元素是否存在,用于记录一些不能重复的数据。例如:网站中注册的用户名,如果要注册的用户名已经存在于集合中,就拒绝此用户注册。
set类型通常用于记录做过某些事情。例如:在投票系统中,每个用户一天只能投票一次,那么可以使用set类型来记录某个用户的投票情况,只需要以日期作为key,将用户ID作为集合中的元素即可。要查看某个用户今天是否投过票,只需以今天的日期作为key去集合中查询用户ID是否存在。 - zset类型:即sorted set类型。zset类型和set类型很相似,都是string类型元素的集合,不同的是zset类型属于有序集合,它通过一个double类型的整数score对集合中的元素进行排序。zset通过SkipList(跳跃表)和HashTable组合完成。SkipList负责排序,而HashTable负责保存数据。set类型能做的事情zset也可以做,而且zset还可以完成一些set不能做的事情,例如使用zset构建一个具有优先级的队列,这也是list类型不能实现的。zset类型在Web应用中非常有用。例如,排行榜应用中按“顶贴”次数排序,方法是:将排序的值设置成zset的score值,将具体数据设置成相应的value,用户每次按“顶贴”按钮时,只需执行zadd命令修改score的值。
- hash类型:hash类型是每个key对应一个HashTable,hash类型适合存储对象,例如用户信息对象,把用户ID作为key,可以把用户信息保存到hash类型中。
新建一个hash类型对象时,为了节省内存,Redis使用zipmap存储数据。这个zipmap并不是真正的HashTable,但是相比普通HashTable,zipmap节省不少内存。如果field或value的大小超出一定限制,Redis在内部自动将zipmap替换成正常的HashTable存储。修改配置文件的hash_max_zipmap_entries和hash_max_zipmap_value选项,可设置这两个限制。
4.事务处理
redis对事务的支持目前还比较简单。它只能保证一个client(客户端)发起的事务中的命令可以连续的执行,中间不会插入其他的client的命令。当一个client在一个连接中发出 multi 命令时,这个连接会进入一个事务,后续的命令不会立即执行,而是先放到一个队列中。当执行 exec 命令时,redis才会顺序执行队列中的所有命令,之后退出事务;当执行 discard 命令时,redis会废弃事务的命令队列并退出事务。
5. 持久化
Redis是基于内存的数据库,内存数据库有个严重的弊端:突然宕机或者断电时,内存中的数据就会丢失。为了解决这个问题,redi提供了两种持久化的方式:
- snapshotting(内存快照),默认方式
- 快照是默认的持久化方式。这种方式是将redis保存在内存中的数据以快照的方式写入二进制文件,默认的文件名为dump.rdb。可以通过修改配置文件,来设置自动快照。
vi /usr/local/redis/etc/redis.conf
save 900 1 # 每900秒,数据更改1次,就发起快照保存
save 300 10 # 每300秒,数据更改10次,则发起快照保存
save 60 10000 # 每60秒,数据更改10000,则发起快照保存
- 上面设置了多个内存快照保存方案,只要其中一个条件成立,Redis都会进行一次内存快照操作。
- Redis每隔一段时间进行一次内存快照操作,客户端使用save或者bgsave命令,告诉Redis需要做一次内存快照操作。save命令在主线程中保存内存快照,Redis使用单线程处理所有请求,执行save命令可能阻塞其他客户端请求,从而导致不能快速响应请求,所以建议不要使用save命令。另外要注意,内存快照每次都把内存数据完整地写入硬盘,而不是只写入增量数据。所以如果数据量很大,写入操作比较频繁,就会严重影响性能。
- 由于快照方式是在一定间隔时间执行一次快照保存,所以如果redis出现问题,就会丢失最后一次快照后的所有修改。
- append-only file(日志追加,缩写为aof)
- 日志追加(aof)方式比快照方式有更好的持久化性,如果启用了aof,Redis会将每一个收到的写命令通过write函数追加到文件appendonly.aof中,当Redis重启时,它会执行该文件中的所有命令,这样就可以在内存中重建整个redis数据库的内容。
- 另外,操作系统内核的 I/O 接口可能存在缓存,所以日志追加方式不可能立即写入文件,这样就有可能丢失部分数据。幸运的是,Redis提供了解决方法,通过修改配置文件,告诉Redis应该在什么时候使用fsync函数强制操作系统把缓存中的写命令写入磁盘中的日志文件。有以下三种方法:
appendonly yes #启用aof持久化方式
# appendfsync always #每次收到写命令就立即写入磁盘,性能最差,持久化最好
appendfsync everysec #每秒钟写入磁盘一次,在性能和持久化方面做了很好的折中
# appendfsync no #是否写入磁盘完全依赖操作系统,性能最好,持久化没保证
- 日志追加方式有效地降低了数据丢失的风险,同时也带来另一个问题,即持久化文件(appendonly.aof)不断膨胀。例如调用 incr nums 命令100次,文件就会保存100条该命令,其实99条都是多余的,因为要恢复数据只需要set nums 100。为了压缩日志文件,Redis提供了bgrewriteaof命令。当Redis收到此命令时,就使用类似于内存快照的方式将内存中的数据以命令的方式保存到临时文件中,最后替换原来的日志文件。
内存快照和日志追加,各有优缺点,选择哪种持久化方式需要自己衡量。也可以把这两种持久化方式都关闭,实现自己的持久化方式,如使用Berkeley DB或者Tokyo Cabinet。
6. 主从复制
主从复制(也叫主从同步)可以防止主机坏掉导致的网站不能正常运作的问题。Redis支持主从复制,而且配置也很简单。redis的主从复制可以让多个从服务器(slave server)拥有和主服务器(master server)相同的数据库副本。
- Redis主从复制的特点:
- 一个master可以拥有多个slave
- 多个slave除了可以连接同一个master外,还可以连接其他的slave
- 不会阻塞master,在slave同步数据时,master可以继续处理客户端的请求
- 提高了系统的伸缩性,比如多个slave专门用于客户端的读操作
- 可在master服务器上禁止数据持久化,而只在slave服务器上进行数据持久化操作
- Redis主从复制的原理:
- 主从复制设置很简单,设置好slave服务器后,slave自动和master建立连接,发送SYNC命令。无论是第一次同步建立的连接还是连接断开后重新建立的连接,master都会启动一个后台进程,将内存数据以快照方式写入文件中,同时master主进程开始收集新的写命令并且缓存起来。master后台进程完成内存快照操作后,把数据文件发给slave,slave将文件保存到磁盘上,然后将数据加载到内存中。接着master把缓存的命令发给slave,后续master收到的写命令都通过开始建立的连接发送给slave。当master与slave断开连接,slave自动重新建立连接。如果master同时收到多个slave发来的同步请求,其只启动一个进程写数据库镜像,然后发送给所有的slave。
- Redis主从复制的过程,分为两个阶段,第一个阶段如下:
(1)slave服务器主动连接到master服务器。
(2)slave服务器发送SYNC命令到master服务器请求同步数据。
(3)master服务器备份数据库到rdb文件。
(4)master服务器将该rdb文件传输给slave服务器。
(5)slave服务器清空数据库数据,把rdb文件数据导入数据库中。
完成第一阶段,接下来master服务器把用户所有更改数据的操作(写操作),通过命令的形式转发给slave服务器,slave服务器只需执行master服务器发送过来的命令就可以实现后续的同步效果。
分布式锁
Java 提供了两种内置的锁的实现,一种是由 JVM 实现的 synchronized 和 JDK 提供的 Lock,当你的应用是单机或者说单进程应用时,可以使用 synchronized 或 Lock 来实现锁。当应用涉及到多机、多进程共同完成时,那么这时候就需要一个全局锁来实现多个进程之间的同步。
使用场景:例如一个应用有手机 APP 端和 Web 端,如果在两个客户端同时进行一项操作时,那么就会导致这项操作重复进行。
1. 数据库分布式锁
(一)基于 MySQL 锁表
该实现方式完全依靠数据库唯一索引来实现。当想要获得锁时,就向数据库中插入一条记录,释放锁时就删除这条记录。如果记录具有唯一索引,就不会同时插入同一条记录。这种方式存在以下几个问题:
-
锁没有失效时间,解锁失败会导致死锁,其他线程无法再获得锁。
-
只能是非阻塞锁,插入失败直接就报错了,无法重试。
-
不可重入,同一线程在没有释放锁之前无法再获得锁。
(二)采用乐观锁增加版本号
根据版本号来判断更新之前有没有其他线程更新过,如果被更新过,则获取锁失败。
2. Redis 分布式锁
(一)基于 SETNX、EXPIRE
使用 SETNX(set if not exist)命令插入一个键值对时,如果 Key 已经存在,那么会返回 False,否则插入成功并返回 True。因此客户端在尝试获得锁时,先使用 SETNX 向 Redis 中插入一个记录,如果返回 True 表示获得锁,返回 False 表示已经有客户端占用锁。EXPIRE 可以为一个键值对设置一个过期时间,从而避免了死锁的发生。
(二)RedLock 算法
ReadLock 算法使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时还可用。
-
尝试从 N 个相互独立 Redis 实例获取锁,如果一个实例不可用,应该尽快尝试下一个。
-
计算获取锁消耗的时间,只有当这个时间小于锁的过期时间,并且从大多数(N/2+1)实例上获取了锁,那么就认为锁获取成功了。
-
如果锁获取失败,会到每个实例上释放锁。
3. Zookeeper 分布式锁
Zookeeper 是一个为分布式应用提供一致性服务的软件,例如配置管理、分布式协同以及命名的中心化等,这些都是分布式系统中非常底层而且是必不可少的基本功能,但是如果自己实现这些功能而且要达到高吞吐、低延迟同时还要保持一致性和可用性,实际上非常困难。
常用的sql语句
(1)建立索引
- 创建唯一索引:create index account_index on `table name `(`字段名`(length)
- 创建组合索引:create index account_index on `table name `(`字段名`,'字段名')
(2)数据库操作:
- 创建数据库:create database database_name;
- 删除数据库:drop datebase database_name;
(3)数据表的操作:
-
创建表: create table table_name(field1 int primary key,field2 varchar(20) not null ...)
-
删除表: drop table table_name
-
插入表: insert into table_name(field1,field2) values(value1,value2)
-
查询表: select * from table_name where 查询条件
-
添加列: alter table table_name add col_name varchar(20) not null
-
删除列: alter table table_name drop column col_name
-
修改列: alter table table_name modify column col_name varchar(50)
-
更新列: update table_name set col1=value1... where 条件...
(4)约束
-
种类:primary key(主键约束)、default(默认约束)、not null(非空约束)、unique(唯一约束)、foreign key(外键约束)、check(检查约束)
-
添加约束: alter table table_name add constraint 约束名 约束类型
比如: alter table student add constraint fk_1 foreign key(class_id) references class(class_id);
-
删除约束: alter table table_name drop 约束类型 约束名称 注意:删除主键时,应先删除引用了它的外键
比如: alter table student drop foreign key fk_1
(5)查询
- 简单查询:
- 无条件查询: select * from table_name;||select col1,col2,... from table_name;
- 条件查询: select * from table_name where 条件;
- 排序查询: select col1,col2,...from table_name where 条件 .. order by 列名 desc/asc desc:从大到小排序。asc:从小到大排序 ,默认是asc。
- 比如: select s_id,s_name,s_score from student where s_score>=60 order by s_id asc 译:查询出及格的学生,并按学生的id从小到大排序
- 模糊查询:查询关键字 like 主要使用 % 、 _ 、[ ] 三个字符 ,% 表示匹配0个或多个字符(通配符), _ 匹配一个字符,[ ] 匹配其中的一个(类似正则表达式)
- 例: select * from student where s_name like '张%' 译:查询学生中姓张的,两个字,三个字的都可以查出来,如:张三、张麻子
- 例: select * from student where s_name like '张_' 译:查询学生中姓张的,且只有两个字的,如:张三、张四
- 例: select * from student where s_name like '[张李王]三' 译:查询学生中姓名为:张三、李三、王三 的信息
- 分组查询: select * from table_name group by 列名 关键字 group by ,将统计的列中相同数据进行分组
- 比如: select s_score,count(*) '人数' from student group by s_score 译:查询学生中每个分数有多少人,就是对相同的成绩进行了分组
- 分组查询常用函数:
- max:求最大值 例: select s_name,max(math_score) from student group by s_name 查询数学成绩最高的学生姓名
- min:求最小值 例: select s_name,min(math_score) from student group by s_name 查询数学成绩最低的学生姓名
- avg:求平均值 例: select class_id,avg(math_score) from student group by class_id 查询每个班的平均数学成绩
- sum:求总数和 例: select sum(s_id) from student 查询表中一共有多少学生
- count:求总行数
-
having用法:筛选成组后的各种数据,它可以筛选真实表中没有的数据作为查询条件
-
比如: select s_name,sum(s_score) from student group by s_name having sum(s_score)>600 查询总成绩大于600分的学生,但我们表没有总分这个记录,只有每科的成绩,这时就可以用having了,where就不能来筛选总成绩大于600的学生了。
-
having和where的区别:
-
having:having对查询结果中的列发挥作用,筛选数据
-
wherer:where针对表中的列发挥作用,查询数据
-
-
-
limit用法:limit 主要是用于分页,limit n,m 表示从n+1开始取m条数据
比如: select * from student limit 2,5 表示取所有信息的 第3条后面的5条记录:3、4、5、6、7 - 简单的多表查询: select table1.*,table2.* from table1,table2 where 条件
- 子查询和连接查询
- where子查询:把内层查询结果当作外层查询的比较条件
- 比如: select s_name,s_score from student where s_score in (select s_score from student wheres_score>=60) 查询成绩及格的学生,后面括号里可以放子查询,也可以放已知的数据。
- from子查询:把子查询的结果作为一个表,进行再次查询
- 比如:查询成绩及格学生的姓名个班级,这里将子查询作为一个新表(st) 再进行查询 ,这里有班级表(calss)和学生表(student):select s_name,class_name from class,(select s_name,class_id from student where s_score>=60) as stu where class.class_id = stu.class_id
- exists子查询:把子查询结果拿到内层,看内层的查询是否成立
- 比如:查询班级中的学生姓名:select class_id,s_name from student where exists(select * from class where class.class_id=student.class_id)
- 连接查询
-
left join 左连接:以左表为准,去右表找数据,如果没有匹配的数据,则以null补空位
-
语法: select col1,col2,col3 from ta left join tb on 条件 ;on后面放连接的一些条件,跟where后面跟条件一样
-
例: SELECT class.*,s_id,s_name FROM class LEFT JOIN student ON class.class_id=student.class_id
-
-
right join 右连接:以右表为准,去左表找数据,如果没有匹配的数据,则以null补空位 和左连接相反
-
语法: select col1,col2,col3 from ta right join tb on 条件
-
例: SELECT class.*,s_id,s_name FROM student RIGHT JOIN class ONclass.class_id=student.class_id
-
-
inner join 内连接:查询的结果是所连接2个表的交集
-
语法: select ta1.*,ta2.* from ta1 inner join ta2 on 条件
-
例: SELECT class.*,s_id,s_name FROM student INNER JOIN class ON class.class_id=student.class_id
-
-
- where子查询:把内层查询结果当作外层查询的比较条件
其他问题
-
PreparedStatement可以防止sql注入的原因
sql注入的例子: where id = 03 or 1=1。
PreparedStatement中sql语句是预编译的,参数都先用?表示,之后只能设置?的值,但不能改变sql的结构,因此or 1=1在此便行不通。 -
数据库如何应对大规模写入和读取?
(1)读写分离
读写分离就是在主服务器上修改数据,数据会同步到从服务器,从服务器只能读取数据,不能写入。(2)分库分表
分表(解决单表数据量过大带来的查询效率下降):
水平切分:不修改数据库表结构,通过对表中数据的拆分而达到分片的目的,一般水平拆分在查询数据库的时候可能会用到 union 操作(多个结果并)。可以根据hash或者日期进行进行分表。
垂直切分:修改表结构,按照访问的差异将某些列拆分出去,一般查询数据的时候可能会用到 join 操作;把常用的字段放一个表,不常用的放一个表,把字段比较大的比如text的字段拆出来放一个表里面。
分库(提升并发能力):采用关键字取模的方法(3)使用nosql
-
char和vachar区别:
char是固定长度,存储需要空间12个字节,处理速度比vachar快,费内存空间,当存储的值没有达到指定的范围时,会用空格替代
vachar是不固定长度,需要存储空间13个字节,节约存储空间,存储的是真实的值,会在存储的值前面加上1-2个字节,用来表示真实数据的大小 -
数据库支持多有标准的SQL数据类型,主要分为三类
数值类型(tinyint,int,bigint,浮点数,bit)
字符串类型(char和vachar,enum,text,set)
日期类型(date,datetime,timestamp)枚举类型与集合类型
-
mysql慢查询
慢查询对于跟踪有问题的查询很有用,可以分析出当前程序里哪些sql语句比较耗费资源
慢查询定义:指mysql记录所有执行超过long_query_time参数设定的时间值的sql语句,慢查询日志就是记录这些sql的日志。
mysql在windows系统中的配置文件一般是my.ini找到mysqld
log-slow-queries = F:\MySQL\log\mysqlslowquery.log 为慢查询日志存放的位置,一般要有可写权限
long_query_time = 2 2表示查询超过两秒才记录 -
什么情况下适合建立索引?
1.为经常出现在关键字order by、group by、distinct后面的字段,建立索引
2.在union等集合操作的结果集字段上,建立索引,其建立索引的目的同上
3.为经常用作查询选择的字段,建立索引
4.在经常用作表连接的属性上,建立索引 -
mysql数据库的两种引擎
-
Innodb引擎
1:Innodb引擎提供了对数据库ACID事务的支持,并且实现了SQL标准的四种隔离级别,关于数据库事务与其隔离级别的内容请见数据库事务与其隔离级别这篇文章。
2:该引擎还提供了行级锁和外键约束,它的设计目标是处理大容量数据库系统,它本身其实就是基于MySQL后台的完整数据库系统
3:MySQL运行时Innodb会在内存中建立缓冲池,用于缓冲数据和索引
4:但是该引擎不支持FULLTEXT类型的索引,而且它没有保存表的行数,当SELECT COUNT(*) FROM TABLE时需要扫描全表。当需要使用数据库事务时,该引擎当然是首选。
5:由于锁的粒度更小,写操作不会锁定全表,所以在并发较高时,使用Innodb引擎会提升效率。但是使用行级锁也不是绝对的,如果在执行一个SQL语句时MySQL不能确定要扫描的范围,InnoDB表同样会锁全表。 -
MyIASM引擎
1:MyIASM是MySQL默认的引擎,但是它没有提供对数据库事务的支持,也不支持行级锁和外键,因此当INSERT(插入)或UPDATE(更新)数据时即写操作需要锁定整个表,效率便会低一些。
2:不过和Innodb不同,MyIASM中存储了表的行数,于是SELECT COUNT(*) FROM TABLE时只需要直接读取已经保存好的值而不需要进行全表扫描。如果表的读操作远远多于写操作且不需要数据库事务的支持,那么MyIASM也是很好的选择。两种引擎的选择
大尺寸的数据集趋向于选择InnoDB引擎,因为它支持事务处理和故障恢复。数据库的大小决定了故障恢复的时间长短,InnoDB可以利用事务日志进行数据恢复,这会比较快。主键查询在InnoDB引擎下也会相当快,不过需要注意的是如果主键太长也会导致性能问题。大批的INSERT语句(在每个INSERT语句中写入多行,批量插入)在MyISAM下会快一些,但是UPDATE语句在InnoDB下则会更快一些,尤其是在并发量大的时候。
-
- 数据库分库分表
https://www.cnblogs.com/butterfly100/p/9034281.html
- java如何连接数据库
- 加载驱动
- Class.forName(“指定数据库的驱动程序”)
- 获取数据库连接
- 通过DriverManager类创建数据库连接对象Connection
- Connection connection = DriverManager.geiConnection(“连接数据库的URL", "用户名", "密码”)
- 创建statement
- Statement 类的主要是用于执行静态 SQL 语句并返回它所生成结果的对象。通过Connection 对象的 createStatement()方法可以创建一个Statement对象。
- Statement statament = connection.createStatement();
- 执行SQL语句
- 调用Statement对象的相关方法执行相对应的 SQL 语句:通过execuUpdate()方法用来数据的更新,包括插入和删除等操作。
- statement.excuteUpdate( "INSERT INTO staff(name, age, sex,address, depart, worklen,wage)" + " VALUES ('Tom1', 321, 'M', 'china','Personnel','3','3000' ) ") ;
- 获取结果集
- 通过调用Statement对象的executeQuery()方法进行数据的查询,而查询结果会得到 ResulSet对象,ResulSet表示执行查询数据库后返回的数据的集合,ResulSet对象具有可以指向当前数据行的指针。通过该对象的next()方法,使得指针指向下一行,然后将数据以列号或者字段名取出。如果当next()方法返回null,则表示下一行中没有数据存在。
- ResultSet resultSel = statement.executeQuery( "select * from staff" );
- 释放资源(关闭statement和连接)
- 使用完数据库或者不需要访问数据库时,通过Connection的close() 方法及时关闭数据连接。
- 加载驱动