数据库学习

数据库学习

MYSQL

数据库三种删除方式

使用delete语句

delete 属于数据库操纵语言DML,表示删除表中的数据,

删除过程是每次从表中删除一行,并把该行删除操作作为事务记录在日志中保存

可以配合事件(transaction)和 回滚(rollback)找回数据,且自增不会被重置

delete 既可以对table也可以对view

可以全部删除,也可以按条件删除

-- 删除表中全部数据
 delete from 表名 
-- 按条件删除
 delete from 表名 where 条件

使用truncate 语句

truncate 属于数据库定义语言DDL,表示删除表中所有数据,DDL操作是隐性提交的!不能rollback

truncate一次性的从表中删除所有数据,不会保存到日志中,相当于直接删除整个表,再重新创建一个一模一样的表

使用truncate 删除的数据不能恢复

truncate 只能对table,执行速度快

-- 删除表中所有数据且不可恢复
 truncate from 表名

使用drop语句

drop 属于数据库定义语言DDL,表示删除表, 也可以用来删除数据库,删除表格中的索引。

执行速度,一般来说: drop> truncate > delete。

--  删除 表
 drop table 表名 
-- 删除数据库
 drop database 数据库名   
--用于 MySQL 的 DROP INDEX 语法
 ALTER TABLE table_name DROP INDEX index_name

MYSQL常见问题总结

MyISAM和InnoDB的区别

MyISAM是MYSQL的默认数据库引擎,但MyISAM不支持事务和行级锁,而且最大的缺陷就是崩溃后无法安全恢复。不过,5.5版本之后,MySQL引入了InnoDB(事务性数据库引擎),MySQL 5.5版本后默认的存储引擎为InnoDB。

大多数时候我们使用的都是 InnoDB 存储引擎,但是在某些情况下使用 MyISAM 也是合适的比如读密集的情况下。(如果你不介意 MyISAM 崩溃恢复问题的话)。

聚簇索引与非聚簇索引

聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据

  1. 使用记录主键值进行记录和页的排序,这包含了三个方面的含义
    • 页内的记录是按照主键的大小顺序拍成一个单向链表
    • 存放的页是一个双向链表
    • 存放目录项记录(只存储主键值和对应的页号)的页分为不同的层次,在同一个层次中的页也是根据页中目录项记录的主键大小顺序排成了一个双向链表
  2. B+树的叶子节点存储的是完整的用户记录

InnoDB会自动创建聚簇索引,索引即数据,数据即索引

二级索引

聚簇索引只能在搜索条件是主键值才能发挥作用,可以多建立几棵B+树,不同的B+树中的数据采用不同的排序规则

但是根据C2列(其他的列)并没有唯一性的约束,所以C2列的值可能分布在多个数据页中,所以需要根据主键值去聚簇索引中再查找一遍完成的用户记录(回表操作)

联合索引

也同时以多个列作为排序的规则,同时为多个列建立索引,按照C2和C3列大小进行排序

  • 先把各个记录和页按照C2列进行排序
  • 在记录的C2列相同的情况下,采用C3进行排序

需要保证在B+树的同一层内节点的目录项除了页号这个字段以外是唯一的。所以对于二级索引,实际上是由三个部分构成:

  • 索引列的值
  • 主键值
  • 页号

一个页面最少存储两条数据

MyISAM中的索引方案简单介绍

InnoDB中索引即数据,也就是聚簇索引的那棵B+树的叶子节点中已经把所有完整的用户记录都包含了,而MyISAM的索引方案虽然也使用了树形结构,但是却将索引和数据分开存储

会将索引的信息存储到一个称为索引文件的另一个文件中(全部都是二级索引)

Unix里启动服务器程序

  • mysqld这个可执行文件就代表着MYSQL服务器程序,运行这个可执行文件就可以直接启动一个服务器进程
  • mysqld_safe是一个启动脚本,间接的调用mysqld,而且顺便启动了另外一个监控进程,这个监控进程在服务器进程挂了的时候,可以帮助重启它。
  • mysqld.multi也可以运行多个服务器实例,也就是运行多个MySQL服务器进程

命令管道和共享内存

Windows下可以使用命名管道以及共享内存来进行客户端和服务端的通信,但是共享内存进程必须在同一台Windows主机中。

Unix域套接字文件

在同一台Unix的机器上,可以使用Unix域套接字文件来进行进程间通信,MYSQL服务器程序默认监控文件路径为/tmp/mysql.sock。可以在启动服务器程序时指定socket参数

mysqld --socket=/tmp/a.txt

Unix的配置文件

/etc/mysql/my.cnf

系统变量简介

对于大部分系统变量来说,它们的值可以动态修改,而不需要重新启动服务器

不同作用范围的系统变量

作用范围分为两种:

  • GLOBAL:全局变量,影响服务器的整体操作
  • SESSION:会话变量,影响某个客户端连接的操作

在服务器程序运行期间通过客户端程序设置系统变量的值:

SET [GLOBAL|SESSION] 系统变量名 =

如果在设置系统变量的语句中省略了作用范围,默认的作用范围就是SESSION

状态变量

它们的只能由服务器程序来设置,程序不能进行修改

show [GLOBAL|SESSION] STATUS [LIKE 匹配的模式]

InnoDB记录结构

InnoDB页简介

InnoDB采取的方式是:将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为16KB

InnoDB行格式

记录在磁盘上的存放方式称为行格式或者记录格式分别为CompactRedundantDynamicCompressed行格式

COMPACT行格式

在这里插入图片描述

变长字段长度列表

在Compact行格式中,把所有变长字段的真实数据占用的字节长度都存放在记录的开头部位,从而形成一个变长字段长度列表,各变长字段数据占用的字节数按照列的顺序逆序存放

InnoDB的一套规则

W:假设某一个字符集中表示一个字符最多需要使用的字节数为W。utf8为3,gbk为2,ascii为1
M:对于变长类型的VARCHAR(M),这种类型表示能存储最多M个字符(注意是字符而不是字节),所以这个类型能表示的字符串最多占用的字节数就是M*W
L:假设实际进行存储的字符串所用的字节数为L

如果该可变字段允许存储的最大字节数( M×W )超过255字节并且真实存储的字节数( L ) 超过127字节,则使用2个字节,否则使用1个字节。

变长字段长度列表中只存储值为非NULL的列内容所占用的长度,值为NULL的列的长度是不存储的

NULL值列表

Compact行格式把值为NULL的统一进行了管理。

  1. 首先统计了哪些列可以为NULL
  2. 如果表中没有允许存储NULL的列,则NULL值列表也不存在了

二进制位的值为1,则表示为NULL,为0则表示不为NULL

MYSQL规定 NULL值表必须是整数个字节的位进行表示,如果使用的二进制个数不是整数个字节位,就在高位补0

00000110 : 代表c4 c3为null, c1不为null 也是逆序进行排列
记录头信息

TODO

记录的真实数据

MYSQL会为每个记录默认添加一些列,来记录真实的数据

row_id 是在没有自定义主键以及Unique键的情况下才会添加该列

transaction_id 事务的ID

roll_pointer 回滚指针

CHAR(M)列的存储格式

当采用CHAR(M)类型的列来说,当采用定长字符集时,该列是不会被加到变长长度列表中,如果采用变长字符集时,会被加入

对于使用 utf8 字符集的 CHAR(10) 的列来说,该列存储的数据字节长度的范围是10~30个字节(字符使用utf8或者ascii码来进行编码)。即 使我们向该列中存储一个空字符串也会占用 10 个字节,这是怕将来更新该列的值的字节长度大于原有值的字节 长度而小于10个字节时,可以在该记录处直接更新,而不是在存储空间中重新分配一个新的记录空间,导致原有的记录空间成为所谓的碎片。

Dynamic和Compressed行格式

MYSQL5.7是默认行格式为Dynamic,在处理行溢出的时候,不会在记录的真实数据处存储真实数据的前768个字节,而是把所有的字节都存储在其他的页面上,只记录存储在其他页面的地址

Compressed行格式和Dynamic不同的一点是,会采用压缩算法对页面进行压缩。

行溢出数据

VARCHAR(M)最多能存储的数据

为了存储一个VARCHAR(M)类型的列,其实需要占用3部分存储空间

  • 真实数据
  • 真实数据占用字节的长度
  • NULL值标识,如果该列有NOT NULL则可以没有这部分的存储空间

在Compact和Reduntant行格式中,对于占用存储空间非常大的列,在记录的真实数据处只会存储该列的一部分数据,把剩余的数据分散存储在其他的页中,然后记录的真实数据处用20个字节存储指向这些页的地址

在本记录的真实数据处只会存储该列的768个字节的数据和一个指向其他页的地址然后把剩下的数据存放到其他页中,这个 过程也叫做 行溢出 ,存储超出 768 字节的那些页面也被称为 溢出页

InnoDB数据页结构

在这里插入图片描述

记录在页中的存储

存储的记录会以指定的行格式存储到User Records部分,每次都会从FreeSpace部分,申请一个记录大小的空间划分到User Records

记录头信息

delete_mask:标记该记录是否被删除
min_rec_mask:B+树的每层非叶子节点的最小记录都会添加该标记
n_owned:表示当前记录拥有的记录数
heap_no:
record_type:表示当前记录的类型,0代表普通记录,1表示B+树非叶节点记录,2表示最小记录,3表示最大记录
next_record:表示下一条记录的相对位置

对于完整的一条记录,比较记录的大小就是比较主键的大小,记录的构造由5字节大小的记录头信息和8字节大小的一个固定部分组成

最大记录和最小记录是固定的,并不存放在页的User Records部分

next_record

从当前记录的真实数据到下一条记录的真实数据的地址偏移量,所以记录按照主键从小到大的顺序形成了一个单链表。

Page Directory

是针对数据页记录的各种状态信息

  • 将所有正常的记录划分为几个组
  • 每个组的最后一条记录的头信息中的n_owned属性表示该组共有几条记录
  • 将每一个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地方,这个地方就是所谓的Page Directory也就是页目录

设计每一个分组中的记录条数为:对于最小记录只能有1个记录,最大记录所在的分组拥有的记录条数只能在1~8条之间,剩下的分组中记录的条数范围只能在是4~8条之间。

通过二分法确定该记录所在的槽,并找到该槽中主键值最小的那条记录。
通过记录的next_record属性遍历该槽所在的组中的各个记录

File Header

是针对各种类型的页都通用,不同类型的页都会以File Header作为第一组成部分

FILE_PAGE_SPACE_OR_CHKSUM:通过某种算法来计算比较短的值来代表一个很长的字节串

FILE_PAGE_PREV和FILE_PAGE_NEXT:一般数据页是需要存储这两种类型的,并不是所有的页都有上一个和下一个页的属性

File Tailer

InnoDB存储引擎会把数据存储到磁盘上,为了检测数据是否同步,为了检验页的完整性,如果File Header和File Tailer中不一致,则代表着同步的过程中出现了错误。

索引方案

新分配的数据页编号可能并不是连续的,这些页在存储空间里可能并不挨着,需要保证下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值的要求,所以在插入操作中可能会出现页分裂的情况

InnoDB中的索引方案

目录项中的两个列是主键和页号而已,所以他们复用了之前存储用户记录的数据页来存储目录项,为了和用户记录做一下区分,InnoDB通过record_type属性来区分用户记录还是目录项记录

不论是用户记录还是目录项都放在B+树这个数据结构中了。实际用户记录都存放在B+树的最底层的节点上了

索引的代价

  • 空间上的代价
    显而易见的,每建立一个索引都要为其建立一课B+树的索引(16KB)的存储空间
  • 时间上的代价
    增删改操作,会修改B+树的索引结构,需要维护每一个节点和记录的排序,会消耗性能

索引查找

索引为name + birthday + phone_number

全值匹配

如果搜索条件中的列和索引列一致的话,就称为全值匹配

匹配左边的列

只要包含左边就行,搜索条件中的列必须是联合索引中从最左边连续的列

select * from person_info where name = 'Ashburn' # 会走索引

select * from person_info where name = 'Ashburn' and birthday = '1990-09-27' 
匹配列前缀

匹配列前列的时候可以利用索引,但是匹配后缀则不能利用索引,所以需要将所有的数据进行逆序存储

匹配范围值

所有记录都是按照索引的值从小到大的顺序排好序的

如果对多个列同时进行范围查找的话,只有对索引最左边的那个列进行范围查找的时候才能用到B+树索引

精确匹配某一列并范围匹配另外一列

对于同一个联合索引来说,如果左边的列是精确查找,则右边的列可以进行范围查找

例如:

select * from person_info where name = 'Ashburn' AND birthday = '1980-01-01'

排序

B+树本身就是按照name, birthday, phone_number进行排序,所以直接从索引中提取数据,然后进行回表操作取出该索引中不包含的列就好了

使用联合索引的注意事项

order by子句后边的列的顺序也必须按照列的顺序给出,否则就使用不了索引列

不可以使用索引进行排序的几种情况

对于联合索引进行排序的场景,要求各个列顺序是一致的,各个列都是ASC规则,要么都是DESC规则

where子句中出现非排序使用到的索引列

排序的时候包含了非同一个索引的列

排序的列使用了复杂的表示式

二级索引 + 回表 以及全表扫描

查询优化器,进行选择是用二级索引(顺序IO)还是全表扫描(随机IO)。一般加上LIMIT语句是二级索引(减少回表的性能消耗)

数据库和文件系统的关系

InnoDB和MyISAM这样的存储引擎,都是把表存储在磁盘上的,而操作系统用来管理磁盘叫做文件系统

像 InnoDB、MyISAM 这样的存储引擎都是把表存储在文件系统上的

InnoDB是如何存储表数据的

系统表空间

InnoDB会在数据目录下建立ibdata1大小为12M的文件,这是一个自扩展的文件

独立表空间

MYSQL5.6以及之后的版本,并不会默认把各个表的数据存储到系统表空间中,而是为每一个创建一个独立表空间。

test.idb 文件用来存储test表中的数据和索引
test.frm

MyISAM是如何存储表数据

test.frm:视图文件
test.MYD : 代表的是数据文件
test.MYI:代表的是表的索引文件

独立表空间结构

对于16KB的页来说,连续64个页就是一个区,默认一个区占用1MB空间的大小

每256个区被划分为一组

为了减少随机I/O,一个就是在物理位置上连续的64个页

B+树叶子节点和非叶子节点进行了区别对待,也就是说叶子节点有自己独有的区,存放叶子节点的区的集合就算是一个段,非叶子节点类比

一个索引会生成2个段,一个叶子节点段,一个非叶子节点

碎片区(不属于任何一个段),并不是所以的页都是为了存储同一段的数据而存在的,而是碎片区中的页可以用于不同的目的

  • 在刚开始向表中插入数据的时候,段是从某个碎片区以单个页面为单位来分配存储空间的。
  • 当某个段已经占用了32个碎片区页面之后,就会以完整的区为单位来分配存储空间。

区的分类

  • 空闲的区 FREE
  • 有剩余空间的碎片区 FREE_FRAG
  • 没有剩余空间的碎片区 FULL_FRAG
  • 附属于某一个段的区 FESG

InnoDB设计了称为XDES Entry的结构,每一个区都对应着一个XDES Entry结构,这个结构记录了对应的区的一些属性

  • SegmentID

每一个段都有一个唯一的编号,Segment ID字段表示就是该区所在的段

  • List Node

这个部分可以将若干个XDES Entry串联成一个链表

  • State

这个字段表明区的状态,可选的值就是前面的4个状态

  • Page State Bitmap

一个区默认有64个页,有128个比特位被划分为64 个部分,每个部分2个比特位,对应区中的一个页。这两个比特位的第一个位表示对应的页是否是空闲的,第二个比特位还没有用。

XDES Entry链表

  • 把状态为FREE的区对应的XDES Entry结构通过List Node来连接成一个链表,称为FREE链表
  • FREE_FRAG的区的链表
  • FULL_FRAG的区的链表

当段中数据已经占满来32个零散的页后,就可以直接申请完整的区来插入数据了

Redis

Redis的五种常用数据类型

String

redis本身是Map,其中所有的数据都是采用key:value的形式存储的

key的部分永远都是字符串类型

如果value使用字符串以整数的形式展示,可以作为数字操作使用

set key value

get key

del key

mset key1 value1 key2 value2

mget key1 key2

strlen key

append key value # 追加信息到原始信息后面

关于string多指令和指令的使用,一次传输多个数据所用的时间会大于单个指令,但是减少了服务起响应的次数,所以需要权衡使用

string类型的扩展操作

当一个数据库表过大的时候,通常需要将表通过不同的主键id进行分割,Oracle数据库具有sequence设定,但是MYSQL并不具有类似的机制。


# Redis设置数值增加指定的范围
incr key 

incr key increment

incrbyfloat key increment

# Redis设置数值减少指定范围的值
decr key 

decrby key increment
string作为数值进行的操作

string在redis内部存储默认就是一个字符串

  • 当被当成数值时,原始数据不能转化超过的范围为LONG.MAX_VALUE
  • 由于操作是原子性操作,所以无需考虑并发带来的数据影响
  • float类型无法进行incr,会出现类型转换错误

tip

  • redis用于控制数据库主键id,为数据库主键提供生成策略,保障数据库表的主键唯一性
  • 此方案适用于所有数据库,且支持数据库集群
string时效性

设置数据具有指定的生命周期

setex key second value # 秒

psetex key milliseconds value # 毫秒

redis控制数据的生命周期,通过数据是否失效控制业务行为,适用于所有具有时效性限定控制的操作

redis string操作注意事项

数据的最大存储量为512MB
数值计算最大范围为Java中的Long值

string类型应用场景

redis中为大V用户设定用户信息,以用户的主键和属性值作为key,后台设定定时刷新

单条数据形式
user:id:3506728370:fans -> 12210947
user:id:3506728370:blogs -> 6164
user:id:3506728370:focus -> 83

json形式
set user:id:3506728370 {id:3506728370, fans:12210947,blogs:6164}

Hash

对象类数据的存储如果具有频繁的更新操作就显得笨重

对一系列存储的数据进行编组,方便管理,典型应用存储对象信息
在这里插入图片描述

Hash类型数据的基本操作

添加数据
hset key field value

获取数据
hget key field

hgetall key

删除数据
hdel key field1 [field2]

添加/修改多个数据
hmset key field1 value1 field2 value2

获取多个数据
hmget key field1 field2

获取哈希表中字段的数量
hlen key

获取哈希表是否存在指定的字段
hexists key field

获取哈希表中所有的字段名或字段值
hkeys user
hvals user

设置指定字段的数值增加指定范围的值
hincrby key field increment
hincrbyfloat key field increment

Redis key通用指令

del key # 删除key

exists key

type key

# 为指定的key设置有效期
expire key second

pexpire key milliseconds

expireat key timestamp # 加上时间戳

pexpireat key milliseconds-timestamp # 加上毫秒时间戳

ttl key # 显示key所剩下的时间 -1:为永久存在, -2:不存在key, 其他的时间为剩余存在的时间

pttl key # 显示key所剩下的时间以毫秒显示

persist key

keys pattern (正则表达式)

rename key newkey # 对于有newkey的数据,直接覆盖

renamenx key newkey # 如果有,则失败

sort # 对key进行排序

Redis数据库DB通用操作

# 切换数据库

select index (0-15) # 默认为0-15个数据库

# 其他操作
quit 
ping
echo message

# 数据移动
move key db # 必须保证原来的key存在

dbsize # 数据库key的大小
flushdb
flushall

持久化

利用永久性存储介质将数据进行保存

持久化过程保存什么

  • 将当前数据状态进行保存,快照形式,存储数据结果,存储格式简单,关注点在数据**(RDB)**
  • 将数据的操作过程进行保存,日志形式,存储操作过程,存储格式复杂,关注点在数据的操作过程**(AOF)**

RDB

RDB启动方式
save # 指令 不推荐使用

bgsave后台稍后进行,由于redis是单线程的,执行的命令都是按照顺序执行,save是阻塞的,对服务器性能影响很大
bgsave过程
自动执行

# 配置文件
save second changes

second: 监控时间范围
changes:监控key的变化量

其中关于key值的变化是指对数据产生了影响,不进行数据比对

save 10 2
set name hyy
set name hyy # 会执行save操作

save 10 2
get name
get name

RDB三种方式进行对比

在这里插入图片描述

rdb优缺点

优点
  • RDB是一个紧凑压缩的二进制文件,存储效率比较高
  • 内部存储的是redis在某一个时间点的数据快照,非常适合用于数据备份
  • RDB恢复数据的速度比AOF快
缺点
  • RDB无论是执行命令还是配置的方式都无法实时执行持久化,具有较大可能性丢失数据
  • bgsave每次执行的时候都会fork子进程,需要消耗一些性能
  • Redis RDB文件没有进行版本的统一管理

AOF

AOF持久化会把被执行的写命令写到AOF文件的末尾,记录数据的变化。

AOF配置文件
# appendonly参数开启AOF持久化存储
appendonly yes

# AOF持久化的文件名,默认是appendonly.aof
appendfilename "appendonly.aof"

# 同步的策略
appendfsync always # 记录每一条操作
appendfsync everysec # 每一秒记录一次
appendfsync no # 由系统控制
AOF的同步策略

AOF的同步策略是涉及到操作系统的writefsync函数的

为了提高文件写入效率,在现代操作系统中,当用户调用write函数,将一些数据写入文件时,操作系统通常会将数据暂存到一个内存缓冲区里,当缓冲区的空间被填满或超过了指定时限后,才真正将缓冲区的数据写入到磁盘里。
这样的操作虽然提高了效率,但也为数据写入带来了安全问题:如果计算机停机,内存缓冲区中的数据会丢失。为此,系统提供了fsync、fdatasync同步函数,可以强制操作系统立刻将缓冲区中的数据写入到硬盘里,从而确保写入数据的安全性。

AOF重写

运行时间长了之后,AOF文件中通常会有一些冗余命令,过期数据的命令、无效的命令、多个命令可以合并成一个命令。所以AOF文件是有压缩版本的

AOF文件重写并不需要对现有的AOF文件进行任何读取、分享和写入的操作,而是通过读取服务器当前的数据库状态来实现

在这里插入图片描述

  • 服务器也会fork一个子进程(避免出现数据不一致的问题)来创建一个新的AOF重写缓存文件
  • 在重写的期间,服务器进程继续处理命令请求,如果有写入的命令,追加到aof_buf的同时,也会增加aof_rewrite_bufAOF重写缓冲区
  • 当子进程完成重写之后,会给父进程一个信号,然后父进程会把AOF重写缓冲区的内容写进新的AOF临时文件中,再对新的AOF文件改名完成替换,这样可以保证新的AOF文件与当前数据库数据的一致性(仅有主进程写入命令到AOF缓存,对新的AOF文件进行改名,信号处理函数执行期间)会造成主进程阻塞。
触发AOF后台重写的条件
  • AOF重写可以由用户调用bgwriteaof手动触发
  • 服务器在AOF功能开启的情况下,会维持以下三个变量
    • 记录当前AOF文件大小的变量aof_current_size
    • 记录最后一次AOF重写之后,AOF文件大小的变量aof_rewrite_base_size
    • 增长百分比变量aof_rewrite_perc
  • 每次当serverCron(服务器周期性操作函数)函数执行时,会检查以下条件是否全部满足,如果全部满足的话,就触发自动的AOF重写操作:
    • 没有bgsave命令(RDB持久化)/AOF持久化在执行
    • 没有bgrewriteaof在进行
    • 当前AOF文件大小要大于server.aof_rewrite_min_size(默认为1MB),或者在redis.conf配置了auto_aof_rewrite_min_size大小
    • 当前AOF文件大小和最后一次重写后的大小的比率等于或者等于指定的增长百分比(在配置文件设置了auto_aof_rewrite_percentage参数,不设置默认为100%)
# 自动触发
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

表示当aof文件的体积大于64mb,且AOF文件的体积比上一次重写后的体积大了一倍(100%)会执行bgrewriteaof

触发AOF后台重写的实现
lpush s1 a 
lpush s1 b
lpush s1 c d

rpush d c b a 

要使用尽量少的命令来记录list键的状态,最简单的不是去读取和分析AOF文件中的内容,而是直接读取list键在数据库中的当前值,然后用一条rpsuh命令来代替之前的命令

实际上为了避免执行命令时造成客户端输入缓冲去溢出,重写程序在处理list hash set zset时,会检查键所包含的元素的个数,如果元素的数量超过了
redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值,那么重写程序会使用多条命令来记录键的值,而不是单使用一条命令。该常量默认值是64即每条命令设置的元素的个数 是最多64个,使用多条命令重写实现集合键中元素数量超过64个的键

RDB和AOF方式对比

在这里插入图片描述

如何选择RDB和AOF
  • 对数据比较敏感,建议使用默认的AOF持久化方案
    AOF持久化策略使用everysecond,每秒种fsync一次,该策略redis仍然可以保持很好的处理性能,当出现问题时,最多丢失0~1秒的数据
  • 数据呈现阶段有效性,建议使用RDB持久化方案,阶段点数据恢复通常采用RDB方案
  • 双保险策略,同时开启RDB和AOF,重启之后,Redis优先使用AOF来恢复数据,降低丢失数据的量

关于redis是否需要持久化

redis持久化,要根据具体业务具体分析,一般数据库中存在的就不需要持久化

Redis事务

执行事务

multi

# 一系列操作

exec 执行事务

discard 取消事务

事务的注意事项

定义事务的过程中,命令格式输入错误怎么办?

  • 语法错误
    指令书写格式有误

  • 处理结果
    整体事务中所有命令均不会被执行,包括那些正确的语法

  • 运行错误

  • 处理结果
    能够正确运行的命令会执行,运行错误的命令不会执行
    已经执行完毕的命令对应的数据不会自动回滚,需要程序员自己在代码中实现

Redis锁

事务锁
基于特定条件的事务执行——锁

业务场景1

商品卖完了需要补货,4个业务员都需要进行补货。补货的操作是一系列的操作,牵扯到多个连续操作

业务分析

  • 多个客户端有可能同时操作一组数据,并且该数据一旦被操作修改后,将不适用于继续操作
  • 在操作之前锁定要操作的数据,一旦发生变化,终止当前操作
解决方案
  • 对key添加监视锁,在执行exec前如果key发生了变化,终止事务的执行
watch key1 [key2...]
  • 取消对所有key的监视
unwatch
基于特定条件的事务执行——分布式锁

业务场景2
秒杀的最后一件商品,不被多人同时购买

业务分析

  • 使用watch监控一个key有没有改变不能解决问题,此处监控的是具体的数据
  • 虽然redis是单线程的,但是多个客户端对同一个数据同时进行操作时,如何避免不被同时修改?

解决方案

使用setnx设置一个公共锁

setnx lock-key value

利用setnx命令的返回值特征,有值则返回设置失败,无值则返回设置成功

  • 对于返回设置成功的,拥有下一步业务操作的权利
  • 对于返回失败的,不具有控制权,排队和等待

操作完毕通过del操作释放掉锁

del lock-key

业务场景3
依赖分布式锁的机制,如果某个用户操作时对应的客户机宕机,此时已经获取到锁了,如何解决?

业务分析

  • 由于锁操作由用户控制加锁,必定会存在加锁后未解锁的风险
  • 需要解锁操作不能仅依赖用户控制,系统级别要给出对应的保底处理方案

解决方案

使用expire为锁key添加时间限定,到时不释放,放弃锁

具体的时间需要业务测试后确认

锁时间设定推荐:最大耗时* 120%+平均网络延迟*110%
如果业务最大耗时<<网络平均延迟,通常为2个数量级,取其中单个耗时较长即可

expire lock-key second
pexpire lock-key millisenconds

Redis分布式锁出现的问题

setnx 和 expire的非原子性

如果setnx成功,在设置锁超时时间后,服务器挂掉、重启或网络问题,导致expire命令没有执行,锁没有设置超时时间变成死锁。

在这里插入图片描述

锁误解除

如果线程 A 成功获取到了锁,并且设置了过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁;随后 A 执行完成,线程 A 使用 DEL 命令来释放锁,但此时线程 B 加的锁还没有执行完成,线程 A 实际释放的线程 B 加的锁。

通过在value中设置当前线程加锁的标识,在删除之前验证key对应的value,判断锁是否是当前线程持有的

在这里插入图片描述

超时解锁导致并发

如果线程 A 成功获取锁并设置过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁,线程 A 和线程 B 并发执行。
在这里插入图片描述
A、B 两个线程发生并发显然是不被允许的,一般有两种方式解决该问题:

  • 将过期时间设置足够长,确保代码逻辑在锁释放之前能够执行完成。
  • 为获取锁的线程增加守护线程,为将要过期但未释放的锁增加有效时间。
可重入锁

TODO

无法等待锁释放

上述命令执行都是立即返回的,如果客户端可以等待锁释放就无法使用。

可以通过客户端轮询的方式解决该问题,当未获取到锁时,等待一段时间重新获取锁,直到成功获取锁或等待超时。这种方式比较消耗服务器资源,当并发量比较大时,会影响服务器的效率。
另一种方式是使用 Redis 的发布订阅功能,当获取锁失败时,订阅锁释放消息,获取锁成功后释放时,发送锁释放消息。如下:

在这里插入图片描述

Redis删除策略

数据删除策略的实现

在这里插入图片描述

数据删除的策略

在内存占用与CPU占用之间寻找一种平衡

定时删除

创建一个定时器,当key设置有过期时间,且过期时间到达时,由定时器任务立即执行对键的删除操作

以时间换空间

惰性删除

数据到达过期时间,不做处理,等下次访问该数据时

  • 如果未过期,返回数据
  • 发现已过期,删除,返回不存在

以空间换时间

定期删除
  • redis启动服务器初始化是,读取配置server.hz的值,默认为10
  • 每秒执行server.hz次serverCron()

在这里插入图片描述

  • 周期性轮询redis库中时效性数据,采用随机抽取的策略,利用过期数据占比的方式删除频度

CPU性能占用设置有峰值,检测频度可自定义设置
内存压力不是很大,长期占用内存的冷数据会被持续清理

(比如每秒都花费固定的CPU资源维护内存,比如每秒花费0.25秒)

在这里插入图片描述

逐出算法

当新数据进入redis时,如果内存不足怎么办?

  • redis使用内存存储数据,在执行每一个命令前,会调用freeMemoryIfNeeded()检测内存是否充足。如果内存不满足新加入数据的最低存储要求,redis要临时删除一些数据为当前指令清理存储空间。清理数据的策略称为逐出算法。
  • 逐出数据的过程不是100%能够清理出足够的可使用的内存空间,如果不成功则反复执行。当对所有数据尝试完毕后,如果不能达到内存清理的要求,将出现OOM错误信息
影响数据逐出的相关配置
  • 最大可以使用内存

占用物理内存的比例,默认为0,表示不限制。生产环境中根据需求设定,通常设置在50%以上

maxmemory
  • 每次选取待删除数据的个数

选取数据时并不会全库扫描,导致严重的性能消耗,降低读写性能。因此采用随机获取数据的方式作为待检测删除数据

maxmemory-samples
  • 删除策略

达到最大内存后的,对被挑选出来的数据进行删除的策略

maxmemory-policy
检查易失数据(可能会过期的数据集server.db[i].expires)

volatile-lru:挑选最近最少使用的数据淘汰
volatile-lfu:挑选最近使用次数最少的数据淘汰
volatile-ttl :挑选将要过期的数据淘汰
volatile-random:任意选择数据淘汰

检测全库数据(所有数据集server.db[i].dict)

allkeys-lru:挑选最近最少使用的数据态太
allkeys-lfu:挑选最近使用次数最少的数据淘汰
allkeys-random:任意选择数据淘汰
放弃数据驱逐
no-enviction:禁止驱逐数据(redis4.0默认策略),会引发错误OOM

maxmemory-policy volatile-lru

LRU和LFU算法实现(Java)

Redis特殊数据类型

Bitmap

HyperLogLog

GEO

Redis 主从复制

单机redis风险与问题

  • 问题1 机器故障
    现象:硬件故障,系统崩溃
    本质:数据丢失,很可能对业务造成灾难性打击

  • 问题2 容量瓶颈
    现象:内存不足,从16G升级到64G,无限升级内存
    本质:硬件条件跟不上

多台服务器链接方案

在这里插入图片描述

  • 提供多数据方:master
    主服务器,主节点,主库
    主客户端
  • 接收数据方:slave
    从服务器,从节点,从库
  • 需要解决的问题
    数据同步
  • 核心工作
    将master的数据复制到slave中

主从复制

主从复制即将master中的数据即时有效地复制到slave中
一个master可以拥有多个slave,一个slave只对应一个master

高可用集群

采用树型结构,slave帮助master分担压力
在这里插入图片描述

主从复制的作用
  • 读写分离:master写,slave读,提高服务器的读写负载能力
  • 负载均衡:基于主从结构,配合读写分离,由slave分担master负载,并根据需求的变化,改变slave的数量,通过多个从节点分担数据读取负载(写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),大大提高Redis服务器并发量与数据吞吐量
  • 故障恢复:当master出现问题时,由slave提供服务,实现快速的故障恢复
  • 数据冗余:实现数据热备份,时持久化之外的一种数据冗余方式
主从复制的工作流程
  • 建立连接阶段(准备阶段)
  • 数据同步阶段
  • 命令传播阶段

在这里插入图片描述
在这里插入图片描述
最后的状态是:

  • slave保存了master的地址和端口
  • master保存了slave的地址和端口
主从链接(slave 链接 master)
  • 方式一:客户端发送命令
slaveof <master ip> <master port>
  • 方式二:启动服务器参数
redis-server -slaveof <master ip> <master port>
  • 方式三:服务器配置
slaveof <master ip> <master port>
主从断开链接
  • 客户端发送命令
slaveof no one
数据同步阶段工作流程

在这里插入图片描述

  1. master创建RDB同步数据(全量复制部分)
  2. slave恢复RDB同步数据
  3. master发送增量(部分)复制的数据
  4. slave恢复AOF重写数据
数据同步阶段master说明
  1. 如果master数据量巨大,数据同步阶段应该避免流量高峰期,避免造成master阻塞,影响业务的正常执行
  2. 复制缓冲区大小设定不合理,会导致数据溢出,该缓冲区也会记录一个offset来说明主从命令的执行的长度,如果主从节点的offset的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制,致使slave陷入死循环状态。

例如如果网络中断的平均时间是60s,而主节点平均每秒产生的写命令(特定协议格式)所占的字节数为100KB,则复制积压缓冲区的平均需求为6MB,保险起见,可以设置为12MB,来保证绝大多数断线情况都可以使用部分复制。

repl-backlog-size 1mb
  1. master单机内存占用主机内存的比例不应该过大,建议使用50%-70%的内存,留下30%-50%的内存用于执行bgsave命令和创建复制缓冲区
数据同步阶段slave说明
  1. 为了避免slave进行全量复制,部分复制时服务器响应阻塞或数据不同步,建议关闭此期间的对外服务
slave-server-stale-data yes | no
  1. 数据同步阶段,master发送给slave信息可以理解master是slave的一个客户端,主动向slave发送命令
  2. 多个slave同时对master请求数据同步,master发送的RDB文件增多,会对带宽造成巨大冲击,如果master带宽不足,因此数据同步需要根据业务需求,适量错峰
  3. slave过多时,建议调整拓扑结构,由一主多从结构变为树状结构,中间即是master,也是slave。注意使用树状结构时,由于层级深度,导致深度越高的slave与最顶层master间数据同步延迟较大,数据一致性变差,应谨慎选择
命令传播阶段
  • master数据库状态被修改后,导致主从服务器数据库状态不一致,此时需要让主从数据同步到一致的状态,同步的动作成为命令传播
  • master将接受到的数据变更命令发送给slave,slave接受命令后执行命令。
命令传播阶段(部分复制)
  1. 复制偏移量(offset)

主节点和从节点分别维护一个复制偏移量(offset),代表的是主节点向从节点传递的字节数。主节点每次向从节点传播N个字节数据时,主节点的offset增加N,从节点每次收到主节点传来的N个字节数据时,从节点的offset增加N。

分类:master复制偏移量:记录发送给所有slave的指令字节对应的位置(多个),slave复制偏移量:记录slave接受master发送过来的指令字节对应的位置(一个)

作用:offset用于判断主从节点的数据库状态是否一致,对比master与slave的差异,当slave断线后,恢复数据使用

  1. 复制积压缓冲区
    复制积压缓冲区是主节点维护的,固定长度的,先进先出的FIFO队列,默认的大小为1MB;其作用是备份主节点最近发送给从节点的数据。注意,无论主节点有一个还是多个从节点,都只需要一个复制积压缓冲区

  2. 服务器运行(runid)
    每个Redis节点(无论主从),在启动时都会自动生成一个随机ID(每次启动都不一样),由40个随机的十六进制字符组成;

主从节点初次复制时,主节点将自己的runid发送给从节点,从节点将这个runid保存起来;当断线重连时,从节点会将这个runid发送给主节点;主节点根据runid判断能否进行部分复制

数据同步 + 命令传播工作流程

在这里插入图片描述

心跳机制
  • 进入命令传播阶段后,master与slave间需要进行信息交换,使用心跳机制进行维护,实现双方连接保持在线

  • master心跳
    指令:PING
    周期:由repl-ping-slave-period决定,默认10秒
    作用:判断slave是否在线
    查询: INFO replication 获取slave最后一次链接时间间隔,lag项维持在0或1视为正常

  • slave心跳指令
    指令:REPLCONF ACK{offset}
    周期:1秒
    作用1:汇报slave自己的复制偏移量,获取最新的数据变更指令
    作用2:判断master是否在线
    心跳阶段注意事项

  • 当slave多数掉线,或延迟过高时,master为保障数据稳定性,将拒绝所有信息同步操作

min-slave-to-write 2
min-slave-max-lag 8

slave数量少于2个或者多有slave延迟都大于等于10秒时,强制关闭master写功能,停止数据同步

slave数量由slave发送REPLCONF ACK命令做确认
slave延迟由slave发送REPLCONF ACK命令做queen

主从复制的常见问题
频繁的全量复制(master重启)

伴随着系统的运行,master的数据量会越来越大,一旦master重启,runid将发生变化,会导致全部salve的全量复制操作

在这里插入图片描述

频繁的全量复制(网络问题)
  • 问题现象
    网络环境不加,出现网络中断,slave不提供服务
  • 问题原因
    复制缓冲区过小,断网后slave的offset越界,触发全量复制
  • 最终结果
    slave反复进行全量复制
  • 解决方案
    修改复制缓冲区大小
repl-backlog-size
频繁的网络中断(slave长时间不响应)
  • 问题现象
    master的CPU占用过高或slave频繁断开连接
  • 问题原因
    slave每1秒发送REPLCONF ACK命令到master,当slave连接了慢查询时(keys * , hgetall等),会大量占用CPU性能
    master每1秒调用复制定时函数replicationCron(),对比slave发现长时间没有进行响应
  • 最终结果
    master各种资源(输出缓冲区、宽带、连接等)被严重占用
  • 解决方案
    通过设置合理的超时时间,确认是否释放slave
repl-timeout 

该参数定义了超时时间的阈值(默认60秒),超过该值,释放slave

频繁的网络中断(slave长时间不响应)
  • 问题现象
    slave与master连接断开
  • 问题原因
    master发送ping指令频度较低
    master设定超时时间较短
    ping指令在网络中存在丢包
  • 解决方案
    提高ping指令发送的频度
repl-ping-slave-period

超时时间repl-time的时间至少是ping指令频度的5-10倍,否则slave很容易判定超时

哨兵机制

哨兵是一个分布式系统,用于对主从结构的每一个服务器进行监控,当出现故障时通过投票机制选择新的master并将所有slave连接到新的master

哨兵的作用
  • 监控
    不断地检查master和slave是否正常运行
  • 通知
    当被监控服务器出现问题时,向其他的哨兵发送通知
  • 自动故障转移
    断开master与slave连接,选取一个slave作为master,将其他slave连接到新的master,并告知客户端新的服务器地址
哨兵模式的监控配置信息
sentinel monitor mymaster 127.0.0.1 6379 2

mymaster表示给数据库定义来一个名字
127.0.0.1 6379表示ip和端口
2 代表至少需要2个Sentinel进程判断master为失效才为失效,如果不满足这个条件,则自动故障不会执行
哨兵的工作流程

TODO

集群方案

集群结构设计

企业常见场景

缓存预热

在高请求之前,做好一系列措施,保证大量用户点击造成灾难

  1. 请求数量较高
  2. 主从之间数据吞吐量较大,数据同步操作频度较高

缓存预热就是系统启动前,提前将相关的数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。用户直接查询事先被预热的缓存数据!

缓存雪崩

系统平稳运行过程中,忽然数据库连接量激增(在一个较短的时间内,缓存中较多的key集中过期)

缓存雪崩式瞬间过期数量太大,导致对数据库服务器造成压力。如果能有效避免过期时间集中,可以有效解决雪崩现象的出现(约40%)。配合其他策略一起使用,并监控服务器的运行数据,根据运行巨鹿做快速调整

缓存击穿

Redis中某个key过期,该key访问量巨大

缓存击穿就是单个高热数据过期的瞬间,数据访问较大,未命中redis后,发起了大量对同一数据的数据库访问,导致对数据库服务器造成压力。应对策略应该在业务数据分析与预防方面进行,配合运行监控测试与即时调整策略,毕竟单个key的过期监控难度较高,配合雪崩处理策略即可

缓存穿透

redis中大面积出现未命中
出现非正常URL访问

缓存穿透是访问了不存在的数据,跳过了合法数据的redis数据缓存阶段,每次访问数据库,导致对数据库服务器造成压力。通常此类数据的出现量是一个较低的值,当出现此类情况以毒攻毒,并即时报警。应对策略应该在临时预案防范方面多做文章
无论是黑名单还是白名单,都是对整体系统的压力,警报解除后尽快移除

Redis五种数据结构底层实现

动态字符串SDS

simple dynamic string的缩写
redis中所有场景中出现的字符串,基本都是有SDS来实现的

结构

在这里插入图片描述

free: 还剩多少空间
len:字符串长度
buf:存放的字符数组

空间预分配

为减少修改字符串带来的内存重分配次数,sds采用了“一次管够”的策略:

  • 若修改之后sds长度小于1MB,则多分配现有len长度的空间
  • 若修改之后sds长度大于等于1MB,则扩充除了满足修改之后的长度外,额外多1MB空间

惰性空间释放

为避免缩短字符串时候的内存重分配操作,sds在数据减少时,并不立刻释放空间。

双向链表

在这里插入图片描述
head:指向双向链表的头
tail:指向双向链表的尾
len:双向链表的长度

ziplist

压缩列表

redis的列表键和哈希键的底层实现之一。此数据结构是为了节约内存而开发的。就减少了很多内存碎片和指针的内存占用,进而节约了内存

在这里插入图片描述
然后文中的entry的结构是这样的:
在这里插入图片描述

元素的遍历

先找到列表尾部元素

然后再根据ziplist节点元素中的previous_entry_length属性,来逐个遍历

连锁更新

再次看看entry元素的结构,又一个previous_entry_length字段。它的长度要么是1个字节,要么都是5个字节

  • 前一个节点小于254字节,则previous_entry_length长度为1字节
  • 前一个节点大于等于254字节,则previous_entry_length长度为5字节

如果目前程序存在一组压缩列表,长度都在250字节至253字节之间,突然增加一个新节点new,程序需要不断的对压缩列表进行空间重分配工作

哈希表

redis的哈希表的制作为拉链法

在这里插入图片描述

rehash

其中关键的属性为htrehashidx
ht是一个数组,ht[0]存放的是redis中使用的哈希表,而ht[1]和rehashidx和哈希表的rehash有关

rehash指的是重新计算键的哈希值和索引值,然后将键值对重排的过程

加载因子(load factor) = ht[0].used / ht[0].size

扩容和收缩标准

扩容:

  • 没有执行BGSAVE和BGREWRITEAOF指令的情况下,哈希表的加载因子大于等于1。
  • 正在执行BGSAVE和BGREWRITEAOF指令的情况下,哈希表的加载因子大于等于5。

收缩:

  • 加载因子小于0.1时,程序自动开始对哈希表进行收缩操作。

TODO

intset
跳表

跳表在redis中只用在两个地方。一是实现有序集合键,二是集群节点中用作内部数据结构

在这里插入图片描述
跳跃表的level是如何定义的?
跳跃表level层级完全是随机的,一般来说,层级越多,访问节点的速度越快

Linux LVM学习

  1. 将物理硬盘格式化为PV(Physical Volume)

格式化物理卷的过程中LVM是将底层的硬盘划分为一个一个PE,LVM磁盘管理中的PE的默认大小为4M大小

  1. 创建一个VG(Volume Group)

VG的作用就是用来装PE的,可以把一个或者多个PE加到VG中

  1. 基于VG创建最后使用的LV(Logical Volume)

要基于VG来创建LV

文章引用

小米技术团队,分布式锁
跳跃表实现和原理

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WeiXiao_Hyy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值