目录名称

数据库

MySQL基础

请你说说数据库引擎有哪些,各自有什么区别

1.InnoDB引擎支持事务,具有提交,回滚和崩溃恢复功能, 支持行级锁,提高多用户并发性能;支持外键,维护数据完整性。

2.MyISAM引擎不支持事务, 不支持外键, 并发性能差,占用空间是InnoDB的五分之二,支持表级锁,能够限制读写工作的负载的性能,查询效率较高,常用于只读场景。

3.Memory引擎,支持表级锁, 将所有数据存储在RAM中,默认使用Hash索引, 检索效率高, 不适合精确查找, 主要用于内容变化不频繁的表。

什么是3NF(范式)

  • 1NF指的是数据表中的任意属性都具有原子性, 不可再分.
  • 2NF指的是对记录的唯一性约束, 要求记录有唯一标识, 即实体的唯一性.
  • 3NF是对字段冗余性的约束, 即任何字段不能由其他字段派生出来, 他要求字段没有冗余.

请你说说innodb和myisam的区别?

  1. myisam引擎是5.1版本之前的默认引擎,支持全文检索、压缩、空间函数等,但是不支持事务和行级锁,所以一般用于有大量查询少量插入的场景来使用,而且myisam不支持外键,并且索引和数据是分开存储的。
  2. innodb是基于B+Tree索引建立的,和myisam相反它支持事务、外键,并且通过MVCC来支持高并发,索引和数据存储在一起。

两者的对比

  1. count运算上的区别: 因为MyISAM缓存有表meta-data(行数等),因此在做COUNT(*)时对于一个结构很好的查询是不需要消耗多少资源的。而对于InnoDB来说,则没有这种缓存.
  2. 是否支持事务和崩溃后的安全恢复: MyISAM 强调的是性能,每次查询具有原子性,其执行速度比InnoDB类型更快,但是不提供事务支持。但是 InnoDB 提供事务支持,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。
  3. 是否支持外键: MyISAM不支持,而InnoDB支持。

关于两者的总结

MyISAM更适合读密集的表,而InnoDB更适合写密集的的表。 在数据库做主从分离的情况下,经常选择MyISAM作为主库的存储引擎。

一般来说,如果需要事务支持,并且有较高的并发读取频率(MyISAM的表锁的粒度太大,所以当该表写并发量较高时,要等待的查询就会很多了),InnoDB是不错的选择。如果你的数据量很大(MyISAM支持压缩特性可以减少磁盘的空间占用),而且不需要支持事务时,MyISAM是最好的选择。

为什么使用InnnoDB

支持事务、聚簇索引、MVCC

MySQL事务

事务有哪些特性?

  • A: 原子性
  • C: 一致性
  • I: 隔离性
  • D: 持久性

ACID

原子性

同一个事务下,多个操作要么成功要么失败,不存在部分成功或者部分失败的情况.

如果能保证一致性和持久性, 就能保证原子性.

一致性

通过undolog实现一致性.

隔离性

锁lock
  1. 粒度: 行锁和表锁.
  2. 类型: 共享锁, 排他锁, 意向共享锁和意向排他锁.
  3. 机制: 无锁或有锁机制.
  4. 算法:
  • RecordLock算法:单个行记录上的互斥锁, 同时会锁住索引记录.
  • GapLock算法:间隙锁, 锁定一个范围, 但不包含记录本身.
  • Next-KeyLock算法: RecordLock+GapLock, 锁定范围+记录.
  1. 问题
  • 读取: 脏读(需要避免);不可重复读; 幻读.
  • 更新: 第一类丢失(某一个事务的回滚, 导致另一个事务已更新的数据丢失); 第二类丢失(某一个事务的提交, 导致另一个事务已更新的数据丢失).----任何隔离级别都不会产生此类问题, 因为会对DML操作先添加意向排他锁.
隔离级别/请你说说MySQL的事务隔离级别

为了避免上面出现的几种情况,在标准SQL规范中,定义了4个事务隔离级别,不同的隔离级别对事务的处理不同。

  1. 读未提交(Read Uncommitted):只处理更新丢失, 不处理脏读和不可重复读。性能最好, 但是实际不用, 因为无法处理脏读问题.

  2. 读提交(Read Committed):处理更新丢失、脏读, 通过MVCC实现总是读取被锁定行的最新一份快照,通过互斥锁RecordLock解决脏读问题MVCC解决并发问题, 互斥锁解决脏读问题.

  3. 可重复读取(Repeatable Read):默认的隔离级别, 处理更新丢失、脏读和不可重复读取。通过Next-Key-Lock算法解决脏读,不可重复读和幻读问题, 同时通过MVCC实现总是读取事务开始时的行数据版本.

  4. 序列化(Serializable):提供严格的事务隔离, 解决脏读和不可重复读问题, 通过共享锁实现

隔离级别越高,越能保证数据的完整性和统一性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读,而且具有较好的并发性能。尽管它会导致不可重复读、幻读这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制

持久性

redo log

当事务提交时, 务必把事务的所有日志写入到redo log中持久化, 待事务的提交操作完成才算完成.

与bin log的区别

redolog是在引擎中产生的, binlog是在数据库上层产生的.

MySQL 锁的类型有哪些呢?

共享锁(S)

行锁, 允许事务读一条数据.

排他锁(X)

行锁, 允许事务删除或更改一条数据.

意向共享锁(IS)

表锁, 事务想要获取一张表中某几行的共享锁.

意向排他锁(IX)

表锁, 事务想要获取一张表中某几行的排他锁.

排他锁与其他锁都不兼容, 意向排他锁与行锁不兼容.

MySQL 锁的机制?

无锁

MVCC: 对于正在更新的数据, InnoDB会去读取该行快照数据(undo log)

有锁

X锁: select...for update S锁: select...lock in share mode

乐观锁和悲观锁的区别?

悲观锁假定会发生冲突,访问的时候都要先获得锁,保证同一个时刻只有线程获得锁,读读也会阻塞;

乐观锁假设不会发生冲突,只有在提交操作的时候检查是否有冲突.

这两种锁在Java和MySQL分别是怎么实现的?

Java乐观锁通过CAS实现,悲观锁通过synchronize实现。

mysql乐观锁通过MVCC,也就是版本实现,悲观锁可以通过select... for update加上排它锁.

多事务并发执行的问题

(1)更新丢失

两个事务都同时更新一行数据,一个事务对数据的更新把另一个事务对数据的更新覆盖了。这是因为系统没有执行任何的锁操作,因此并发并没有被隔离开来。

(2)脏读

一个事务读取到了另一事务未提交的数据操作结果。这是相当危险的,因为很可能所有的操作都被回滚。

(3)不可重复读

不可重复读(Non-repeatable Reads):一个事务对同一行数据重复读取两次,但是却得到了不同的结果

(4)幻读 事务在操作过程中进行两次查询,第二次查询的结果包含了第一次查询中未出现的数据或者缺少了第一次查询中出现的数据。这是因为在两次查询过程中有另外一个事务插入数据造成的,某一个事务, 对同一张表前后查询到的行数不一致

什么是MVCC? 说说MySQL实现MVCC的原理?

通过MVCC(多版本并发控制)来支持高并发, 通过undolog实现MVCC.

在Mysql的InnoDB引擎中就是指在已提交读(READ COMMITTD)和可重复读(REPEATABLE READ)这两种隔离级别下的事务对于SELECT操作会访问版本链中的记录的过程。

实现

MVCC实现由三部分配合实现:

  • undolog
  • mysql的表中每个表都有三个隐藏字段
  1. row-id:InnoDB引擎提供的隐藏主键--当表中没有主键时自动生成--隐藏主键.
  2. DB_trx_id:事务的id---从1开始自增--该列中保存的id值是最后操作该数据的事务id.
  3. DB_roll_ptr: 数据回滚指针.
  • ReadView

MySQL调优

Explain的使用

Explain是用来获取select语句执行计划的.

其中type字段说明索引有没有生效. 如果是All, 说明是全表扫描, 索引失效或没有索引.

为什么要使用索引/请你说说MySQL索引,以及它们的好处和坏处

  1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
  2. 可以大大加快数据的检索速度(大大减少的检索的数据量), 这也是创建索引的最主要的原因。
  3. 帮助服务器避免排序和临时表
  4. 将随机IO变为顺序IO
  5. 可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。

索引这么多优点,为什么不对表中的每一个列创建一个索引呢?

  1. 当对表中的数据进行增加、删除和修改的时候,每个增删改操作后相应列的索引都必须被更新, 索引也要动态的维护,这样就降低了数据的维护速度.
  2. 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。
  3. 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。

索引分类

  • 普通索引---建索引之前没有修饰. ALTER TABLE table_name ADD INDEX index_name ( column ).
  • 唯一索引---建索引Unique修饰. ALTER TABLE table_name ADD UNIQUE ( column ) , 主键索引就是唯一索引, 但是主键索引不能有null. ALTER TABLE table_name ADD PRIMARY KEY ( column ) .
  • 全文索引---一般用ES来代替, 不用MySQL作为全文检索. ALTER TABLE table_name ADD FULLTEXT ( column)
  • 空间索引---一般存地理信息.

索引是如何提高查询速度的?

将无序的数据变成相对有序的数据(就像查目录一样)

Mysql索引主要使用的两种数据结构/存储方式分类

  • 哈希索引:InnoDB不支持, 对于哈希索引来说,底层的数据结构就是哈希表.
  • BTree索引:Mysql的InnoDB的BTree索引使用的是B树中的B+Tree, 数据存储是有序的。对于主要的两种存储引擎(MyISAM和InnoDB)的实现方式是不同的, 并且通过MVCC来支持高并发,索引和数据存储在一起。

请你说说聚簇索引和非聚簇索引

聚簇索引和非聚簇索引是按照数据分布来进行分类的. 主键索引属于聚簇索引, 非聚簇索引又叫二级索引, 也叫辅助索引, 也是普通索引.

主键索引和普通索引的区别

主键索引的叶子结点存放了整行记录,普通索引的叶子结点存放了主键ID,查询的时候需要做一次回表查询.

主键索引的叶子节点保存的是一行记录。而辅助索引叶子节点的数据区保存的是主键索引关键字的值。

一定要回表查询么?/什么情况下使用非聚簇索引查询不用回表, 什么是回表?

不一定,当查询的字段刚好是索引的字段或者索引的一部分,就可以不用回表,这也是索引覆盖的原理.

什么是覆盖索引?

如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为“覆盖索引”。

聚簇索引包含了所有的数据, 但是它不是覆盖索引. 只有在select, where中出现的字段, 被索引覆盖的情况才是覆盖索引, 此时Extra中会出现Using Index.

要确定一个查询是否是覆盖索引,我们只需要explain sql语句看Extra的结果是否是“Using index”即可。

组合索引

ALTER TABLE table_name ADD INDEX index_name ( column1, column2, column3 );

index(a,b,c);

假设在id、name、age字段上已经成功建立了一个名为MultiIdx的组合索引。索引行中按id、name、age的顺序存放,索引可以搜索id、(id,name)、(id, name, age)字段组合。如果列不构成索引最左面的前缀,那么MySQL不能使用局部索引,如(age)或者(name,age)组合则不能使用该索引查询。

什么情况下索引会失效

组合索引中最左前缀

alt

条件是要查询的字段一定要有序, 所有where后必须是最左前缀, 且like模糊查询, 也必须是首位字符.

说一下使用索引的注意事项?

  1. 避免 where 子句中对字段施加函数,这会造成无法命中索引。
  2. 将打算加索引的列设置为 NOT NULL ,否则将导致引擎放弃使用索引而进行全表扫描.
  3. 删除长期未使用的索引,不用的索引的存在会造成不必要的性能损耗 MySQL 5.7 可以通过查询 sys 库的 schema_unused_indexes 视图来查询哪些索引从未被使用

MySQL高性能优化规范建议

MySQL高性能优化规范建议

SQL调优方法

SQL语句优化方法

数据库为什么不用红黑树而用B+树?

  1. 红黑树本质上还是二叉树,一个结点最多只能拥有两个子结点,而B+树则是多叉的,这会使得相同的数据存储,二叉树的高度会大于B+树, B+树的高度一般为2-4, 一个千万级记录表的索引的高度大概在3-5;
  2. 而数据是存储在磁盘上的,树的高度越高,磁盘IO次数越多,开销越大,B+树可以有效的减少这一开销;
  3. 且B+树的叶子节点是通过链表的方式进行相连的,能在找到起点和终点后快速取出需要的数据.

请你说说索引怎么实现的B+树,为什么选这个数据结构?

B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现,它具有 B Tree 的平衡性,并且通过顺序访问指针来提高区间查询的性能。一个节点中的key从左到右非递减排列, 如果某个指针的左右相邻 key 分别是 key_i 和 key_{i+1},且不为 null,则该指针指向节点的所有 key 大于等于 key_i 且小于等于key_{i+1}。

B+树的叶子节点是通过链表的方式进行相连.

alt

alt

B+树可以对叶子结点顺序查找,因为叶子结点存放了数据结点且有序.

MySQL高级

如何实现mysql的读写分离?

其实很简单,就是基于主从复制架构,简单来说,就搞一个主库,挂多个从库,然后我们就单单只是写主库,然后主库会自动把数据给同步到从库上去。一般情况下,主库可以挂4-5个从库

alt

主服务器处理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。

读写分离能提高性能的原因在于:

  • 主从服务器负责各自的读和写,极大程度缓解了锁的争用;
  • 从服务器可以使用 MyISAM,提升查询性能以及节约系统开销;
  • 增加冗余,提高可用性。 读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。

alt

MySQL主从复制原理的是啥?

主要涉及三个线程: binlog 线程、I/O 线程和 SQL 线程。

  • binlog 线程 : 负责将主服务器上的数据更改写入二进制日志中。
  • I/O 线程 : 负责从主服务器上读取二进制日志,并写入从服务器的中继日志中。
  • SQL 线程 : 负责读取中继日志并重放其中的 SQL 语句。

alt

全同步复制

主库写入binlog后强制同步日志到从库,所有的从库都执行完成后才返回给客户端,但是很显然这个方式的话性能会受到严重影响。

半同步复制

和全同步不同的是,半同步复制的逻辑是这样,从库写入日志成功后返回ACK确认给主库,主库收到至少一个从库的确认就认为写操作完成。

MySQL里有一个概念,叫binlog日志,就是每个增删改类的操作,会改变数据的操作,除了更新数据以外,对这个增删改操作还会写入一个日志文件,记录这个操作的日志。

主库将变更写binlog日志,然后从库连接到主库之后,从库有一个IO线程,将主库的binlog日志拷贝到自己本地,写入一个中继日志中。接着从库中有一个SQL线程会从中继日志读取binlog,然后执行binlog日志中的内容,也就是在自己本地再次执行一遍SQL,这样就可以保证自己跟主库的数据是一样的。

这里有一个非常重要的一点,就是从库同步主库数据的过程是串行化的,也就是说主库上并行的操作,在从库上会串行执行。所以这就是一个非常重要的点了,由于从库从主库拷贝日志以及串行执行SQL的特点,在高并发场景下,从库的数据一定会比主库慢一些,是有延时的。所以经常出现,刚写入主库的数据可能是读不到的,要过几十毫秒,甚至几百毫秒才能读取到。

而且这里还有另外一个问题,就是如果主库突然宕机,然后恰好数据还没同步到从库,那么有些数据可能在从库上是没有的,有些数据可能就丢失了。

所以mysql实际上在这一块有两个机制,一个是半同步复制,用来解决主库数据丢失问题;一个是并行复制,用来解决主从同步延时问题。

这个所谓半同步复制,semi-sync复制,指的就是主库写入binlog日志之后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的relay log之后,接着会返回一个ack给主库,主库接收到至少一个从库的ack之后才会认为写操作完成了。

所谓并行复制,指的是从库开启多个线程,并行读取relay log中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。

  • 主从复制的原理
  • 主从延迟问题产生的原因
  • 主从复制的数据丢失问题,以及半同步复制的原理
  • 并行复制的原理,多库并发重放relay日志,缓解主从延迟问题

alt

MySQL主从同步延时问题(重点)

线上确实处理过因为主从同步延时问题,导致的线上的bug,小型的生产事故

show status,Seconds_Behind_Master,你可以看到从库复制主库的数据落后了几ms

其实这块东西我们经常会碰到,就比如说用了mysql主从架构之后,可能会发现,刚写入库的数据结果没查到,结果就完蛋了。。。。

所以实际上你要考虑好应该在什么场景下来用这个mysql主从同步,建议是一般在读远远多于写,而且读的时候一般对数据时效性要求没那么高的时候,用mysql主从同步

所以这个时候,我们可以考虑的一个事情就是,你可以用mysql的并行复制,但是问题是那是库级别的并行,所以有时候作用不是很大

所以这个时候。。通常来说,我们会对于那种写了之后立马就要保证可以查到的场景,采用强制读主库的方式,这样就可以保证你肯定的可以读到数据了吧。其实用一些数据库中间件是没问题的。

一般来说,如果主从延迟较为严重

  • 分库,将一个主库拆分为4个主库,每个主库的写并发就500/s,此时主从延迟可以忽略不计
  • 打开mysql支持的并行复制,多个库并行复制,如果说某个库的写入并发就是特别高,单库写并发达到了2000/s,并行复制还是没意义。28法则,很多时候比如说,就是少数的几个订单表,写入了2000/s,其他几十个表10/s。
  • 重写代码,写代码的同学,要慎重,当时我们其实短期是让那个同学重写了一下代码,插入数据之后,直接就更新,不要查询
  • 如果确实是存在必须先插入,立马要求就查询到,然后立马就要反过来执行一些操作,对这个查询设置直连主库。不推荐这种方法,你这么搞导致读写分离的意义就丧失了

alt

什么时候分库分表

说白了,分库分表是两回事儿,大家可别搞混了,可能是光分库不分表,也可能是光分表不分库,都有可能。我先给大家抛出来一个场景。

假如我们现在是一个小创业公司(或者是一个BAT公司刚兴起的一个新部门),现在注册用户就20万,每天活跃用户就1万,每天单表数据量就1000,然后高峰期每秒钟并发请求最多就10。。。天,就这种系统,随便找一个有几年工作经验的,然后带几个刚培训出来的,随便干干都可以。

结果没想到我们运气居然这么好,碰上个CEO带着我们走上了康庄大道,业务发展迅猛,过了几个月,注册用户数达到了2000万!每天活跃用户数100万!每天单表数据量10万条!高峰期每秒最大请求达到1000!同时公司还顺带着融资了两轮,紧张了几个亿人民币啊!公司估值达到了惊人的几亿美金!这是小独角兽的节奏!

好吧,没事,现在大家感觉压力已经有点大了,为啥呢?因为每天多10万条数据,一个月就多300万条数据,现在咱们单表已经几百万数据了,马上就破千万了。但是勉强还能撑着。高峰期请求现在是1000,咱们线上部署了几台机器,负载均衡搞了一下,数据库撑1000 QPS也还凑合。但是大家现在开始感觉有点担心了,接下来咋整呢。。。。。。

再接下来几个月,我的天,CEO太牛逼了,公司用户数已经达到1亿,公司继续融资几十亿人民币啊!公司估值达到了惊人的几十亿美金,成为了国内今年最牛逼的明星创业公司!天,我们太幸运了。

但是我们同时也是不幸的,因为此时每天活跃用户数上千万,每天单表新增数据多达50万,目前一个表总数据量都已经达到了两三千万了!扛不住啊!数据库磁盘容量不断消耗掉!高峰期并发达到惊人的5000~8000!别开玩笑了,哥。我跟你保证,你的系统支撑不到现在,已经挂掉了!

好吧,所以看到你这里你差不多就理解分库分表是怎么回事儿了,实际上这是跟着你的公司业务发展走的,你公司业务发展越好,用户就越多,数据量越大,请求量越大,那你单个数据库一定扛不住。

比如你单表都几千万数据了,你确定你能抗住么?绝对不行,单表数据量太大,会极大影响你的sql执行的性能,到了后面你的sql可能就跑的很慢了。一般来说,就以我的经验来看,单表到几百万的时候,性能就会相对差一些了,你就得分表了。

分表是啥意思?就是把一个表的数据放到多个表中,然后查询的时候你就查一个表。比如按照用户id来分表,将一个用户的数据就放在一个表中。然后操作的时候你对一个用户就操作那个表就好了。这样可以控制每个表的数据量在可控的范围内,比如每个表就固定在200万以内。

分库是啥意思?就是你一个库一般我们经验而言,最多支撑到并发2000,一定要扩容了,而且一个健康的单库并发值你最好保持在每秒1000左右,不要太大。那么你可以将一个库的数据拆分到多个库中,访问的时候就访问一个库好了。

这就是所谓的分库分表,为啥要分库分表?你明白了吧

alt

分库分表的中间件?

分类

数据库中间件分为两类

  • proxy:中间经过一层代理,需要独立部署
  • client:在客户端就知道指定到那个数据库

各个中间件

这个其实就是看看你了解哪些分库分表的中间件,各个中间件的优缺点是啥?然后你用过哪些分库分表的中间件。

比较常见的包括:cobar、TDDL、atlas、sharding-jdbc、mycat

cobar:阿里b2b团队开发和开源的,属于proxy层方案。早些年还可以用,但是最近几年都没更新了,基本没啥人用,差不多算是被抛弃的状态吧。而且不支持读写分离、存储过程、跨库join和分页等操作。

TDDL:淘宝团队开发的,属于client层方案。不支持join、多表查询等语法,就是基本的crud语法是ok,但是支持读写分离。目前使用的也不多,因为还依赖淘宝的diamond配置管理系统。

atlas:360开源的,属于proxy层方案,以前是有一些公司在用的,但是确实有一个很大的问题就是社区最新的维护都在5年前了。所以,现在用的公司基本也很少了。

sharding-jdbc:当当开源的,属于client层方案。确实之前用的还比较多一些,因为SQL语法支持也比较多,没有太多限制,而且目前推出到了2.0版本,支持分库分表、读写分离、分布式id生成、柔性事务(最大努力送达型事务、TCC事务)。而且确实之前使用的公司会比较多一些(这个在官网有登记使用的公司,可以看到从2017年一直到现在,是不少公司在用的),目前社区也还一直在开发和维护,还算是比较活跃,个人认为算是一个现在也可以选择的方案。

mycat:基于cobar改造的,属于proxy层方案,支持的功能非常完善,而且目前应该是非常火的而且不断流行的数据库中间件,社区很活跃,也有一些公司开始在用了。但是确实相比于sharding jdbc来说,年轻一些,经历的锤炼少一些。

总结

所以综上所述,现在其实建议考量的,就是sharding-jdbc和mycat,这两个都可以去考虑使用。

sharding-jdbc这种client层方案的优点在于不用部署,运维成本低,不需要代理层的二次转发请求,性能很高,但是如果遇到升级啥的需要各个系统都重新升级版本再发布,各个系统都需要耦合sharding-jdbc的依赖;

mycat这种proxy层方案的缺点在于需要部署,自己及运维一套中间件,运维成本高,但是好处在于对于各个项目是透明的,如果遇到升级之类的都是自己中间件那里搞就行了。

通常来说,这两个方案其实都可以选用,但是我个人建议中小型公司选用sharding-jdbc,client层方案轻便,而且维护成本低,不需要额外增派人手,而且中小型公司系统复杂度会低一些,项目也没那么多;

但是中大型公司最好还是选用mycat这类proxy层方案,因为可能大公司系统和项目非常多,团队很大,人员充足,那么最好是专门弄个人来研究和维护mycat,然后大量项目直接透明使用即可。

我们,数据库中间件都是自研的,也用过proxy层,后来也用过client层

订单表有做拆分么,怎么拆的?

拆分方法

  • 垂直拆分
  • 水平拆分

水平拆分

水平拆分的意思,就是把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来抗更高的并发,还有就是用多个库的存储容量来进行扩容。

垂直拆分

垂直拆分的意思,就是把一个有很多字段的表给拆分成多个表,或者是多个库上去。每个库表的结构都不一样,每个库表都包含部分字段。一般来说,会将较少的访问频率很高的字段放到一个表里去,然后将较多的访问频率很低的字段放到另外一个表里去。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。

这个其实挺常见的,不一定我说,大家很多同学可能自己都做过,把一个大表拆开,订单表、订单支付表、订单商品表。

还有表层面的拆分,就是分表,将一个表变成N个表,就是让每个表的数据量控制在一定范围内,保证SQL的性能。否则单表数据量越大,SQL性能就越差。一般是200万行左右,不要太多,但是也得看具体你怎么操作,也可能是500万,或者是100万。你的SQL越复杂,就最好让单表行数越少。

好了,无论是分库了还是分表了,上面说的那些数据库中间件都是可以支持的。就是基本上那些中间件可以做到你分库分表之后,中间件可以根据你指定的某个字段值,比如说userid,自动路由到对应的库上去,然后再自动路由到对应的表里去。

你就得考虑一下,你的项目里该如何分库分表?一般来说,垂直拆分,你可以在表层面来做,对一些字段特别多的表做一下拆分;水平拆分,你可以说是并发承载不了,或者是数据量太大,容量承载不了,你给拆了,按什么字段来拆,你自己想好;分表,你考虑一下,你如果哪怕是拆到每个库里去,并发和容量都ok了,但是每个库的表还是太大了,那么你就分表,将这个表分开,保证每个表的数据量并不是很大。

而且这儿还有两种分库分表的方式,一种是按照range来分,就是每个库一段连续的数据,这个一般是按比如时间范围来的,但是这种一般较少用,因为很容易产生热点问题,大量的流量都打在最新的数据上了;或者是按照某个字段hash一下均匀分散,这个较为常用。

range来分,好处在于说,后面扩容的时候,就很容易,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了;缺点,但是大部分的请求,都是访问最新的数据。实际生产用range,要看场景,你的用户不是仅仅访问最新的数据,而是均匀的访问现在的数据以及历史的数据

hash分法,好处在于说,可以平均分配没给库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的这么一个过程!

alt

如何让系统不停机迁移到分库分表

现在有一个未分库分表的系统,未来要分库分表,如何设计才可以让系统从未分库分表动态切换到分库分表上?

剖析

你看看,你现在已经明白为啥要分库分表了,你也知道常用的分库分表中间件了,你也设计好你们如何分库分表的方案了(水平拆分、垂直拆分、分表),那问题来了,你接下来该怎么把你那个单库单表的系统给迁移到分库分表上去?所以这都是一环扣一环的,就是看你有没有全流程经历过这个过程

假设,你现有有一个单库单表的系统,在线上在跑,假设单表有600万数据,3个库,每个库里分了4个表,每个表要放50万的数据量

假设你已经选择了一个分库分表的数据库中间件,sharding-jdbc,mycat,都可以,你怎么把线上系统平滑地迁移到分库分表上面去

停机迁移方案

我先给你说一个最low的方案,就是很简单,大家伙儿凌晨12点开始运维,网站或者app挂个公告,说0点到早上6点进行运维,无法访问。。。。。。

接着到0点,停机,系统挺掉,没有流量写入了,此时老的单库单表数据库静止了。然后你之前得写好一个导数的一次性工具,此时直接跑起来,然后将单库单表的数据哗哗哗读出来,写到分库分表里面去。

导数完了之后,就ok了,修改系统的数据库连接配置啥的,包括可能代码和SQL也许有修改,那你就用最新的代码,然后直接启动连到新的分库分表上去。

验证一下,ok了,完美,大家伸个懒腰,看看看凌晨4点钟的北京夜景,打个滴滴回家吧

但是这个方案比较low,谁都能干,我们来看看高大上一点的方案

alt

双写迁移方案

这个是我们常用的一种迁移方案,比较靠谱一些,不用停机,不用看北京凌晨4点的风景

简单来说,就是在线上系统里面,之前所有写库的地方,增删改操作,都除了对老库增删改,都加上对新库的增删改,这就是所谓双写,同时写俩库,老库和新库。

然后系统部署之后,新库数据差太远,用之前说的导数工具,跑起来读老库数据写新库,写的时候要根据gmt_modified这类字段判断这条数据最后修改的时间,除非是读出来的数据在新库里没有,或者是比新库的数据新才会写。

接着导万一轮之后,有可能数据还是存在不一致,那么就程序自动做一轮校验,比对新老库每个表的每条数据,接着如果有不一样的,就针对那些不一样的,从老库读数据再次写。反复循环,直到两个库每个表的数据都完全一致为止。

接着当数据完全一致了,就ok了,基于仅仅使用分库分表的最新代码,重新部署一次,不就仅仅基于分库分表在操作了么,还没有几个小时的停机时间,很稳。所以现在基本玩儿数据迁移之类的,都是这么干了。

alt

如何设计可以动态扩容的分库分表方案?

思考步骤

  • 选择一个数据库中间件,调研、学习、测试
  • 设计你的分库分表的一个方案,你要分成多少个库,每个库分成多少个表,3个库每个库4个表
  • 基于选择好的数据库中间件,以及在测试环境建立好的分库分表的环境,然后测试一下能否正常进行分库分表的读写
  • 完成单库单表到分库分表的迁移,双写方案
  • 线上系统开始基于分库分表对外提供服务
  • 扩容了,扩容成6个库,每个库需要12个表,你怎么来增加更多库和表呢?

这个是你必须面对的一个事儿,就是你已经弄好分库分表方案了,然后一堆库和表都建好了,基于分库分表中间件的代码开发啥的都好了,测试都ok了,数据能均匀分布到各个库和各个表里去,而且接着你还通过双写的方案咔嚓一下上了系统,已经直接基于分库分表方案在搞了。

那么现在问题来了,你现在这些库和表又支撑不住了,要继续扩容咋办?这个可能就是说你的每个库的容量又快满了,或者是你的表数据量又太大了,也可能是你每个库的写并发太高了,你得继续扩容。

停机扩容

这个方案就跟停机迁移一样,步骤几乎一致,唯一的一点就是那个导数的工具,是把现有库表的数据抽出来慢慢倒入到新的库和表里去。但是最好别这么玩儿,有点不太靠谱,因为既然分库分表就说明数据量实在是太大了,可能多达几亿条,甚至几十亿,你这么玩儿,可能会出问题。

从单库单表迁移到分库分表的时候,数据量并不是很大,单表最大也就两三千万

写个工具,多弄几台机器并行跑,1小时数据就导完了

3个库+12个表,跑了一段时间了,数据量都1亿~2亿了。光是导2亿数据,都要导个几个小时,6点,刚刚导完数据,还要搞后续的修改配置,重启系统,测试验证,10点才可以搞完

优化后的方案

一开始上来就是32个库,每个库32个表,1024张表

我可以告诉各位同学说,这个分法,第一,基本上国内的互联网肯定都是够用了,第二,无论是并发支撑还是数据量支撑都没问题

每个库正常承载的写入并发量是1000,那么32个库就可以承载32 * 1000 = 32000的写并发,如果每个库承载1500的写并发,32 * 1500 = 48000的写并发,接近5万/s的写入并发,前面再加一个MQ,削峰,每秒写入MQ 8万条数据,每秒消费5万条数据。

有些除非是国内排名非常靠前的这些公司,他们的最核心的系统的数据库,可能会出现几百台数据库的这么一个规模,128个库,256个库,512个库,1024张表,假设每个表放500万数据,在MySQL里可以放50亿条数据

每秒的5万写并发,总共50亿条数据,对于国内大部分的互联网公司来说,其实一般来说都够了

谈分库分表的扩容,第一次分库分表,就一次性给他分个够,32个库,1024张表,可能对大部分的中小型互联网公司来说,已经可以支撑好几年了

一个实践是利用32 * 32来分库分表,即分为32个库,每个库里一个表分为32张表。一共就是1024张表。根据某个id先根据32取模路由到库,再根据32取模路由到库里的表。

刚开始的时候,这个库可能就是逻辑库,建在一个数据库上的,就是一个mysql服务器可能建了n个库,比如16个库。后面如果要拆分,就是不断在库和mysql服务器之间做迁移就可以了。然后系统配合改一下配置即可。

比如说最多可以扩展到32个数据库服务器,每个数据库服务器是一个库。如果还是不够?最多可以扩展到1024个数据库服务器,每个数据库服务器上面一个库一个表。因为最多是1024个表么。

这么搞,是不用自己写代码做数据迁移的,都交给dba来搞好了,但是dba确实是需要做一些库表迁移的工作,但是总比你自己写代码,抽数据导数据来的效率高得多了。

alt

哪怕是要减少库的数量,也很简单,其实说白了就是按倍数缩容就可以了,然后修改一下路由规则。

对2 ^ n取模

orderId 模 32 = 库

orderId / 32 模 32 = 表

259      3        8

1189     5        5

352      0        11

4593     17       15

总结

  • 设定好几台数据库服务器,每台服务器上几个库,每个库多少个表,推荐是32库 * 32表,对于大部分公司来说,可能几年都够了
  • 路由的规则,orderId 模 32 = 库,orderId / 32 模 32 = 表
  • 扩容的时候,申请增加更多的数据库服务器,装好mysql,倍数扩容,4台服务器,扩到8台服务器,16台服务
  • 由dba负责将原先数据库服务器的库,迁移到新的数据库服务器上去,很多工具,库迁移,比较便捷
  • 我们这边就是修改一下配置,调整迁移的库所在数据库服务器的地址
  • 重新发布系统,上线,原先的路由规则变都不用变,直接可以基于2倍的数据库服务器的资源,继续进行线上系统的提供服务

如果落到某个分片的数据很大怎么办?

(按照某种规则,比如哈希取模、range,将单张表拆分为多张表)

哈希取模会有什么问题么?

(有的,数据分布不均,扩容缩容相对复杂 )

分库分表后怎么解决读写压力?

(一主多从、多主多从)

拆分后主键怎么保证惟一?/分库分表后ID主键如何处理

前言

其实这是分库分表之后你必然要面对的一个问题,就是id咋生成?因为要是分成多个表之后,每个表都是从1开始累加,那肯定不对啊,需要一个全局唯一的id来支持。所以这都是你实际生产环境中必须考虑的问题。

  1. UUID
  2. Snowflake算法

数据库自增ID

这个就是说你的系统里每次得到一个id,都是往一个库的一个表里插入一条没什么业务含义的数据,然后获取一个数据库自增的一个id。拿到这个id之后再往对应的分库分表里去写入。

这个方案的好处就是方便简单,谁都会用;缺点就是单库生成自增id,要是高并发的话,就会有瓶颈的;如果你硬是要改进一下,那么就专门开一个服务出来,这个服务每次就拿到当前id最大值,然后自己递增几个id,一次性返回一批id,然后再把当前最大id值修改成递增几个id之后的一个值;但是无论怎么说都是基于单个数据库。

适合的场景:你分库分表就俩原因,要不就是单库并发太高,要不就是单库数据量太大;除非是你并发不高,但是数据量太大导致的分库分表扩容,你可以用这个方案,因为可能每秒最高并发最多就几百,那么就走单独的一个库和表生成自增主键即可。

并发很低,几百/s,但是数据量大,几十亿的数据,所以需要靠分库分表来存放海量的数据!

alt

UUID

好处就是本地生成,不要基于数据库来了;不好之处就是,uuid太长了,作为主键性能太差了,不适合用于主键。

适合的场景:如果你是要随机生成个什么文件名了,编号之类的,你可以用uuid,但是作为主键是不能用uuid的。

获取系统时间戳

这个就是获取当前时间即可,但是问题是,并发很高的时候,比如一秒并发几千,会有重复的情况,这个是肯定不合适的。基本就不用考虑了。

适合的场景:一般如果用这个方案,是将当前时间跟很多其他的业务字段拼接起来,作为一个id,如果业务上你觉得可以接受,那么也是可以的。你可以将别的业务字段值跟当前时间拼接起来,组成一个全局唯一的编号,订单编号,时间戳 + 用户id + 业务含义编码

Snowflake算法(雪花算法)

twitter开源的分布式id生成算法,就是把一个64位的long型的id,1个bit是不用的,用其中的41 bit作为毫秒数,用10 bit作为工作机器id,12 bit作为序列号

1 bit:不用,为啥呢?因为二进制里第一个bit为如果是1,那么都是负数,但是我们生成的id都是正数,所以第一个bit统一都是0

41 bit:表示的是时间戳,单位是毫秒。41 bit可以表示的数字多达2^41 - 1,也就是可以标识2 ^ 41 - 1个毫秒值,换算成年就是表示69年的时间。

10 bit:记录工作机器id,代表的是这个服务最多可以部署在2^10台机器上哪,也就是1024台机器。但是10 bit里5个bit代表机房id,5个bit代表机器id。意思就是最多代表2 ^ 5个机房(32个机房),每个机房里可以代表2 ^ 5个机器(32台机器)。

12 bit:这个是用来记录同一个毫秒内产生的不同id,12 bit可以代表的最大正整数是2 ^ 12 - 1 = 4096,也就是说可以用这个12bit代表的数字来区分同一个毫秒内的4096个不同的id

64位的long型的id,64位的long -> 二进制

0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 | 1 1001 | 0000 00000000

2018-01-01 10:00:00 -> 做了一些计算,再换算成一个二进制,41bit来放 -> 0001100 10100010 10111110 10001001 ``

机房id,17 -> 换算成一个二进制 -> 10001

机器id,25 -> 换算成一个二进制 -> 11001

snowflake算法服务,会判断一下,当前这个请求是否是,机房17的机器25,在2175/11/7 12:12:14时间点发送过来的第一个请求,如果是第一个请求

假设,在2175/11/7 12:12:14时间里,机房17的机器25,发送了第二条消息,snowflake算法服务,会发现说机房17的机器25,在2175/11/7 12:12:14时间里,在这一毫秒,之前已经生成过一个id了,此时如果你同一个机房,同一个机器,在同一个毫秒内,再次要求生成一个id,此时我只能把加1

比如我们来观察上面的那个,就是一个典型的二进制的64位的id,换算成10进制就是910499571847892992。

alt

算法

public class IdWorker{

    private long workerId;
    private long datacenterId;
    private long sequence;

    public IdWorker(long workerId, long datacenterId, long sequence){
        // sanity check for workerId
// 这儿不就检查了一下,要求就是你传递进来的机房id和机器id不能超过32,不能小于0
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));
        }
        System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
                timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);

        this.workerId = workerId;
        this.datacenterId = datacenterId;
        this.sequence = sequence;
    }

    private long twepoch = 1288834974657L;

    private long workerIdBits = 5L;
    private long datacenterIdBits = 5L;
    private long maxWorkerId = -1L ^ (-1L << workerIdBits); // 这个是二进制运算,就是5 bit最多只能有31个数字,也就是说机器id最多只能是32以内
    private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); // 这个是一个意思,就是5 bit最多只能有31个数字,机房id最多只能是32以内
    private long sequenceBits = 12L;

    private long workerIdShift = sequenceBits;
    private long datacenterIdShift = sequenceBits + workerIdBits;
    private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
    private long sequenceMask = -1L ^ (-1L << sequenceBits);

    private long lastTimestamp = -1L;

    public long getWorkerId(){
        return workerId;
    }

    public long getDatacenterId(){
        return datacenterId;
    }

    public long getTimestamp(){
        return System.currentTimeMillis();
    }

public synchronized long nextId() {
// 这儿就是获取当前时间戳,单位是毫秒
        long timestamp = timeGen();

        if (timestamp < lastTimestamp) {
            System.err.printf("clock is moving backwards.  Rejecting requests until %d.", lastTimestamp);
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds",
                    lastTimestamp - timestamp));
        }

// 0
// 在同一个毫秒内,又发送了一个请求生成一个id,0 -> 1

        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask; // 这个意思是说一个毫秒内最多只能有4096个数字,无论你传递多少进来,这个位运算保证始终就是在4096这个范围内,避免你自己传递个sequence超过了4096这个范围
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0;
        }

// 这儿记录一下最近一次生成id的时间戳,单位是毫秒
        lastTimestamp = timestamp;

// 这儿就是将时间戳左移,放到41 bit那儿;将机房id左移放到5 bit那儿;将机器id左移放到5 bit那儿;将序号放最后10 bit;最后拼接起来成一个64 bit的二进制数字,转换成10进制就是个long型
        return ((timestamp - twepoch) << timestampLeftShift) |
                (datacenterId << datacenterIdShift) |
                (workerId << workerIdShift) |
                sequence;
    }

0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 | 1 1001 | 0000 00000000


    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    private long timeGen(){
        return System.currentTimeMillis();
    }

    //---------------测试---------------
    public static void main(String[] args) {
        IdWorker worker = new IdWorker(1,1,1);
        for (int i = 0; i < 30; i++) {
            System.out.println(worker.nextId());
        }
    }

}

怎么说呢,大概这个意思吧,就是说41 bit,就是当前毫秒单位的一个时间戳,就这意思;然后5 bit是你传递进来的一个机房id(但是最大只能是32以内),5 bit是你传递进来的机器id(但是最大只能是32以内),剩下的那个10 bit序列号,就是如果跟你上次生成id的时间还在一个毫秒内,那么会把顺序给你累加,最多在4096个序号以内。

所以你自己利用这个工具类,自己搞一个服务,然后对每个机房的每个机器都初始化这么一个东西,刚开始这个机房的这个机器的序号就是0。然后每次接收到一个请求,说这个机房的这个机器要生成一个id,你就找到对应的Worker,生成。

他这个算法生成的时候,会把当前毫秒放到41 bit中,然后5 bit是机房id,5 bit是机器id,接着就是判断上一次生成id的时间如果跟这次不一样,序号就自动从0开始;要是上次的时间跟现在还是在一个毫秒内,他就把seq累加1,就是自动生成一个毫秒的不同的序号。

这个算法那,可以确保说每个机房每个机器每一毫秒,最多生成4096个不重复的id。

利用这个snowflake算法,你可以开发自己公司的服务,甚至对于机房id和机器id,反正给你预留了5 bit + 5 bit,你换成别的有业务含义的东西也可以的。

这个snowflake算法相对来说还是比较靠谱的,所以你要真是搞分布式id生成,如果是高并发啥的,那么用这个应该性能比较好,一般每秒几万并发的场景,也足够你用了。

Snowflake生成的ID是全局递增唯一么?

(不是,只是全局唯一,单机递增)

怎么实现全局递增的唯一ID?

(讲了TDDL的一次取一批ID,然后再本地慢慢分配的做法)

Redis基础

什么是Redis?它主要用来什么的?

  • Redis,英文全称是Remote Dictionary Server(远程字典服务),是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

  • 与MySQL数据库不同的是,Redis的数据是存在内存中的。它的读写速度非常快,每秒可以处理超过10万次读写操作。因此redis被广泛应用于缓存,另外,Redis也经常用来做分布式锁。除此之外,Redis支持事务、持久化、LUA 脚本、LRU 驱动事件、多种集群方案。

单线程的redis为什么这么快?

主要有以下几点: 1、基于内存的操作 2、使用了 I/O 多路复用模型,select、epoll 等,基于 reactor 模式开发了自己的网络事件处理器。 3、单线程可以避免不必要的上下文切换和竞争条件,减少了这方面的性能消耗。 4、以上这三点是 redis 性能高的主要原因,其他的还有一些小优化,例如:对数据结构进行了优化,简单动态字符串、压缩列表等

Redis是单线程还是多线程?

Redis的线程模型,要看具体的版本:

Redis6.0 前的请求解析、键值数据读写、结果返回都是由⼀个线程完成,所以称 Redis 为单线程模型。Redis 单线程容易阻塞,为了避免阻塞,Redis 设计了⼦进程和异步线程的⽅式来完成某些耗时操作,例如使用⼦进程实现 RDB ⽣成,AOF 重写等;

Redis 6.0 开始,使用多个线程来完成请求解析和结果返回,提升对⽹络请求的处理,提升系统整体的吞吐量,不过读写命令的执行还是单线程的。

Redis为什么是单线程的?

官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!),Redis利用队列技术将并发访问变为串行访问。

1)绝大部分请求是纯粹的内存操作(非常快速)。 2)采用单线程,避免了不必要的上下文切换和竞争条件。 3)非阻塞IO操作。

Memcache与Redis的区别都有哪些?

1)数据结构:memcached 支持简单的 key-value 数据结构,而 redis 支持丰富的数据结构:String、List、Set、Hash、SortedSet 等。

2)数据存储:memcached 和 redis 的数据都是全部在内存中。

网上有一种说法 “当物理内存用完时,Redis可以将一些很久没用到的 value 交换到磁盘,同时在内存中清除”,这边指的是 redis 里的虚拟内存(Virtual Memory)功能,该功能在 Redis 2.0 被引入,但是在 Redis 2.4 中被默认关闭,并标记为废弃,而在后续版中被完全移除。

3)持久化:memcached 不支持持久化,redis 支持将数据持久化到磁盘

4)灾难恢复:实例挂掉后,memcached 数据不可恢复,redis 可通过 RDB、AOF 恢复,但是还是会有数据丢失问题

5)事件库:memcached 使用 Libevent 事件库,redis 自己封装了简易事件库 AeEvent

6)过期键删除策略:memcached 使用惰性删除,redis 使用惰性删除+定期删除

7)内存驱逐(淘汰)策略:memcached 主要为 LRU 算法,redis 当前支持8种淘汰策略。

8)性能比较

按“CPU 单核” 维度比较:由于 Redis 只使用单核,而 Memcached 可以使用多核,所以在比较上:在处理小数据时,平均每一个核上 Redis 比 Memcached 性能更高,而在 100k 左右的大数据时, Memcached 性能要高于 Redis。

按“实例”维度进行比较:由于 Memcached 多线程的特性,在 Redis 6.0 之前,通常情况下 Memcached 性能是要高于 Redis 的,同时实例的 CPU 核数越多,Memcached 的性能优势越大。

至于网上说的 redis 的性能比 memcached 快很多,这个说法就离谱。

说说Redis内部存储结构?

dict 本质上是为了解决算法中的查找问题(Searching),是一个用于维护key和value映射关系的数据结构,与很多语言中的Map或dictionary类似。本质上是为了解决算法中的查找问题(Searching)

sds:就等同于char * ,它可以存储任意二进制数据,不能像C语言字符串那样以字符’\0’来标识字符串的结 束,因此它必然有个长度字段。

skiplist (跳跃表): 跳表是一种实现起来很简单,单层多指针的链表,它查找效率很高,堪比优化过的二叉平衡树,且比平衡树的实现,quicklist.

ziplist 压缩表:ziplist是一个编码后的列表,是由一系列特殊编码的连续内存块组成的顺序型数据结构。

Redis的基本数据结构类型有哪些?

五种基本类型:

  • String(字符串)
  • Hash(哈希)
  • List(列表)
  • Set(集合)
  • Zset(有序集合)

Redis的基本数据结构类型有哪些?

Redis6三种新的数据结构类型:

  • Geospatial (地理信息)
  • Hyperloglog (基数统计)
  • Bitmap (二进制位图)

请你说说Redis数据类型中的zset,它和set有什么区别?底层是怎么实现的?

介绍一下五种基本数据类型,和他们的使用场景。

  • String是Redis最基础的数据结构类型,它是二进制安全的,可以存储图片或者序列化的对象,值最大存储为512M
  • 数据缓存、共享session、分布式锁,计数器、限流

说说Redis中的数据类型?

常见的有五种基本数据类型和三种特殊数据类型,基本数据结构:String、 list、set、zset和hash,三种特殊数据类型:位图(bitmaps) 、计数器(hyperloglogs)和地理空间(geospatial indexes)。

string:以字符串形式存储数据,经常用于记录用户的访问次数、文章访问量等。

hash:以对象形式存储数据,比较方便的就是操作其中的某个字段,例如存储登陆用户登陆状态,实现购物车。

list:以列表形式存储数据,可记录添加顺序,允许元素重复,通常应用于发布与订阅或者说消息队列、慢查询。

set:以集合形式存储数据,不记录添加顺序,元素不能重复,也不能保证存储顺序,通常可以做全局去重、投票系统。

zset:排序集合,可对数据基于某个权重进行排序。可做排行榜,取TOP N操作。直播系统中的在线用户列表,礼物排行榜,弹幕消息等。

Redis数据过期后的删除策略?

常用的过期数据的删除策略就两个(重要!自己造缓存轮子的时候需要格外考虑的东西)。

定期删除 : 每隔一段时间随机抽取一批 key ,检查是否过期,过期了则删除。这里Redis 底层会通过限制删除操作执行时长和频率来减少删除操作对CPU时间的影响。

惰性删除 :只会在取出key的时候才对数据进行过期检查。这样对CPU最友好,但是可能会造成太多过期 key 没有被删除。

两者各有千秋,定期删除对内存更加友好,惰性删除对CPU更加友好。所以Redis 采用的是 定期删除+惰性/懒汉式删除 。

Redis的数据淘汰策略有哪些?

Redis 中默认提供 6 种数据淘汰策略,分别为:

volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰

volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰

volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰

allkeys-lru:当内存不足以容纳新写入数据时,移除最近最少使用的 key(这个是最常用的)

allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰。

no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!

Sorted Set底层数据存储结构是什么?

Sorted Set当前有两种数据存储结构,分别为ziplist和skiplist。

1)ziplist:使用压缩列表实现,当保存的元素长度都小于64字节,同时数量小于128时,使用该方式,否则会使用 skiplist。这两个参数可以通过 zset-max-ziplist-entries、zset-max-ziplist-value 来自定义修改。

2)skiplist:一个zset同时包含一个字典(dict)和一个跳跃表(skiplist)

为什么同时使用字典和跳跃表?

主要是为了提升性能。

单独使用字典:在执行范围型操作(比如 zrank、zrange),字典需要进行排序,至少需要 O(NlogN) 的时间复杂度及额外 O(N) 的内存空间。

单独使用跳跃表:根据成员查找分值操作的复杂度从 O(1) 上升为 O(logN)。

为什么使用跳跃表,而不是红黑树?

主要有以下几个原因:

1)跳表的性能和红黑树差不多。

2)跳表更容易实现和调试。

说说Redis对应的Java客户端有哪些?

Redis 支持的 Java 客户端都有jedis、lettuce、Redisson等。

说说Redis中的事务操作?

Redis的一个事务从开始到结束通常会经历以下3个阶段:

1)事务开始:multi 命令将执行该命令的客户端从非事务状态切换至事务状态,底层通过 flags 属性标识。

2)命令入队:当客户端处于事务状态时,服务器会根据客户端发来的命令执行不同的操作,例如exec、discard、watch、multi 命令会被立即执行,其他命令不会立即执行,而是将命令放入到一个事务队列,然后向客户端返回 QUEUED 回复。

3)事务执行:当一个处于事务状态的客户端向服务器发送 exec 命令时,服务器会遍历事务队列,执行队列中的所有命令,最后将结果全部返回给客户端。

说明: redis 的事务并不推荐在实际中使用,如果要使用事务,推荐使用 Lua 脚本,redis 会保证一个 Lua 脚本里的所有命令的原子性。

Redis是否可以保证事务的原子性?

这个问题要从三个层面进行说明,分别是:

1)命令都正常执⾏,此时原⼦性可以保证;

2)命令⼊队时出错,EXEC 时会拒绝执⾏所有命令,原⼦性可以保证;

3)命令实际执⾏时出错,Redis 会执⾏剩余命令,原⼦性得不到保证;

Redis如何实现分布式锁?

Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系,Redis中可以使用SETNX命令来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。

如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样? set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的!

使用过Redis做消息队列吗?

Redis 本身提供了一些组件来实现消息队列的功能,但是多多少少都存在一些缺点,相比于市面上成熟的消息队列,例如 Kafka、Rocket MQ 来说并没有优势,因此目前我们并没有使用 Redis 来做消息队列。

关于 Redis 做消息队列的常见方案主要有以下:

1)Redis 5.0 之前可以使用 List(blocking)、Pub/Sub 等来实现轻量级的消息发布订阅功能组件,但是这两种实现方式都有很明显的缺点,两者中相对完善的 Pub/Sub 的主要缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃。

2)为了解决 Pub/Sub 模式等的缺点,Redis 在 5.0 引入了全新的 Stream,Stream 借鉴了很多 Kafka 的设计思想,有以下几个特点:提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。引入了消费者组的概念,不同组接收到的数据完全一样(前提是条件一样),但是组内的消费者则是竞争关系。

Redis Stream 相比于 pub/sub 已经有很明显的改善,但是相比于 Kafka,其实没有优势,同时尚未经过大量验证、成本较高、不支持分区(partition)、无法支持大规模数据等问题。

Redis的高可用

Redis的高可用主要从如下几个方面进行实现:

第一:数据的持久化(AOF,RDB)

第二:与哨兵机制结合实现负载均衡

第三:Redis集群高可用

Redis持久化

  • Spring工程中用于描述应用缓存的切入点注解你了解哪些?(@Cacheable,@CachePut,@CacheEvict,…)
  • Spring工程中假如需要基于AOP方式整合缓存应用需要在配置上加什么注解吗?(@EnableCaching)
  • Spring工程中CacheManager作用是什么?(管理缓存,定义缓存配置及实现,也可以定制)
  • 为什么要持久化?(更好的保证数据的可靠性,防止数据断电丢失)
  • Redis中持久化的方式有哪些?(RDB,AOF)
  • 你是否了解AOF中的Rewrite操作?(重写aof日志文件)
  • 你了解Redis中的哪些持久化配置?
  • 说说Redis中Rdb和Aof方式持久化数据的优势和劣势?
  • 生产环境下如何应用Rdb和Aof的持久化方式?(两种都要配置)

说说Redis 中持久化机制?

具体持久化机制是单独fork()一个子进程,将当前父进程的数据库数据复制到子进程的内存中,然后由子进程写入到临时文件中,持久化的过程结束了,再用这个临时文件替换上次的快照文件,然后子进程退出,内存释放。

说说Redis中持久化以及方式?

Redis持久化是把内存中的数据同步到硬盘文件中,当Redis重启后再将硬盘文件内容重新加载到内存以实现数据恢复的目的。具体持久化方式,分别为RDB和AOF方式。

如何理解Redis中RDB方式的持久化?

RDB是Redis默认的持久化方式,按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二进制文件。即快照存储,对应产生的数据文件为dump.rdb,通过配置文件中的save参数来定义快照的周期。

RDB 的优点:

1)RDB 文件是是经过压缩的二进制文件,占用空间很小,它保存了 Redis 某个时间点的数据集,很适合用于做备份。

2)RDB 非常适用于灾难恢复,它只有一个文件,并且内容都非常紧凑,可以(在加密后)将它传送到别的数据中心。

3)RDB 方式持久化性能好,执行持久化时可以fork 一个子进程,由子进程处理保存工作,父进程无须执行任何磁盘 I/O 操作。

4)RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

RDB 的缺点

1)RDB 在服务器故障时容易造成数据的丢失。RDB 允许我们通过修改 save point 配置来控制持久化的频率。但是,因为 RDB 文件需要保存整个数据集的状态, 所以它是一个比较重的操作,如果频率太频繁,可能会对 Redis 性能产生影响。所以通常可能设置至少5分钟才保存一次快照,这时如果 Redis 出现宕机等情况,则意味着最多可能丢失5分钟数据。

2)RDB 方式使用 fork 子进程进行数据的持久化,如果数据比较大的话,fork 可能会非常耗时,造成 Redis 停止处理服务N毫秒。如果数据集很大且 CPU 比较繁忙的时候,停止服务的时间甚至会到一秒。

3)Linux fork 子进程采用的是 copy-on-write 的方式。在 Redis 执行 RDB 持久化期间,如果 client 写入数据很频繁,那么将增加 Redis 占用的内存,最坏情况下,内存的占用将达到原先的2倍。刚 fork 时,主进程和子进程共享内存,但是随着主进程需要处理写操作,主进程需要将修改的页面拷贝一份出来,然后进行修改。极端情况下,如果所有的页面都被修改,则此时的内存占用是原先的2倍。

如何理解Redis中AOF方式的持久化?

AOF持久化是将Redis收到的每一个写命令都追加到文件最后,类似于MySQL的binlog。当Redis重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。AOF 持久化默认是关闭的,可以通过配置appendonly yes 开启。当 AOF 持久化功能打开后,服务器在执行完一个写命令之后,会将被执行的写命令追加到服务器状态的 aof 缓冲区(aof_buf)的末尾。然后再将 aof_buf 的内容写到磁盘。Linux 操作系统中为了提升性能,使用了页缓存(page cache)。当我们将 aof_buf 的内容写到磁盘上时,此时数据并没有真正的落盘,而是在 page cache 中,为了将 page cache 中的数据真正落盘,需要执行 fsync / fdatasync 命令来强制刷盘。这边的文件同步做的就是刷盘操作,或者叫文件刷盘可能更容易理解一些。

AOF 的优点: AOF 比 RDB更加可靠。你可以设置不同的 fsync 策略(no、everysec 和 always)。默认是 everysec,在这种配置下,redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据。AOF文件是一个纯追加的日志文件。即使日志因为某些原因而包含了未写入完整的命令(比如写入时磁盘已满,写入中途停机等等), 我们也可以使用 redis-check-aof 工具也可以轻易地修复这种问题。当 AOF文件太大时,Redis 会自动在后台进行重写。重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。整个重写是绝对安全,因为重写是在一个新的文件上进行,同时 Redis 会继续往旧的文件追加数据。当新文件重写完毕,Redis 会把新旧文件进行切换,然后开始把数据写到新文件上。AOF 文件有序地保存了对数据库执行的所有写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析也很轻松。如果你不小心执行了 FLUSHALL 命令把所有数据刷掉了,但只要 AOF 文件没有被重写,那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。

AOF 的缺点: 对于相同的数据集,AOF 文件的大小一般会比 RDB 文件大。根据所使用的 fsync 策略,AOF 的速度可能会比 RDB 慢。通常 fsync 设置为每秒一次就能获得比较高的性能,而关闭 fsync 可以让 AOF 的速度和 RDB 一样快。AOF 在过去曾经发生过这样的 bug :因为个别命令的原因,导致 AOF 文件在重新载入时,无法将数据集恢复成保存时的原样。阻塞命令 BRPOPLPUSH 就曾经引起过这样的 bug 。虽然这种 bug 在 AOF 文件中并不常见, 但是相较而言, RDB 几乎是不可能出现这种 bug 的。

如何理解Redis的混合持久化?

描述:混合持久化并不是一种全新的持久化方式,而是对已有方式的优化。混合持久化只发生于 AOF 重写过程。使用了混合持久化,重写后的新 AOF 文件前半段是 RDB 格式的全量数据,后半段是 AOF 格式的增量数据。

开启:混合持久化的配置参数为 aof-use-rdb-preamble,配置为 yes 时开启混合持久化,在 redis 4 刚引入时,默认是关闭混合持久化的,但是在 redis 5 中默认已经打开了。

关闭:使用 aof-use-rdb-preamble no 配置即可关闭混合持久化。混合持久化本质是通过 AOF 后台重写(bgrewriteaof 命令)完成的,不同的是当开启混合持久化时,fork 出的子进程先将当前全量数据以 RDB 方式写入新的 AOF 文件,然后再将 AOF 重写缓冲区(aof_rewrite_buf_blocks)的增量命令以 AOF 方式写入到文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。

优点:结合 RDB 和 AOF 的优点, 更快的重写和恢复。

缺点:AOF 文件里面的 RDB 部分不再是 AOF 格式,可读性差。

Save和Bgsave有什么不同?

SAVE生成 RDB 快照文件,但是会阻塞主进程,服务器将无法处理客户端发来的命令请求,所以通常不会直接使用该命令。

BGSAVEfork 子进程来生成 RDB 快照文件,阻塞只会发生在 fork 子进程的时候,之后主进程可以正常处理请求

Redis为什么要AOF重写?

AOF 持久化是通过保存被执行的写命令来记录数据库状态的,随着写入命令的不断增加,AOF 文件中的内容会越来越多,文件的体积也会越来越大。如果不加以控制,体积过大的 AOF 文件可能会对 Redis 服务器、甚至整个宿主机造成影响,并且 AOF 文件的体积越大,使用 AOF 文件来进行数据还原所需的时间就越多。举个例子, 如果你对一个计数器调用了 100 次 INCR , 那么仅仅是为了保存这个计数器的当前值, AOF 文件就需要使用 100 条记录。然而在实际上, 只使用一条 SET 命令已经足以保存计数器的当前值了, 其余 99 条记录实际上都是多余的。为了处理这种情况, Redis 引入了 AOF 重写:可以在不打断服务端处理请求的情况下, 对 AOF 文件进行重建(rebuild)。

说说AOF重写的过程、存在的问题、以及数据不一致问题?

描述:Redis 生成新的 AOF 文件来代替旧 AOF 文件,这个新的 AOF 文件包含重建当前数据集所需的最少命令。具体过程是遍历所有数据库的所有键,从数据库读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令。

命令:有两个 Redis 命令可以用于触发 AOF 重写,一个是 BGREWRITEAOF 、另一个是 REWRITEAOF 命令;开启:AOF 重写由两个参数共同控制,auto-aof-rewrite-percentage 和 auto-aof-rewrite-min-size,同时满足这两个条件,则触发 AOF 后台重写 BGREWRITEAOF。// 当前AOF文件比上次

重写后的AOF文件大小的增长比例超过100 
auto-aof-rewrite-percentage 100  
// 当前AOF文件的文件大小大于64MB 
auto-aof-rewrite-min-size 64mb

关闭:auto-aof-rewrite-percentage 0,指定0的百分比,以禁用自动AOF重写功能。auto-aof-rewrite-percentage 0 REWRITEAOF:进行 AOF 重写,但是会阻塞主进程,服务器将无法处理客户端发来的命令请求,通常不会直接使用该命令。BGREWRITEAOF:fork 子进程来进行 AOF 重写,阻塞只会发生在 fork 子进程的时候,之后主进程可以正常处理请求。REWRITEAOF 和 BGREWRITEAOF 的关系与 SAVE 和 BGSAVE 的关系类似。

AOF 后台重写存在的问题?

AOF 后台重写使用子进程进行从写,解决了主进程阻塞的问题,但是仍然存在另一个问题:子进程在进行 AOF 重写期间,服务器主进程还需要继续处理命令请求,新的命令可能会对现有的数据库状态进行修改,从而使得当前的数据库状态和重写后的 AOF 文件保存的数据库状态不一致。

如何解决 AOF 后台重写存在的数据不一致问题为了解决上述问题? Redis 引入了 AOF 重写缓冲区(aof_rewrite_buf_blocks),这个缓冲区在服务器创建子进程之后开始使用,当 Redis 服务器执行完一个写命令之后,它会同时将这个写命令追加到 AOF 缓冲区和 AOF 重写缓冲区。这样一来可以保证:1、现有 AOF 文件的处理工作会如常进行。这样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的。2、从创建子进程开始,也就是 AOF 重写开始,服务器执行的所有写命令会被记录到 AOF 重写缓冲区里面。这样,当子进程完成 AOF 重写工作后,父进程会在 serverCron 中检测到子进程已经重写结束,则会执行以下工作:1、将 AOF 重写缓冲区中的所有内容写入到新 AOF 文件中,这时新 AOF 文件所保存的数据库状态将和服务器当前的数据库状态一致。2、对新的 AOF 文件进行改名,原子的覆盖现有的 AOF 文件,完成新旧两个 AOF 文件的替换。之后,父进程就可以继续像往常一样接受命令请求了。

如何从Redis 上亿个 key中找出10 个包含Java的 key?

1)keys java 命令,该命令性能很好,但是在数据量特别大的时候会有性能问题。

2)scan 0 MATCH java 命令,基于游标的迭代器,更好的选择SCAN 命令是一个基于游标的迭代器(cursor based iterator): SCAN 命令每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。当 SCAN 命令的游标参数被设置为 0 时, 服务器将开始一次新的迭代, 而当服务器向用户返回值为 0 的游标时, 表示迭代已结束。

说说Redis中哨兵?

哨兵(Sentinel) 是 Redis 的高可用性解决方案,由一个或多个 Sentinel 实例组成的 Sentinel 系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器。

Sentinel 可以在被监视的主服务器进入下线状态时,自动将下线主服务器的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。

1)哨兵故障检测

检查主观下线状态

在默认情况下,Sentinel 会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他 Sentinel 在内)发送 PING 命令,并通过实例返回的 PING 命令回复来判断实例是否在线。如果一个实例在 down-after-miliseconds 毫秒内,连续向 Sentinel 返回无效回复,那么 Sentinel 会修改这个实例所对应的实例结构,在结构的 flags 属性中设置 SRI_S_DOWN 标识,以此来表示这个实例已经进入主观下线状态。

检查客观下线状态

当 Sentinel 将一个主服务器判断为主观下线之后,为了确定这个主服务器是否真的下线了,它会向同样监视这一服务器的其他 Sentinel 进行询问,看它们是否也认为主服务器已经进入了下线状态(可以是主观下线或者客观下线)。当 Sentinel 从其他 Sentinel 那里接收到足够数量(quorum,可配置)的已下线判断之后,Sentinel 就会将服务器置为客观下线,在 flags 上打上 SRI_O_DOWN 标识,并对主服务器执行故障转移操作。

2)哨兵故障转移流程

当哨兵监测到某个主节点客观下线之后,就会开始故障转移流程。核心流程如下:发起一次选举,选举出领头 Sentinel领头 Sentinel 在已下线主服务器的所有从服务器里面,挑选出一个从服务器,并将其升级为新的主服务器。领头 Sentinel 将剩余的所有从服务器改为复制新的主服务器。领头 Sentinel 更新相关配置信息,当这个旧的主服务器重新上线时,将其设置为新的主服务器的从服务器。

说说Redis的主从同步机制

redis主从机制了解么?怎么实现的?

Redis常见问题

如何理解缓存穿透和雪崩?

缓存穿透又称之为缓存击穿,是按key在请求缓存数据时,从缓存找不到此key对应的数据,而去执行了数据库(例如mysql)的查询,有时候,有人会基于这种行为做恶意攻击,不断请求缓存中不存在key,去访问你的关系数据库,导致数据库宕机。缓存雪崩类似出现了多个key的缓存击穿,当多个key在缓存中集体失效时,这些key又被同时访问到,此时的现象称之为缓存雪崩,

如何理解Redis缓存穿透?

描述:访问一个缓存和数据库都不存在的 key,此时会直接打到数据库上,并且查不到数据,没法写缓存,所以下一次同样会打到数据库上。此时,缓存起不到作用,请求每次都会走到数据库,流量大时数据库可能会被打挂。此时缓存就好像被“穿透”了一样,起不到任何作用。一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。

1.接口校验。在正常业务流程中可能会存在少量访问不存在 key 的情况,但是一般不会出现大量的情况,所以这种场景最大的可能性是遭受了非法攻击。可以在最外层先做一层校验:用户鉴权、数据合法性校验等,例如商品查询中,商品的ID是正整数,则可以直接对非正整数直接过滤等等。

2.缓存空值。当访问缓存和DB都没有查询到值时,可以将空值写进缓存,但是设置较短的过期时间,该时间需要根据产品业务特性来设置。3.布隆过滤器。使用布隆过滤器存储所有可能访问的 key,不存在的 key 直接被过滤,存在的 key 则再进一步查询缓存和数据库。可把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。

如何理解Redis缓存击穿?

描述:

某一个热点 key,在缓存过期的一瞬间,同时有大量的请求打进来,由于此时缓存过期了,所以请求最终都会走到数据库,造成瞬时数据库请求量大、压力骤增,甚至可能打垮数据库。

如何避免?

1.热点数据不设置过期时间,后由定时任务去异步加载数据,更新缓存。 这种方式适用于比较极端的场景,例如流量特别特别大的场景,使用时需要考虑业务能接受数据不一致的时间,还有就是异常情况的处理,不要到时候缓存刷新不上,一直是脏数据,那就凉了

2.使用互斥锁:在并发的多个请求中,保证只有一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,直接走缓存。

关于互斥锁的选择,网上看到的大部分文章都是选择 Redis 分布式锁,因为这个可以保证只有一个请求会走到数据库,这是一种思路。但是其实仔细想想的话,这边其实没有必要保证只有一个请求走到数据库,只要保证走到数据库的请求能大大降低即可,所以还有另一个思路是 JVM 锁。JVM 锁保证了在单台服务器上只有一个请求走到数据库,通常来说已经足够保证数据库的压力大大降低,同时在性能上比分布式锁更好。需要注意的是,无论是使用“分布式锁”,还是“JVM 锁”,加锁时要按 key 维度去加锁。我看网上很多文章都是使用一个“固定的 key”加锁,这样会导致不同的 key 之间也会互相阻塞,造成性能严重损耗。

使用 redis 分布式锁的伪代码,仅供参考:

public Object loadData(String key) throws InterruptedException {
    Object value = redis.get(key);
    // 缓存值过期
    if (value == null) {
        // lockRedis:专门用于加锁的redis;
        // "empty":加锁的值随便设置都可以
        if (lockRedis.set(key, "empty", "PX", lockExpire, "NX")) {
            try {
                // 查询数据库,并写到缓存,让其他线程可以直接走缓存
                value = getDataFromDb(key);
                redis.set(key, value, "PX", expire);
            } catch (Exception e) {
                // 异常处理
            } finally {
                // 释放锁
                lockRedis.delete(key);
            }
        } else {
            // sleep50ms后,进行重试
            Thread.sleep(50);
            return getData(key);
        }
    }
    return value;
}

如何理解Redis缓存雪崩?

缓存雪崩是当缓存服务器重启或者大量缓存集中在某一个时间段失效,造成瞬时数据库请求量大,压力骤增,导致系统崩溃。缓存雪崩其实有点像“升级版的缓存击穿”,缓存击穿是一个热点 key,缓存雪崩是一组热点 key。

如何避免?

1:打散过期时间,不同的key,设置不同的过期时间(例如使用一个随机值),让缓存失效的时间点尽量均匀。

2:做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期

3:加互斥锁。缓存失效后,通过加锁或者队列来控制写缓存的线程数量。比如对某个key只允许一个线程操作缓存,其他线程等待。

4:热点数据不过期。该方式和缓存击穿一样,要着重考虑刷新的时间间隔和数据异常如何处理的情况。

如何理解Redis缓存预热?

缓存预热就是系统上线后,将相关的数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,接下来,用户直接查询事先被预热的缓存数据。

解决思路:直接写个缓存刷新页面,上线时手工操作下;数据量不大,可以在项目启动的时候自动进行加载;定时刷新缓存;

如何理解Redis缓存更新?

除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:第一是定时去清理过期的缓存。第二是当有用户请求过来时,再判断这个请求所用到的缓存是否过期。过期的话就去底层系统得到新数据并更新缓存。两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂。具体用哪种方案,大家可以根据自己的应用场景来权衡。

如何理解Redis缓存降级?

当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。

参考日志级别设置预案:

普通:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;

警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;

错误:可用率低于90%或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阈值,此时可根据情况降级;

严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

如何保证数据库和缓存的数据一致性?

实际项目中,无论是先操作数据库,还是先操作缓存,都会存在脏数据的情况,有办法避免吗?答案是有的,由于数据库和缓存是两个不同的数据源,要保证其数据一致性,其实就是典型的分布式事务场景,可以引入分布式事务来解决,常见的有:2PC、TCC、MQ事务消息等。但是引入分布式事务必然会带来性能上的影响,这与我们当初引入缓存来提升性能的目的是相违背的。所以在实际使用中,通常不会去保证缓存和数据库的强一致性,而是做出一定的牺牲,保证两者数据的最终一致性。如果是实在无法接受脏数据的场景,则比较合理的方式是放弃使用缓存,直接走数据库。

保证数据库和缓存数据最终一致性的常用方案如下:

1)更新数据库,数据库产生 binlog。

2)监听和消费 binlog,执行失效缓存操作。

3)如果步骤2失效缓存失败,则引入重试机制,将失败的数据通过MQ方式进行重试,同时考虑是否需要引入幂等机制。

兜底:当出现未知的问题时,及时告警通知,人为介入处理。 人为介入是终极大法,那些外表看着光鲜艳丽的应用,其背后大多有一群苦逼的程序员,在不断的修复各种脏数据和bug。

---延迟双删

MongoDB

ES