目录
数据库
数据库事务ACID
原子性
事务是最小的执行单位,不可分割的(原子的)。事务的原子性确保动作要么全部执行,要么全部不执行。
一致性
事务在执行前后数据库必须处于一致状态,多个事务对同一个数据读取的结果是相同的。
隔离性
并发访问数据库 时,一个用户的事务不被其他事务所干扰,各个事务不干涉内部的数据。
持久性
一个事务被提交之后,它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响
如何实现事务的ACID特性?
DBMS 采用日志来保证事务的原子性、一致性和持久性。日志记录了事务对数据库所做的更新,如果某个事务在执行过程中发生错误,就可以根据日志,撤销事务对数据库已做的更新,使数据库退回到执行事务前的初始状态。
DBMS 采用锁机制来实现事务的隔离性。当多个事务同时更新数据库中相同的数据时,只允许 持有锁的事务 能更新该数据,其他事务必须等待,直到前一个事务释放了锁,其他事务才有机会更新该数据。
事务之间的相互影响/并发一致性问题?
脏读(Dirty Read)
(针对未提交数据)如果一个事务中对数据进行了更新,但事务还没有提交,另一个事务可以 “看到” 该事务没有提交的更新结果,这样造成的问题就是,如果第一个事务回滚,那么,第二个事务在此之前所 “看到” 的数据就是一笔脏数据。 (脏读又称无效数据读出。一个事务读取另外一个事务还没有提交的数据叫脏读。)
解决办法:
把数据库的事务隔离级别调整到 READ_COMMITTED
丢失更新(Lost Update)
两个事务同时读取同一条记录,事务 A 先修改记录,事务 B 也修改记录(B 是不知道 A 修改过),当 B 提交数据后, 其修改结果覆盖了 A 的修改结果,导致事务 A 更新丢失。
不可重复读(Non-repeatable Read)
就是在一个事务范围内,两次相同的查询会返回两个不同的数据,这是因为在此间隔内有其他事务对数据进行了修改。
解决办法:
如果只有在修改事务完全提交之后才可以读取数据,则可以避免该问题。把数据库的事务隔离级别调整到REPEATABLE_READ
幻读(Phantom Read)
事务 T1 读取一条指定的 Where 子句所返回的结果集,然后 T2 事务新插入一行记录,这行记录恰好可以满足T1 所使用的查询条件。然后 T1 再次对表进行检索,但又看到了 T2 插入的数据。 (和可重复读类似,但是事务 T2 的数据操作仅仅是插入和删除,不是修改数据,读取的记录数量前后不一致)
幻读的重点在于新增或者删除 (数据条数变化)
解决办法:
如果在操作事务完成数据处理之前,任何其他事务都不可以添加新数据,则可避免该问题。把数据库的事务隔离级别调整到 SERIALIZABLE_READ
事务的隔离级别
为了尽可能的避免上述事务之间的相互影响,从而达到事务的四大特性,SQL 标准定义了 4 种不同的事务隔离级别(TRANSACTION ISOLATION LEVEL),即 并发事务对同一资源的读取深度层次,由低到高依次是 读取未提交(READ-UNCOMMITTED)、读取已提交(READ-COMMITTED)、可重复读(REPEATABLE-READ)、可串行化(SERIALIZABLE)
1,读取未提交
最低的隔离级别,一个事务可以读到另一个事务未提交的结果,所有的并发事务问题都会发生。
2,读取已提交
只有在事务提交后,其更新结果才会被其他事务看见,可以解决 脏读问题,但是不可重复读或幻读仍有可能发生。SQL server/Oracle 默认采用的是该隔离级别。
3,可重复读
在一个事务中,对于同一份数据的读取结果总是相同的,无论是否有其他事务对这份数据进行操作,以及这个事务是否提交,除非数据是被本身事务自己所修改。可以解决 脏读、不可重复读。MySQL 默认采用可重复读隔离级别。
4,可串行化
事务 串行化执行,隔离级别最高,完全服从 ACID,牺牲了系统的并发性,也就是说,所有事务依次逐个执行,所以可以解决并发事务的所有问题。
mysql,那innoDB是怎么解决幻读的?
mysql默认级别RR下 (MVCC只在读提交 可重复读两种隔离级别下工作)
在RR级别下,快照读是通过MVCC(多版本控制)和undo log来实现的,当前读是通过加record lock(记录锁)和gap lock(间隙锁)来实现的
1)当前读:
所谓当前读,是指加锁的select,update,delete等语句。在RR事务隔离级别下,数据库会使用next-key locks来锁住本条记录及索引区间。
以上述幻读的例子来说,在RR情况下,假设使用的是当前读,加锁了读select * from table where id>3 ,锁住的就是id=3这条记录及id>3这条区间范围,锁住索引记录之间的范围,避免范围内插入记录,避免产生幻读行记录。
2)普通读:
普通读是不加锁的读,解决幻读的手段是MVCC。
MVCC的目的就是多版本的并发控制,在数据库中的实现,就是为了解决读-写冲突的问题,它的实现原理主要是依赖记录中的 3个隐式字段、undo日志、read view 来实现的。
MVCC会给每行元组加一些辅助字段,记录创建版本号和删除版本号。
而每个事务在启动的时候,都有一个唯一的递增的版本号。开启新事务,事务版本号就会递增。
SELECT
读取创建版本小于或等于当前事务版本号,并且删除版本为空或大于当前事务版本号的记录。这样就可以保证在读取之前记录是存在的。
INSERT
将当前事务的版本号保存至行的创建版本号
UPDATE
新插入一行,并以当前事务的版本号作为新行的创建版本号,同时将原记录行的删除版本号设置为当前事务版本号。
DELETE
将当前事务的版本号保存至行的删除版本号
什么是MVCC?
mvcc,也就是多版本并发控制,是为了在读取数据时不加锁来提高读取效率和并发性的一种手段。
数据库并发有以下几种场景:
读-读:不存在任何问题。
读-写:有线程安全问题,可能出现脏读、幻读、不可重复读。
写-写:有线程安全问题,可能存在更新丢失等。
mvcc解决的就是读写时的线程安全问题,线程不用去争抢读写锁。
mvcc所提到的读是快照读,也就是普通的select语句。快照读在读写时不用加锁,不过可能会读到历史数据。
还有一种读取数据的方式是当前读,是一种悲观锁的操作。它会对当前读取的数据进行加锁,所以读到的数据都是最新的。主要包括以下几种操作:
select lock in share mode(共享锁)
select for update(排他锁)
update(排他锁)
insert(排他锁)
delete(排他锁)
MVCC的实现
1.回顾事务的特性
- 原子性:通过undolog实现。
- 持久性:通过redolog实现。
- 隔离性:通过加锁(当前读)&MVCC(快照读)实现。
- 一致性:通过undolog、redolog、隔离性共同实现。
幻读问题
快照读:通过mvcc,RR的隔离级别解决了幻读问题,因为每次使用的都是同一个readview。
当前读:通过next-key锁(行锁+gap锁),RR隔离级别并不能解决幻读问题。
next-key lock
这样,当你执行 select * from LOL where hero_title = '疾风剑豪' for update 的时候,就不止是给数据库中已有的 7 个记录加上了行锁,还同时加了 8 个间隙锁。这样就确保了无法再插入新的记录,也就是Session C在T4新增(10,'雪人骑士','努努','450') 行时,由于ID大于7,被间隙锁(7,+∞)锁住。
在一行行扫描的过程中,不仅将给行加上了行锁,还给行两边的空隙,也加上了间隙锁。MySQL将行锁 + 间隙锁组合统称为 next-key lock,通过 next-key lock 解决了幻读问题。
注意
next-key lock的确是解决了幻读问题,但是next-key lock在并发情况下也经常会造成死锁。死锁检测和处理也会花费时间,一定程度上影响到并发量。
MVCC带来的好处是?
**多版本并发控制(MVCC)**是一种用来解决 读-写 冲突的无所并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,也就是每个事务都有一个对应版本的快照,快照版本按照单向增长的时间戳来决定先后顺序。
在这样的情况下,读操作,我们只读该事务开始前的数据库快照,并不去读取正在修改的数据,我们读取事务开始前的最新版本。
所以解决了数据库在并发读取时的问题,即可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能,同时还可以解决脏读,不可重复读,幻读等事务隔离级别带来的问题。但不能解决更新丢失问题。
SQL 语句可分为以下几类:
数据定义语言 DDL(Data Definition Language):例如 CREATE,DROP,ALTER 等,对逻辑结构等有操作的,其中包括表结构,视图和索引。
数据查询语言 DQL(Data Query Language):即查询操作,以 SELECT 关键字为主,各种简单查询、连接查询等都属于 DQL。
数据操纵语言 DML(Data Manipulation Language):例如 INSERT,UPDATE,DELETE 等,对数据进行操作的。DQL 与 DML共同构建了多数初级程序员常用的 增删改查 操作,而查询是较为特殊的一种,被划分到 DQL 中。
数据控制语言 DCL(Data Control Language):例如 GRANT,REVOKE,COMMIT,ROLLBACK 等,对数据库安全性、完整性等有操作的,可以简单的理解为权限控制等。
MySQL 主从复制
主从复制是指将 主数据库(Master)中的 DDL 和 DML 操作通过二进制日志传输到 从数据库(Slave) 上,然后将这些日志重新执行(重做),从而使得从数据库的数据与主数据库保持一致。MySQL 支持单向、异步复制,复制过程中一个服务器充当主服务器,而一个或多个其它服务器充当从服务器。
主从复制的作用有:
当主数据库出现问题时,可以切换到从数据库;
可以进行数据库层面的读写分离,实现负载均衡;
可以在从数据库上进行实时数据备份。
MySQL 主从复制的工作原理
MySQL 的主从复制是一个 异步 的复制过程(一般情况下感觉是实时的),数据将从一个 MySQL 数据库(Master)复制到另外一个 MySQL 数据库(Slave),在 Master 与 Slave 之间实现整个主从复制的过程是由三个线程参与完成的,其中有两个线程(SQL 线程和 I/O 线程)在 Slave 端,另外一个线程( I/O 线程)在 Master 端。
基本原理流程
Master 端:打开二进制日志(binlog )记录功能 —— 记录下所有改变了数据库数据的语句,放进 Master 的 binlog 中;
Slave 端:开启一个 I/O 线程 —— 负责从 Master上拉取 binlog 内容,放进自己的中继日志(Relay log)中;
Slave 端:SQL 执行线程 —— 读取 Relay log,并顺序执行该日志中的 SQL 事件。
聚簇索引和非聚簇索引
索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息
聚簇索引,又称 聚集索引, 首先并不是一种索引类型,而是一种数据存储方式。具体的,聚簇索引指将 数据存储 和 索引 放到一起,找到索引也就找到了数据。
也叫聚簇索引,是一种数据存储方式(将索引和数据存储在一起),是按照每张表的主键构造一颗B+树,同时叶子节点中存放的就是整张表的行记录数据,也将聚集索引的叶子节点称为数据页。这个特性决定了索引组织表中数据也是索引的一部分,每张表只能拥有一个聚簇索引。B+树将数据存储与索引放到了一块,找到索引也就找到了数据,InnoDB聚集索引的叶子节点存储行记录,因此, InnoDB必须要有,且只有一个聚集索引:
(1)如果表定义了主键PK,则PK就是聚集索引;
(2)如果表没有定义PK,则第一个非空唯一键not NULL unique列是聚集索引;
(3)否则,InnoDB会创建一个隐藏的row-id作为聚集索引;
除了聚簇索引以外的其他索引,均称之为非聚簇索引(二级索引)。非聚簇索引也是 B+树结构,与聚簇索引的存储结构不同之处在于,非聚簇索引中不存储真正的数据行,只包含一个指向数据行的指针
回表查询
在非聚簇索引的叶子节点上存储的并不是真正的行数据,而是主键 ID,所以当我们使用非聚簇索引进行查询时,首先会得到一个主键 ID,然后再使用主键 ID 去聚簇索引上找到真正的行数据,我们把这个过程称之为回表查询。
先通过普通索引的值定位聚簇索引值,再通过聚簇索引的值定位行记录数据,需要扫描两次索引B+树,它的性能较扫一遍索引树更低
索引覆盖
只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表,速度更快
非聚簇索引不一定回表
不一定,这涉及到查询语句所要求的字段是否全部命中了索引,如果全部命中了索引,那么就不必再进行回表查询。
锁的分类
Mysql中锁的类型有哪些?
1. 基于锁的属性分类:共享锁、排他锁
2. 基于锁的粒度分类:行级锁(INNODB)、表级锁(INNODB、MYISAM)、页级锁(BDB引擎)、记录锁、间隙锁、临键锁。
3.基于表的状态分类:意向共享锁、意向排他锁。
1. 基于锁的属性分类:共享锁、排他锁
共享锁
又称为读锁,S锁,当一个事务为数据加上读锁之后,其他事务只能对该时间加读锁,而不能对数据加写锁,知道所有读锁释放之后,其他事务才能对其进行加持写锁。共享锁的特性主要是为了支持并发的读取数据,读取数据的时候不支持修改,避免重复读取的问题。
排他锁
排他锁又称写锁,简称X锁,当一个事务为数据加上写锁的时候,其他事务不能对其加上任何锁。直到排他锁释放后,才能对数据进行加锁。排他锁的目的是在数据修改的时候,不允许其他人读取和修改,避免了脏数据的问题。
2. 基于锁的粒度分类:行级锁(INNODB)、表级锁(INNODB、MYISAM)、页级锁(BDB引擎)、记录锁、间隙锁、临键锁。
表锁
对整个表进行上锁,当下一个事务访问该表的时候,必须等待上一个事务释放了表锁,才能对其表进行访问。不会会出现死锁。
粒度大、加锁简单、容易冲突
页锁
页锁介于标所和行锁之间,表锁速度快,但是冲突多,行锁冲突少,但是速度慢。一次锁定一组数据,并发一般,会出现死锁
行锁
锁住的是表中的一行或者多行记录,其他事务访问同一张表的时,只有被锁住的记录不能访问,其他的记录可以正常访问。会出现死锁。
粒度小,加锁比表锁困难,不易冲突,支持更高的并发
记录锁
行锁的一种,和行锁的不同是,只能锁某一行的记录,属于精准命中,命中字段为唯一索引。
粒度更小,加锁更困难,不易冲突,支持更高的并发。
间隙锁
对于键值在条件范围内但不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这些“间隙”进⾏加锁,这种锁机制就是所谓的间隙锁(NEXT-KEY)锁。表记录的一个区间加锁。
临键锁
也是行锁的一种,总结说就是记录所和间隙锁的组合,临键锁会把查询出的记录锁住,并且吧范围查询内的所有间隙也锁住。
3.基于表的状态分类:意向共享锁、意向排他锁。
意向共享锁
当一个事务试图对整个表进行加共享锁之前,首先需要获取这个表的意向共享锁。
意向排他锁
当一个事务试图对整个表进行加排他锁之前,需要先获得这个表的意向排他锁。
当加了这样一个状态,就相当于,告诉其他事务,我已经对整个表进行了共享锁会这我排他锁。避免了对整个索引的每个节点扫描是否加锁,而这个状态就是意向锁。
从数据库系统的角度,锁模式可分为://锁级别
共享锁(S):又叫 读锁。可以并发读取数据,但不能修改数据。也就是说当数据资源上存在共享锁时,所有的事务都不能对该数据进行修改,直到数据读取完成,共享锁释放。
排它锁(X):又叫 独占锁、写锁。对数据资源进行增删改操作时,不允许其它事务操作这块资源,直到排它锁被释放,从而防止同时对同一资源进行多重操作。
意向锁
事物B对一行数据使用行锁,当有另一个事物A对这个表使用了表锁,那么这个行锁就会升级为表锁,事务A在申请行锁(写锁)之前,数据库会自动先给事务A申请表的意向排他锁。当事务B去申请表的写锁时就会失败,因为表上有意向排他锁之后事务B申请表的写锁时会被阻塞。
当一个事务在需要获取资源的锁定时,如果该资源已经被排他锁占用,则数据库会自动给该事务申请一个该表的意向锁。如果自己需要一个共享锁定,就申请一个意向共享锁。如果需要的是某行(或者某些行)的排他锁定,则申请一个意向排他锁
从程序员角度分为两种://使用方式
一种是悲观锁:
悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据
悲观锁的实现方式是加锁,传统的关系型数据库里边就用到了很多悲观锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。加锁既可以是对代码块加锁(如Java的synchronized关键字),也可以是对数据加锁(如MySQL中的排它锁)
一种乐观锁:
顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以,不会上锁。但是在更新的时候会判断一下在此期间别人有没有更新这个数据,可以使用版本号/时间戳/待更新字段/所有字段等机制。
乐观锁的实现方式主要有两种:CAS机制和版本号机制
1.版本号机制:
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。就是通过version版本号作为一个标识,标识这个字段所属的数据是否被改变。
2,CAS算法:
即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数
- 需要读写的内存值 V
- 进行比较的值 A
- 拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
乐观锁的缺点:
1 ABA 问题
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。
但是在某些场景下,ABA却会带来隐患,例如栈顶问题:一个栈的栈顶经过两次(或多次)变化又恢复了原值,但是栈可能已发生了变化。
对于ABA问题,比较有效的方案是引入版本号,内存中的值每发生一次变化,版本号都+1;
在进行CAS操作时,不仅比较内存中的值,也会比较版本号,只有当二者都没有变化时,CAS才能执行成功。
2 循环时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。
3 只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效
乐观锁本身是不加锁的,只是在更新时判断一下数据是否被其他线程更新了;AtomicInteger便是一个例子。
有时乐观锁可能与加锁操作合作,例如,在前述updateCoins()的例子中,MySQL在执行update时会加排它锁。
乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
悲观锁适用于读比较少的情况下(多写场景),如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
自旋锁和互斥锁
最底层的两种锁实现就是互斥锁和自旋锁,许多高级的锁都是基于他们实现的。
加锁的目的是保证共享资源在任意时间内,只有一个线程访问,这样就可以避免多线程导致共享数据错乱的问题。
当有一个线程加锁成功后,其他线程就会加锁失败
互斥锁:
互斥锁加锁失败后,线程会释放 CPU ,给其他线程;
自旋锁:(自旋锁是一种特殊的互斥锁)
自旋锁加锁失败后,线程会忙等待,直到它拿到锁;
互斥锁加锁失败后,会从用户态陷入到内核态,让内核帮助我们切换线程,虽然简化了使用锁的难度,但是存在一定的性能开销成本
性能开销成本:两次线程上下文切换的成本。
1、当线程加锁失败时,内核将线程的状态从【运行】切换到睡眠状态,然后把CPU切换给其他线程运行;
2、当锁被释放时,之前睡眠状态的线程会变成就绪状态,然后内核就会在合适的时间把CPU切换给该线程运行;
线程切换的上下文?
当两个线程属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。
上下切换的耗时大概在几十纳秒到几微秒之间,如果锁住的代码执行时间比较短,可能上下文切换的时间比锁住的代码执行时间还要长。
若是能确定被锁住的代码执行时间很短,就不应该使用互斥锁,而应该选择自旋锁。
分布式锁?
Martin认为一般我们使用分布式锁有两个场景:
效率:使用分布式锁可以避免不同节点重复相同的工作,这些工作会浪费资源。比如用户付了钱之后有可能不同节点会发出多封短信。
正确性:加分布式锁同样可以避免破坏正确性的发生,如果两个节点在同一条数据上面操作,比如多个节点机器对同一个订单操作不同的流程有可能会导致该笔订单最后状态出现错误,造成损失。
分布式锁到底应该有哪些特点:
互斥性:和我们本地锁一样互斥性是最基本,但是分布式锁需要保证在不同节点的不同线程的互斥。
可重入性:同一个节点上的同一个线程如果获取了锁之后那么也可以再次获取这个锁。
锁超时:和本地锁一样支持锁超时,防止死锁。
高效,高可用:加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级。
支持阻塞和非阻塞:和ReentrantLock一样支持lock和trylock以及tryLock(long timeOut)。
支持公平锁和非公平锁(可选):公平锁的意思是按照请求加锁的顺序获得锁,非公平锁就相反是无序的。这个一般来说实现的比较少。
1,基于redis实现(利用redis的原子性操作setnx来实现)
Redis分布式锁简单实现
熟悉Redis的同学那么肯定对setNx(set if not exist)方法不陌生,如果不存在则更新,其可以很好的用来实现我们的分布式锁。对于某个资源加锁我们只需要
使用命令介绍:
(1)SETNX
SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
(2)expire
expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁
(3)delete
delete key:删除key
在使用Redis实现分布式锁的时候,主要就会使用到这三个命令。
实现思想:
(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
Redission
Javaer都知道Jedis,Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持。Redission也是Redis的客户端,相比于Jedis功能简单。Jedis简单使用阻塞的I/O和redis交互,Redission通过Netty支持非阻塞I/O
Redis小结
优点:对于Redis实现简单,性能对比ZK和Mysql较好。如果不需要特别复杂的要求,那么自己就可以利用setNx进行实现,如果自己需要复杂的需求的话那么可以利用或者借鉴Redission。对于一些要求比较严格的场景来说的话可以使用RedLock。
缺点:需要维护Redis集群,如果要实现RedLock那么需要维护更多的集群。
2,基于mysql实现(mysql数据库实现分布式锁主要有两种方式:一种是基于数据库表实现的乐观锁和悲观锁,另一种是基于Mysql自带的悲观锁)
悲观锁
Mysql实现分布式悲观锁:直接创建一张锁表,然后通过操作该表中的数据来实现了。当我们要锁住某个方法或资源时,我们就在该表中增加一条记录,想要释放锁的时候就删除这条记录。
因为我们对method_name做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁
①这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
②这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
③这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
④这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了
乐观锁
基于版本号
基于Mysql自带的悲观锁实现
利用for update加显式的行锁,这样就能利用这个行级的排他锁来实现分布式锁了,同时unlock的时候只要释放commit这个事务,就能达到释放锁的目的。
MySQL实现方式小结
适用场景: Mysql分布式锁一般适用于资源不存在数据库,如果数据库存在比如订单,那么可以直接对这条数据加行锁,不需要我们上面多的繁琐的步骤,比如一个订单,那么我们可以用select * from order_table where id = ‘xxx’ for update进行加行锁,那么其他的事务就不能对其进行修改。
优点:理解起来简单,不需要维护额外的第三方中间件(比如Redis,Zk)。
缺点:虽然容易理解但是实现起来较为繁琐,需要自己考虑锁超时,加事务等等。性能局限于数据库,一般对比缓存来说性能较低。对于高并发的场景并不是很适合。
3,基于Zookeeper实现(利用zk的临时顺序节点来实现)
ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:
(1)创建一个目录mylock;
(2)线程A想获取锁就在mylock目录下创建临时顺序节点;
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。
缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。
ZK小结
优点:ZK可以不需要关心锁超时时间,实现起来有现成的第三方包,比较方便,并且支持读写锁,ZK获取锁会按照加锁的顺序,所以其是公平锁。对于高可用利用ZK集群进行保证。
缺点:ZK需要额外维护,增加维护成本,性能和Mysql相差不大,依然比较差。并且需要开发人员了解ZK是什么。
B树和B+树的区别
B 树也称 B-树,全称为 多路平衡查找树 ,B+ 树是 B 树的一种变体。B 树和 B+树中的 B 是 Balanced (平衡)的意思。
目前大部分数据库系统及文件系统都采用 B-Tree 或其变种 B+Tree 作为索引结构
一棵m阶B树是一棵平衡的m路搜索树,它或者是空树,或者是满足下列性质的树:
每个结点最多 m 个子结点;
除了根结点和叶子结点外,每个结点最少有 m/2(向上取整)个子结点;
所有的叶子结点都位于同一层;
每个结点都包含 k 个元素(关键字),这里 m/2≤k<m,这里 m/2 向下取整;
每个节点中的元素(关键字)从小到大排列;
每个元素子左结点的值,都小于或等于该元素,右结点的值都大于或等于该元素。
B+ Tree 与 B-Tree 的结构很像,但是也有自己的特性:
所有的非叶子结点只存储 关键字信息;
所有具体数据都存在叶子结点中;
所有的叶子结点中包含了全部元素的信息;
所有叶子节点之间都有一个链指针。
使用 B+ 树的好处:
由于B+树的内部结点只存放键,不存放值. 因此在内存页中能够存放更多的key。 数据存放的更加紧密,具有更好的空间局部性。因此访问叶子节点上关联的数据也具有更好的缓存命中率。因此,一次读取,可以在同一内存页中获取更多的键,有利于更快地缩小查找范围。
B+ 树的叶结点由一条链相连,因此当需要进行一次 全数据遍历 的时候,B+ 树只需要使用 O(logN) 时间找到最小结点,然后通过链进行 O(N) 的顺序遍历即可;或者,在找 大于某个关键字或者小于某个关键字的数据的时候,B+树只需要找到该关键字然后沿着链表遍历即可, 而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。
B树也有优点,其优点在于由于B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速
索引失效的情况
在创建表时,InnoDB 存储引擎默认会创建一个主键索引,也就是聚簇索引,其它索引都属于二级索引。
MySQL 的 MyISAM 存储引擎支持多种索引数据结构,比如 B+ 树索引、R 树索引、Full-Text 索引。MyISAM 存储引擎在创建表时,创建的主键索引默认使用的是 B+ 树索引。
虽然,InnoDB 和 MyISAM 都支持 B+ 树索引,但是它们数据的存储结构实现方式不同。不同之处在于:
- InnoDB 存储引擎:B+ 树索引的叶子节点保存数据本身;
- MyISAM 存储引擎:B+ 树索引的叶子节点保存数据的物理地址;
6 种会发生索引失效的情况:
• 当我们使用左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx%这两种方式都会造成索引失效;
// 因为索引 B+ 树是按照「索引值」有序排列存储的,只能根据前缀进行比较。
• 当我们在查询条件中对索引列使用函数,就会导致索引失效。
• 当我们在查询条件中对索引列进行表达式计算,也是无法走索引的。
• MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。如果字符串是索引列,而条件语句中的输入参数是数字的话,那么索引列会发生隐式类型转换,由于隐式类型转换是通过 CAST 函数实现的,等同于对索引列使用了函数,所以就会导致索引失效。
• 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效。
• 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效
MYSQL Innodb B+树,MongDB B树
MongoDB 是文档型的数据库,是一种 nosql,它使用类 Json 格式保存数据
MongoDB不是传统的关系数据库,而是以BSON格式(可以认为是JSON)存储的nosql。目的是高性能,高可用性和易于扩展。
Mysql是关系型数据库,最常用的是数据遍历操作(join),而MongoDB它的数据更多的是聚合过的数据,不像Mysql那样表之间的关系那么强烈,因此MongoDB更多的是单个查询。
由于Mysql使用B+树,数据在叶节点上,叶子节点之间又通过双向链表连接,更加有利于数据遍历,而MongoDB使用B树,所有节点都有一个数据字段。只要找到指定的索引,就可以对其进行访问。毫无疑问,单个查询MongoDB平均查询速度比Mysql快。
MongoDB 最终选择使用 B 树的两个原因:
1,MySQL 使用 B+ 树是因为数据的遍历在关系型数据库中非常常见,它经常需要处理各个表之间的关系并通过范围查询一些数据;但是 MongoDB 作为面向文档的数据库,与数据之间的关系相比,它更看重以文档为中心的组织方式,所以选择了查询单个文档性能较好的 B 树,这个选择对遍历数据的查询也可以保证可以接受的时延;
2,LSM 树是一种专门用来优化写入的数据结构,它将随机写变成了顺序写显著地提高了写入性能,但是却牺牲了读的效率,这与大多数场景需要的特点是不匹配的,所以 MongoDB 最终还是选择读取性能更好的 B 树作为默认的数据结构;
MongoDb 3.2 之后,其使用了 B+ 树作为其数据结构
其实用的也是B+树,用b树磁盘I/O会很深,不过分析的确实很好,B树没办法解决磁盘I/O的问题,随机I/O很耗时
Mongodb与redis相比较:
mongoDB 源码语言是C++,redis也是C或C++,
mongodb 文件存储是BSON格式类似JSON,或自定义的二进制格式。
mongodb与redis性能都很依赖内存的大小,mongodb 有丰富的数据表达、索引;最类似于关系数据库,支持丰富的查询语言,redis数据丰富,较少的IO ,这方面mongodb优势明显。
mongodb不支持事务,靠客户端自身保证,redis支持事务,比较弱,仅能保证事物中的操作按顺序执行,这方面 redis优于mongodb。
mongodb对海量数据的访问效率提升,redis 较小数据量的性能及运算,这方面 mongodb性能优于redis .monbgodb 有mapredurce功能,提供数据分析,redis 没有 ,这方面 mongodb优于redis
REDIS
Redis用setnx+expire实现分布式锁存在什么隐患,如何改进?
从2.6.12版本后, 就可以使用set来获取锁, Lua 脚本来释放锁
用Redis实现分布式锁,2.6.12之前版本方案:setnx加锁,del释放锁,如果锁没释放,设置过期时间,到了时间,del释放锁。但是,这会存在一些问题。
setnx和expire不是原子操作。一旦redis宕机,expire没有设置成功,锁就无法释放。只有一个请求的setnx可以成功,任何一个请求的expire都可以成功。请求比较密集,过期时间一直刷新,导致锁一直有效。
超时后,删除其他线程的锁。在线程A执行过程中,锁已释放,A还未在执行业务,但是还未删除锁。线程B获取锁执行业务,线程A执行完,A误删B的锁。
多个线程并发获取锁、释放锁。同一时间有线程A、B在访问同一代码块。
对于上面的隐患,Redis已改善。下面,我们针对隐患逐一改善。
Redis2.6.12以上版本,可以用set获取锁。set可以实现setnx和expire,这个是原子操作。
Lua删除锁。Lua是原子操作。
让获取锁的线程开启一个守护线程,给线程还没执行完,又快要过期的锁续航。大概是这样的,线程A还没执行完,守护线程每当快过期时,延时expire时间。当线程A执行完,显示关闭守护线程。如果中间宕机,锁超过超时,守护线程也不在了,自动释放锁
redis的特点及优势
特点:
redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加在进行使用。
redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的储存
redis支持数据的备份,即master-slave模式的数据备份
优势:
性能极高——redis能读得的速度是110000次/s,写的速度是81000次/s。
丰富的数据类型——redis支持二进制案例的Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
原子——redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过multi和exec指令包起来。
丰富的特性-redis还支持publish/subscribe,通知,key过期等等特性
Redis为什么这么快
redis使用了IO多路复用,保证了redis在进行IO操作是依然能处理socket请求,不会再IO上浪费时间;
单线程机制也避免了不必要的上下文切换和锁机制;
而redis的每一次IO操作都是基于内存的,非常高效。
可以说,redis的这三个特性相辅相成,共同造就了redis的高并发。
我们在原始链表的基础上,每两个结点提取一个结点建立索引,我们把抽取出来的结点叫做索引层或者索引,这种通过对链表加多级索引的机构,就是跳表了。所以跳表的查询任意数据的时间复杂度为 O(2*log(n)),前边的常数 2 可以忽略,为 O(log(n))。
可以看到redis选择跳跃表而非红黑树作为有序集合实现方式的原因并非是基于并发上的考虑,因为redis是单线程的,选用跳跃表的原因仅仅是因为跳跃表的实现相较于红黑树更加简洁。
redis为什么采用跳表而不是红黑树
在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。
平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当;而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些。所以我们平常使用的各种Map或dictionary结构,大都是基于哈希表实现的。
从算法实现难度上来比较,skiplist比平衡树要简单得多。
红黑树红黑规则
节点不是黑色,就是红色(非黑即红)
根节点为黑色
叶节点为黑色(叶节点是指末梢的空节点 Nil或Null)
一个节点为红色,则其两个子节点必须是黑色的(根到叶子的所有路径,不可能存在两个连续的红色节点)
每个节点到叶子节点的所有路径,都包含相同数目的黑色节点(相同的黑色高度)
为什么用红黑树不用b+树
首先,红黑树是一种近似平衡二叉树(不完全平衡),结点非黑即红的树,它的树高最高不会超过 2*log(n),因此查找的时间复杂度为 O(log(n)),无论是增删改查,它的性能都十分稳定; 但是,红黑树本质还是二叉树,在数据量非常大时,需要访问+判断的节点数还是会比较多,同时数据是存在磁盘上的,访问需要进行磁盘IO,导致效率较低; 而B+树是多叉的,可以有效减少磁盘IO次数;同时B+树增加了叶子结点间的连接,能保证范围查询时找到起点和终点后快速取出需要的数据。
加分回答 红黑树做索引底层数据结构的缺陷 试想一下,以红黑树作为底层数据结构在面对在些表数据动辄数百万数千万的场景时,创建的索引它的树高得有多高? 索引从根节点开始查找,而如果我们需要查找的数据在底层的叶子节点上,那么树的高度是多少,就要进行多少次查找,数据存在磁盘上,访问需要进行磁盘IO,这会导致效率过低; 那么红黑树作为索引数据结构的弊端即是:树的高度过高导致查询效率变慢
Redis——缓存击穿、穿透、雪崩
1、缓存穿透:
(1)问题描述:key对应的数据并不存在,每次请求访问key时,缓存中查找不到,请求都会直接访问到数据库中去,请求量超出数据库时,便会导致数据库崩溃。如一个用户id不存在,数据库与缓存都不存在该id,此时黑客便可以利用此漏洞不断访问该id,造成数据库崩溃。
(2)解决方法:
①对空值缓存:如果一个查询数据为空(不管数据是否存在),都对该空结果进行缓存,其过期时间会设置非常短。
②设置可以访问名单:使用bitmaps类型定义一个可以访问名单,名单id作为bitmaps的偏移量,每次访问时与bitmaps中的id进行比较,如果访问id不在bitmaps中,则进行拦截,不给其访问。
③采用布隆过滤器:布隆过滤器可以判断元素是否存在集合中,他的优点是空间效率和查询时间都比一般算法快,缺点是有一定的误识别率和删除困难。
④进行实时监控:对于redis缓存中命中率急速下降时,迅速排查访问对象和访问数据,将其设置为黑名单。
2.缓存击穿:
(1)问题描述:key中对应数据存在,当key中对应的数据在缓存中过期,而此时又有大量请求访问该数据,缓存中过期了,请求会直接访问数据库并回设到缓存中,高并发访问数据库会导致数据库崩溃。
(2)解决方案:
①预先设置热门数据:在redis高峰访问时期,提前设置热门数据到缓存中,或适当延长缓存中key过期时间。
②实时调整:实时监控哪些数据热门,实时调整key过期时间。
③对于热点key设置永不过期。
3、缓存雪崩·:
(1)问题描述:key中对应数据存在,在某一时刻,缓存中大量key过期,而此时大量高并发请求访问,会直接访问后端数据库,导致数据库崩溃。
注意:缓存击穿是指一个key对应缓存数据过期,缓存雪崩是大部分key对应缓存数据过期
(2)解决方法:
①构建多级缓存机制:nginx缓存+redis缓存+其他缓存。
②设置过期标志更新缓存:记录缓存数据是否过期,如果过期会触发另外一个线程去在后台更新实时key的缓存。
③将缓存可以时间分散:如在原有缓存时间基础上增加一个随机值,这个值可以在1-5分钟随机,这样过期时间重复率就会降低,防止大量key同时过期。
④使用锁或队列机制:使用锁或队列保证不会有大量线程一次性对数据库进行读写,从而避免大量并发请求访问数据库,该方法不适用于高并发情况。
什么是缓存预热?
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统,这样就可以避免在用户请求的时候,先查询数据库,然后再将数据回写到缓存。
如果不进行预热, 那么 Redis 初始状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。
缓存降级
缓存降级是指缓存失效或缓存服务器挂掉的情况下,不去访问数据库,直接返回默认数据或访问服务的内存数据。
在项目实战中通常会将部分热点数据缓存到服务的内存中,这样一旦缓存出现异常,可以直接使用服务的内存数据,从而避免数据库遭受巨大压力。
降级一般是有损的操作,所以尽量减少降级对于业务的影响程度
数据库主库和从库不一致,常见有这么几种优化方案:
(1)业务可以接受,系统不优化
(2)强制读主,高可用主库,用缓存提高读性能
(3)在cache里记录哪些记录发生过写请求,来路由读主还是读从
慢查询
慢查询原因: 数据库慢查询,就是查询时间超过了我们设定的时间的语句
show variables like 'long%'; -- 上限连接数
show variables like '%max_connections%'; -- 上限连接数
要对慢查询进行优化,首先要搞清楚慢查询的原因,原因主要有三:
(1)加载了不需要的数据列
(2)查询条件没有命中索引
(3)数据量太大
二、优化方案
优化也是针对这三个方向的:
(1)先分析语句,看看是否加载了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,如果有这些问题,则对语句进行分析、重写
(2)分析语句的执行计划,获得其使用索引的情况,然后修改语句或修改索引,使得语句尽可能地命中索引
(3)如果对语句的优化都已经无法进行了,可以考虑是否是表中数据量太大引起的慢查询,如果是,则可以进行横向或者纵向分表
常见的慢查询优化
(1)索引没起到作用的情况(或者未加索引)
使用LIKE关键字的查询语句。在使用LIKE关键字进行查询的查询语句中,如果匹配字符串的第一个字符为“%”,索引不会起作用。只有“%”不在第一个位置索引才会起作用。
使用多列索引的查询语句。MySQL可以为多个字段创建索引,一个索引最多可以包括16个字段。对于多列索引,只有查询条件使用了这些字段中的第一个字段时,索引才会被使用。
(2)优化数据库表的结构
将字段很多的表拆解成多个表。
设置中间表。
sql优化手段
【MySQL】性能调优(三):SQL。慢查询日志及SQL优化建议_A minor的博客-CSDN博客
对慢SQL优化一般可以按下面几步的思路:
1、开启慢查询日志,设置超过几秒为慢SQL,抓取慢SQL
2、通过explain对慢SQL分析(重点)
3、show profile查询SQL在Mysql服务器里的执行细节和生命周期情况(重点)
4、对数据库服务器的参数调优
1.explain 分析SQL的执行计划
EXPLAIN是MySQl必不可少的一个分析工具,主要用来测试sql语句的性能及对sql语句的优化,或者说模拟优化器执行SQL语句。在select语句之前增加explain关键字,执行后MySQL就会返回执行计划的信息,而不是执行sql
SQL创建索引
1.创建普通索引
SQL CREATE INDEX 语法
在表上创建一个简单的索引。允许使用重复的值:
- CREATE INDEX index_name
- ON table_name (column_name);
创建唯一索引
SQL CREATE UNIQUE INDEX 语法
在表上创建一个唯一的索引。唯一的索引意味着两个行不能拥有相同的索引值。
CREATE UNIQUE INDEX index_name ON table_name (column_name);
索引添加约束
如果您希望以降序索引某个列中的值,您可以在列名称之后添加保留字 DESC:
CREATE INDEX PersonIndex ON Person (LastName DESC);
5.组合索引
假如您希望索引不止一个列,您可以在括号中列出这些列的名称,用逗号隔开:
CREATE INDEX PersonIndex ON Person (LastName, FirstName);
alter table table_name add index index_name(column_list)
1.覆盖索引是一种数据查询方式,不是索引类型
2.在索引数据结构中,通过索引值可以直接找到要查询字段的值,而不需要通过主键值回表查询,那么就叫覆盖索引
3.查询的字段被使用到的索引树全部覆盖到
常用索引优化策略:
覆盖索引:查询的列要被所建的索引覆盖
最左前缀匹配:联合索引中必须遵循”最左前缀匹配“,sql查询where条件字段必须从索引的最左前列开始匹配,不能跳过索引中的列。否者的话没法使用到索引
范围查询字段放在最后:联合索引,范围查询后,后面的字段不走索引
不对索引字段进行逻辑操作:在索引字段上进行计算、函数、类型转换(自动\手动)都会导致索引失效。
尽量全值匹配:使用like会使查询效率降低
like查询,左侧尽量不要加%:like以%开头,当前列索引无效(当为联合索引时,当前列和后续列索引不生效,导致索引使用不充分);当like前缀没有%,后缀有%时,索引有效。
尽量避免null:字段定义默认为null时,null索引生效,not null索引列索引均失效;字段定义为not null ,不允许为空时,null/not null索引列均失效。
尽量减少使用不等于:不等于操作符是不会使用引索的,不等于操作符包括:not,<>,!=。优化方法:数值型key<>改为key>0 or key <0.
字符类型务必加上单引号:varchar类型字段值不加单引号可能会发生数据隐式转化,自动转化为int型,使索引无效
or关键字前后尽量都为索引列:当or左右查询字段只有一个是索引时,会使索引失效
索引分类
索引有哪几种类型?
主键索引:数据列不允许重复,不允许为NULL,一个表只有一个主键。
唯一索引:数据列不允许重复,允许为NULL,一个表允许多个列创建唯一索引。
普通索引:基本的索引类型,没有唯一性的限制,允许为NULL值。
全文索引:是目前搜索引擎使用的一种关键技术,对文本的内容进行分词、搜索。
主键索引
主键是一种唯一性索引,但它必须指定为PRIMARY KEY,每个表只能有一个主键。
alert table tablename add primary key(`字段名`)
唯一索引:
索引列的所有值都只能出现一次,即必须唯一,值可以为空。
alter table table_name add unique index(`字段名`);
#alter table table_name drop index `字段名` , add unique index(`字段名`);#删除旧索引再添加
普通索引 :
基本的索引类型,值可以为空,没有唯一性的限制。
alter table table_name add index(`字段名`);
全文索引:
全文索引的索引类型为FULLTEXT。全文索引可以在varchar、char、text类型的列上创建。可以通过ALTER TABLE或CREATE INDEX命令创建。对于大规模的数据集,通过ALTER TABLE(或者CREATE INDEX)命令创建全文索引要比把记录插入带有全文索引的空表更快。MyISAM支持全文索引,InnoDB在mysql5.6之后支持了全文索引。
全文索引不支持中文需要借sphinx(coreseek)技术处理中文。
alter table 表名 add FULLTEXT(`字段名`);
覆盖索引:查询列要被创建的索引覆盖,不必读取数据行。
组合索引:多列值组成一个索引,用于组合搜索,效率大于索引合并。
创建索引的注意事项
1.选择合适的字段创建索引:
- 不为 NULL 的字段 :索引字段的数据应该尽量不为 NULL,因为对于数据为 NULL 的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为 NULL,建议使用 0,1,true,false 这样语义较为清晰的短值或短字符作为替代。
- 被频繁查询的字段 :我们创建索引的字段应该是查询操作非常频繁的字段。
- 被作为条件查询的字段 :被作为 WHERE 条件查询的字段,应该被考虑建立索引。
- 频繁需要排序的字段 :索引已经排序,这样查询可以利用索引的排序,加快排序查询时间。
- 被经常频繁用于连接的字段 :经常用于连接的字段可能是一些外键列,对于外键列并不一定要建立外键,只是说该列涉及到表与表的关系。对于频繁被连接查询的字段,可以考虑建立索引,提高多表连接查询的效率。
2.被频繁更新的字段应该慎重建立索引。
虽然索引能带来查询上的效率,但是维护索引的成本也是不小的。 如果一个字段不被经常查询,反而被经常修改,那么就更不应该在这种字段上建立索引了。
3.尽可能的考虑建立联合索引而不是单列索引。
因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗 B+树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升。
4.注意避免冗余索引 。
冗余索引指的是索引的功能相同,能够命中索引(a, b)就肯定能命中索引(a) ,那么索引(a)就是冗余索引。如(name,city )和(name )这两个索引就是冗余索引,能够命中前者的查询肯定是能够命中后者的 在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引。
5.考虑在字符串类型的字段上使用前缀索引代替普通索引。
前缀索引仅限于字符串类型,较普通索引会占用更小的空间,所以可以考虑使用前缀索引带替普通索引。
如果公共关键字在一个关系中是主关键字,那么这个公共关键字被称为另一个关系的外键。由此可见,外键表示了两个关系之间的相关联系。以另一个关系的外键作主关键字的表被称为主表,具有此外键的表被称为主表的从表。外键又称作外关键字。
什么是前缀索引
有时需要索引很长的字符列,它会使索引变大并且变慢,一个策略就是索引开始的几个字符,而不是全部值,即被称为 前缀索引,以节约空间并得到好的性能。使用前缀索引的前提是 此前缀的标识度高,比如密码就适合建立前缀索引,因为密码几乎各不相同。
什么是最左前缀匹配原则
在 MySQL 建立 联合索引(多列索引) 时会遵守最左前缀匹配原则,即 最左优先,在检索数据时从联合索引的最左边开始匹配。例如有一个 3 列索引(a,b,c),则已经对(a)、(a,b)、(a,b,c)上建立了索引。所以在创建 多列索引时,要根据业务需求,where 子句中 使用最频繁 的一列放在最左边。
根据最左前缀匹配原则,MySQL 会一直向右匹配直到遇到 范围查询(>、<、between、like)就停止匹配,比如采用查询条件 where a = 1 and b = 2 and c > 3 and d = 4 时,如果建立(a,b,c,d)顺序的索引,d 是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,并且 where 子句中 a、b、d 的顺序可以任意调整。
如果建立的索引顺序是 (a,b) ,那么根据最左前缀匹配原则,直接采用查询条件 where b = 1 是无法利用到索引的。
幂等
幂等的实现方案
幂等处理的是多次执行的问题,这并不仅仅出现在并发场景中,无论是顺序执行还是并发执行,都需要做好幂等,而幂等的核心是确保唯一性。实现幂等的方式有很多,比如建立数据库唯一索引、创建唯一数据和状态机约束、乐观锁等。
2.1 建立数据库唯一索引
在数据库中创建唯一索引,用作幂等记录,可以防止插入重复数据。以一个插入业务数据的场景为例,可通过业务维度定义唯一索引作为幂等记录,在插入数据方法中,首先查询该幂等记录是否存在,如果存在则直接返回第一次执行的结果,如果不存在则继续执行,并发场景中可能存在多个线程同时插入幂等记录的情况,这种情况下唯一索引可确保只有一个线程可以插入幂等记录成功,其余线程抛异常。插入幂等记录成功的线程可以继续执行后续操作,抛异常的线程执行事务回滚操作。
2.2 唯一数据
创建唯一数据的方式有很多种,比如基于tair或redis实现的分布式锁都属于创建唯一数据来实现幂等,以基于tair实现的分布式锁为例:
2.3 状态机约束
通过状态机的约束可以实现幂等,如果状态机已处于下一个状态,这时候不能往回跳转到上一个状态,通过状态机的跳转约束,可以做到有线状态机的跳转约束,
2.4 基于版本控制的乐观锁
基于版本控制的乐观锁有多种实现方式,比如基于数据库的版本控制乐观锁实现、基于redis的版本控制乐观锁实现等,以数据库的版本控制乐观锁为例:
Redis的io模型
常见的IO模型有四种
(1)同步阻塞IO(Blocking IO):即传统的IO模型
老李去火车站买票,排队三天买到一张退票。
耗费:在车站吃喝拉撒睡 3天,其他事一件没干。
(2)同步非阻塞IO(Non-blocking IO):默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。注意这里所说的NIO并非Java的NIO(New IO)库
老李去火车站买票,隔12小时去火车站问有没有退票,三天后买到一张票。
耗费:往返车站6次,路上6小时,其他时间做了好多事。
(3)IO多路复用(IO Multiplexing):即经典的Reactor设计模式,有时也称为异步阻塞IO,Linux中的epoll都是这种模型。
select/poll
老李去火车站买票,委托黄牛,然后每隔6小时电话黄牛询问,黄牛三天内买到票,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,黄牛手续费100元,打电话17次
epoll
老李去火车站买票,委托黄牛,黄牛买到后即通知老李去领,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,黄牛手续费100元,无需打电话
信号驱动I/O模型
老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,免黄牛费100元,无需打电话
4,信号驱动IO模型
应用程序可以创建一个信号驱动程序SIGIO,当数据没有处理好时,应用程序继续运行,不会被阻塞。当数据准备好之后,操作系统向应用程序发送信号,之后信号驱动程序就会执行,在信号处理函数中调用 IO函数处理数据。
(4)异步IO(Asynchronous IO):即经典的Proactor设计模式,也称为异步非阻塞IO。
异步I/O模型
老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李并快递送票上门。
耗费:往返车站1次,路上1小时,免黄牛费100元,无需打电话
所以总结起来,redis 支持多线程主要就是两个原因:
• 可以充分利用服务器 CPU 资源,目前主线程只能利用一个核
• 多线程任务可以分摊 Redis 同步 IO 读写负荷
Redis网络模型
单线程Reactor模型
基于NIO多路复用机制提出的高性能IO设计模式,把响应事件和业务进行分离,一个或多个线程处理IO事件,Redis6.0之前使用的此模型
多线程Reactor模型
单线程Reactor模型对事件的处理Handler是串行的,一旦一个事件的IO处理阻塞,后面的所有事件处理都需要阻塞等待;所以Redis6.0之后引入多线程Reactor模型,多线程Reactor模型通过线程池实现Handler是异步的
多线程多Reactor模型
MainReactor负责接收客户端连接,接收到后把连接分发给不同的SubReactor去处理,SubReactor负责IO读写事件,并将IO事件交给Handler去处理
Redis6.0 默认是否开启了多线程?
Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis.conf 配置文件
REDIS应用场景:
Redis的典型应用场景_Java识堂的博客-CSDN博客_redis使用场景
场景:
缓存
分布式锁(string)
计数器(string)
分布式全局唯一id(string)
消息队列(list)
排行版(zset)
Redis持久化策略
Redis的数据是存在内存中的,如果Redis发生宕机,那么数据会全部丢失,因此必须提供持久化机制。
- RDB(Redis DataBase)
- AOF(Append of file)
Redis 的持久化机制有两种,第一种是快照(RDB),第二种是 AOF 日志。快照是一次全量备份,AOF 日志是连续的增量备份。快照是内存数据的二进制序列化形式,在存储上非常紧凑,而 AOF 日志记录的是内存数据修改的指令记录文本。AOF 日志在长期的运行过程中会变的无比庞大,数据库重启时需要加载 AOF 日志进行指令重放,这个时间就会无比漫长。所以需要定期进行 AOF 重写,给 AOF 日志进行瘦身。
RDB是通过Redis主进程fork子进程,让子进程执行磁盘 IO 操作来进行 RDB 持久化,AOF 日志存储的是 Redis 服务器的顺序指令序列,AOF 日志只记录对内存进行修改的指令记录。即RDB记录的是数据,AOF记录的是指令
RDB和AOF到底该如何选择
不要仅仅使用 RDB,因为那样会导致你丢失很多数据,因为RDB是隔一段时间来备份数据
也不要仅仅使用 AOF,因为那样有两个问题,第一,通过 AOF 做冷备没有RDB恢复速度快; 第二,RDB 每次简单粗暴生成数据快照,更加健壮,可以避免 AOF 这种复杂的备份和恢复机制的 bug
用RDB恢复内存状态会丢失很多数据,重放AOP日志又很慢。Redis4.0退出了混合持久化来解决这个问题。将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。
RDB 的优势和劣势
优势:
RDB文件紧凑,全量备份,非常适合用于进行备份和灾难恢复。
生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。
RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
劣势:
RDB快照是一次全量备份,存储的是内存数据的二进制序列化形式,存储上非常紧凑。当进行快照持久化时,会开启一个子进程专门负责快照持久化,子进程会拥有父进程的内存数据,父进程修改内存子进程不会反应出来,所以在快照持久化期间修改的数据不会被保存,可能丢失数据。
AOF 机制
全量备份总是耗时的,有时候我们提供一种更加高效的方式AOF,工作机制很简单,redis会将每一个收到的写命令都通过write函数追加到文件中。通俗的理解就是日志记录
三种触发机制
- 每修改同步always:同步持久化 每次发生数据变更会被立即记录到磁盘 性能较差但数据完整性比较好
- 每秒同步everysec:异步操作,每秒记录 如果一秒内宕机,有数据丢失
- 不同no:从不同步
左连接,右连接,内连接
1.Left join:即左连接,是以左表为基础,根据ON后给出的两表的条件将两表连接起来。结果会将左表所有的查询信息列出,而右表只列出ON后条件与左表满足的部分。左连接全称为左外连接,是外连接的一种。
左连接,左边一行,右边两行,返回几行?
看on后面的条件,如果能匹配的话就返回两行
2.Right join:即右连接,是以右表为基础,根据ON后给出的两表的条件将两表连接起来。结果会将右表所有的查询信息列出,而左表只列出ON后条件与右表满足的部分。右连接全称为右外连接,是外连接的一种。
3.Inner join:即内连接,同时将两表作为参考对象,根据ON后给出的两表的条件将两表连接起来。结果则是两表同时满足ON后的条件的部分才会列出。
select *from table_a left join table_b on table_a.id=table_b.id;
MySQL中没有全外连接
对连接条件的两个表都不加限制。当两表有不匹配的元组时,另一边的相应列值取Null。
嵌套查询
在SQL语言中,一个SELECT-FROM-WHERE语句称为一个查询块。 将一个查询块嵌套在另一个查询块的 WHERE子句或HAVING短语的条件中的查询称为嵌套查询(nstedquery)。
上层的查询块称为外层查询或父查询,下层查询块称为内层查询或子查询。
带有IN谓词的子查询
在嵌套查询中,子查询结果往往是一个集合,所以为此IN是嵌套查询中最经常使用的谓词。
查询与“刘晨”在同一个系学习的学生
SELECT Sno,Sname,Sdept
FROM Student
WHERE Sdept IN
(SELECT Sdept
FROM Student
WHERE Sname='刘晨');
EXISTS谓词
EXISTS代表存在量词∃。带有EXISTS谓词的子查询不返回任何数据,只产生逻辑真值“true”或逻辑价值“false”。
数据库常用引擎
比较常用的是MYISAM,InnoDB和Memory
InnoDB:支持事务处理,支持外键,支持崩溃修复能力和并发控制。如果需要对事务的完整性要求比较高(比如银行),要求实现并发控制(比如售票),那选择InnoDB有很大的优势。如果需要频繁的更新、删除操作的数据库,也可以选择InnoDB,因为支持事务的提交(commit)和回滚
MYISAM:**插入数据快,空间和内存使用比较低。如果表主要是用于插入新记录和读出记录,那么选择MyISAM能实现处理高效率。如果应用的完整性、并发性要求比较低,也可以使用。
Memory:**所有的数据都在内存中,数据的处理速度快,但是安全性不高。如果需要很快的读写速度,对数据的安全性要求较低,可以选择MEMOEY。它对表的大小有要求,不能建立太大的表。所以,这类数据库只使用在相对较小的数据库表。