1. 需求描述 热点数据保存在缓存中,缓存过期的瞬间,避免大量的请求直接打到数据库服务器上。
2. singleflight 使用 demo package main import ( "errors" "fmt" "golang.org/x/sync/singleflight" "sync" "sync/atomic" ) var ErrNotFund = errors.New("not fund") func main() { var clt = NewCacheClient() wg := sync.WaitGroup{} wg.Add(10) for i := 0; i < 10; i++ { //模拟10个并发请求从缓存获取数据 go func() { defer wg.Done() clt.Get("a") }() } wg.Wait() } type CacheClient struct { keys atomic.Value //实际存储的是 map[string]string group singleflight.Group } func NewCacheClient() *CacheClient { clt := new(CacheClient) m := make(map[string]string) clt.
1. 公钥、私钥 非对称加密里的概念,公钥和私钥是成对出现的,公钥加密的信息只能由对应的私钥解密,私钥加密的信息只能由对应的公钥解密。
2. 命令行、实验 生成私钥 //1024 为 rsa 算法中一个因子的长度,一般为1024或2048 openssl genrsa -out private.pem 2048 查看生成的密钥信息 openssl rsa -in private.pem -text 从私钥中提取出公钥 openssl rsa -in private.pem -pubout -out public.pem 使用公钥加密 openssl rsautl -encrypt -inkey public.pem -pubin -in test.text -out encrypt_test.txt 使用私钥解密 openssl rsautl -decrypt -inkey private.pem -in encrypt_test.txt -out decrypt_test.txt 使用私钥签名 openssl rsautl -sign -inkey private.pem -in test.text -out sign.text 使用公钥验签 openssl rsautl -verify -pubin -inkey public.pem -in sign.text -out verify.
什么是 CAS CAS 的全称是 Compare And Swap.
该指令会将给定的值和一个内存地址中的值进行比较,如果它们是同一个值,就使用新值替换内存地址中的值,整个操作过程是原子性的。
那啥是原子性呢?
原子性我们只需要记住一点,就是这个操作总是基于最新的值进行计算,如果同时有其它线程已经修改了这个值,那么这次操作不成功。
听起来还是很绕?贴个 golang 源码片段(还带汇编的哦)
// func cas(val *int32, old, new int32) bool // Atomically: // if *val == old { // *val = new; // return true; // }else // return false; TEXT sync·cas(SB), 7, $0 MOVQ 8(SP), BX MOVL 16(SP), AX MOVL 20(SP), CX LOCK CMPXCHGL CX, 0(BX) JZ ok MOVL $0, 24(SP) RET ok: MOVL $1, 24(SP) RET 再贴一段早期的 Mutex 源码片段 源码地址
01 | 基础架构:一条SQL查询语句是如何执行的? MySQL的基本架构是什么样子的 分为客户端、server层和引擎层,其中引擎层是插件式的,server层的模块包括连接器、查询缓存、分析器、优化器、执行器
连接器的主要功能是什么 负责与客户端建立连接、获取权限、维持和管理连接
修改用户的权限后什么时机生效 连接建立后,连接器会去权限表获取你拥有的权限,之后的权限判断都依赖此时获取到的权限,所以修改用户的权限并不会对已经建立的连接产生影响。
为什么更加建议使用长连接 连接的建立是一个复杂的过程,如果频繁的建立连接,会影响性能
长连接可能会导致哪些问题且如何解决这些问题 MySQL 在执行过程中临时使用的内存是管理在连接对象里面的,连接断开的时候才会被释放,如果长连接过多会导致内存占用过大。 解决方案有两个:(1)定期断开长连接;(2)MySQL5.7 及以后的版本,可以在每次执行一个较大的操作后执行一次mysql_reset_connection来初始化连接资源,此操作不会重连也不会重新做权限校验,只是将连接初始化到刚刚建立时的状态 查询缓存的功能是什么、它是如何工作的 查询语句被作为 key,查询结果被作为 value,以 key-value的形式缓存到内存中,这样如果一个查询语句已经存在于内存中,结果可以被直接返回
为什么不建议使用查询缓存 查询缓存会频繁失效,缓存命中率特别低,整体来说弊大于利;除非查询的表是一个静态表;
分析器的功能是什么 判断要做什么 进行词法分析、语法分析,判断输入的 SQL 语法是否正确
优化器的功能是什么 决定该怎么做 索引选择、多表关联(join)的时候决定表的连接顺序,目的是为了以最快的方式获取数据;
执行器的功能是什么 真正去做 做之前要判断是否有足够的权限,权限足够,调用引擎提供的接口获取数据 (命中查询缓存的情况下,在返回结果前会做权限验证;查询也会在优化器之前调用precheck验证权限)
思考题 问题: 如果表 T 中没有字段 k,而你执行了这个语句 select * from T where k=1, 那肯定是会报“不存在这个列”的错误: “Unknown column ‘k’ in ‘where clause’”。你觉得这个错误是在我们上面提到的哪个阶段报出来的呢?
答案: 分析器
02 | 日志系统:一条SQL更新语句是如何执行的 什么是 redo log? 类比于实际生活中酒店记账的操作,如果有人赊账,可以先把账记录到粉板上,等到空闲或是粉板记不下的时候再核算整理到账本上,这样可以避免频繁翻查账本,提高了工作效率;
赊账相当于数据库的更新操作,记录到粉板相当于写 redo log,粉板满了相当于 redo log文件满了,整理到账本上相当于刷脏,频繁翻查账本相当于去磁盘上获取对应的记录
redo log 是在引擎层实现的重做日志,是 InnoDB 特有的
redo log 的作用是什么? 加快更新速度 保证 InnoDB 的 crash-safe 能力(即使数据库异常重启,之前提交的记录也不会丢失) 什么是 binlog? binlog是在server层实现的归档日志
binlog的作用是什么? 数据恢复 主从同步 binlog 和 redo log 的区别是什么 binlog 是server层的归档日志,redo log是引擎层的InnoDB引擎特有的重做日志 redo log 是物理日志,记录的是在某个数据页上做了什么更改;binlog是逻辑日志,记录的是操作的原始逻辑,例如 “给 ID=2 这一行的 c 字段 + 1” binlog 是追加写的,一个文件写满就继续写下一个文件,不会覆盖之前的日志;redo log的空间固定,是循环写的; 什么是两阶段提交 update T set c=c+1 where id=2 的执行过程大致如下(浅色在InnoDB内部,深色在执行器中): 更新操作过程中,先把操作记录写入 redo log 并将 redo log 标记为 prepare 状态,然后写入 binlog,最后再提交事务并将 redo log 的状态更改为 commit
03 | 事务隔离:为什么你改了我还看不见? 什么是事务? 简单来说,事务就是要保证一组数据库操作,要么全部成功,要么全部失败
事务的ACID 原子性(Atomicity) 一致性(Consistency) 隔离性(Isolation) 持久性(Durability) 事务并发可能产生的问题 脏读(dirty read) 不可重复读(non-repeatable read) 幻读(phantom read) 隔离级别 读未提交(read uncommitted):一个事务还没提交时,它做的变更就能被别的事务看到 读提交(read committed):一个事务提交之后,它做的变更才会被其他事务看到 可重复读(repeatable read):一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的 串行化(serializable ):顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行 读提交和可重复读的实现 数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。
“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。
“读提交”隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的。
“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念。
“串行化”隔离级别下直接用加锁的方式来避免并行访问
尽量避免使用长事务 视图的本质是保留了undo log,当需要获取旧版本数据时,通过undo log执行回滚推算出旧版本的数据,当系统里没有比这个undo log更早的视图活跃时,才可以删除这个undo log。所以长事务意味着大量的undo log被保存,这会大量占用空间
事务的启动方式 begin 和 start transaction,配套的语句是 commit,回滚语句是 rollback
建议使用 set autocommit=1 set autocommit=0,会导致自动启动事务且不会自动提交,直到执行 commit 或 rollback,意外导致长事务
04 | 深入浅出索引(上) 什么是索引 索引就像书的目录一样,为了提高数据的检索速度
索引常见的模型极其利弊 哈希表:适用于只有等值查询的情况,区间查询非常慢 有序数组:适用于等值查询和范围查询,数据更新慢、效率低 搜索树(N叉树):读写性能优秀、适配磁盘的访问模式(按块读取),被广泛应用于数据库引擎 InnoDB 的索引模型 B+树,每一个索引对应一颗 B+ 树
主键索引的叶子节点存储的是整行数据
非主键索引的叶子节点存储的是主键索引的值
基于主键索引和普通索引的查询有什么区别? 通过主键索引获取数据,只需要搜索主键索引对应的搜索树;
通过非主键索引获取数据,需要先搜索非主键索引对应的树获取到对应行的主键值,然后再去搜索主键对应的搜索树,获取到整行数据,这个过程称为回表
索引维护过程 索引的维护牵涉到数据页的分裂和合并,还会出现数据页利用率低下的问题,我们应该尽可能地优化,去避免这种出力不讨好的事
主键的选择 InnoDB要求记录必须有且只能有一个主键,因为数据行的存储顺序就是按主键的顺序来的。
指定自增主键 > 选择第一个非空的唯一索引 > 自动生成一个不可见的ROW_ID
推荐设置自增主键 性能:自增主键保证数据的插入都是递增插入,追加操作不会导致数据页的分裂、合并 存储:整型做主键只需要4个字节,这对比业务中的字段应该更加短小,主键长度越小,那非主键索引的叶子节点存储的数据就越少
为什么要重建索引 由于删除、更新等操作会导致页分裂,使页面利用率变低;重建索引的过程会创建一个新的索引,把数据按顺序插入,这样页面的利用率最高,也就是索引更紧凑、更省空间
问题讨论 对于 InnoDB 表 T,如果你要重建索引 k,你的两个 SQL 语句可以这么写:
alter table T drop index k; alter table T add index(k); 也可以这么写:
alter table T drop primary key; alter table T add primary key(id); 对于上面这两个重建索引的作法,说出你的理解。如果有不合适的,为什么,更好的方法是什么?
答案 重建索引 k 的做法是合理的,可以达到省空间的目的。但是,重建主键的过程不合理。不论是删除主键还是创建主键,都会将整个表重建。所以连着执行这两个语句的话,第一个语句就白做了。这两个语句,你可以用这个语句代替:alter table T engine=InnoDB
05 | 深入浅出索引(下) 覆盖索引 一个查询操作,如果索引 k 已经包含我们要获取的所有字段,可以少一次回表操作,我们称这种情况为覆盖索引
最左前缀 MySQL数据模型是B+树,索引项在B+树里的存储是按照索引定义里面字段出现的顺序来排序的,只要满足索引的最左前缀(可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符),就可以利用索引来加速检索
建立联合索引的时候,如何安排字段顺序 根据最左前缀规则,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的 空间最小原则:如果查询中既有同时筛选 a, b 的情况,又有分别筛选 a,b 的情况,且 a 的字段长度 > b 的字段长度,应该建立 (a, b) 和 b 这两个索引,而不是 (b, a) 和 a 这两个索引 索引下推 对于联合索引 (a, b, c),查询时筛选 a=1, c!=1,按最左前缀原则仅能使用到索引 a,每次获取到一行a=1的数据就需要回表,再做c!=1的判断。MySQL5.6引入来索引下推,在回表前会先做 c!=1的判断,不满足直接跳过,从而减少来回表次数,加快来检索
问题讨论 DBA 小吕在入职新公司的时候,就发现自己接手维护的库里面,有这么一个表,表结构定义类似这样的:
CREATE TABLE `geek` ( `a` int(11) NOT NULL, `b` int(11) NOT NULL, `c` int(11) NOT NULL, `d` int(11) NOT NULL, PRIMARY KEY (`a`,`b`), KEY `c` (`c`), KEY `ca` (`c`,`a`), KEY `cb` (`c`,`b`) ) ENGINE=InnoDB; 公司的同事告诉他说,由于历史原因,这个表需要 a、b 做联合主键,这个小吕理解了。
06 | 全局锁和表锁 :给表加个字段怎么有这么多阻碍? mysql锁分类 全局锁 表级锁 行锁 如何加全局锁 MySQL加全局读锁:Flush tables with read lock (FTWRL)
全局锁的使用场景是什么 全库逻辑备份
通过加全局锁备份数据库会导致什么问题 在主库上加全局锁,加锁期间整库都不能更新,业务几乎停摆 在从库上加全局锁,加锁期间不能执行主库同步过来的binlog,导致主从延迟 备份数据为什么要加锁 为了数据的一致性
通过一致性读来备份数据库 在可重复读隔离级别下,开启一个事务,就可以拿到一个一致性读视图,由于MVCC的支持,备份期间数据库是可以正常更新的
既然可以用一致性读视图来备份数据库为什么还需要FTWRL 一致性读并不是每个数据库引擎都支持的,要想使用这种方式备份方式,要求数据库里每张表使用的都是支持事务的引擎
为什么不使用set global readonly=true 某些系统中,readonly会被用来做其它逻辑,比如判断一个库是主库还是从库 执行 FTWRL 之后,即使客户端异常断开,MySQL也会自动释放这个全局锁;如果执行 set global readonly=true之后客户端异常断开,则正库会一直处于不可写状态 表级锁的分类 表锁 元数据锁(MDL) lock tables锁对线程的限制 加锁:lock tables … read/write 解锁:unlock tables … lock tables 除了限制别的线程的读写,也限制来接下来这个线程可以进行的操作
元数据锁MDL 无需显示调用,在访问一个表的时候会自动加锁,用于解决或者保证DDL操作与DML操作之间的一致性。 对一个表做增删改查操作的时候,加 MDL 读锁; 当要对表做结构变更操作的时候,加 MDL 写锁; 如何安全地给小表加字段 在 alter table 语句里设置等待时间,如果在这个时间拿到MDL写锁最好,拿不到就先放弃,以免影响后面的业务,之后再重试执行
问题讨论 备份一般都会在备库上执行,你在用–single-transaction 方法做逻辑备份的过程中,如果主库上的一个小表做了一个 DDL,比如给一个表上加了一列。这时候,从备库上会看到什么现象呢?
07 | 行锁功过:怎么减少行锁对性能的影响? 行锁由引擎实现,InnoDB支持行锁,MyISAM不支持 两阶段锁的概念是什么? mysql中,行锁在需要的时候才会加上,但是要等事务提交以后才会释放
了解两阶段锁对使用事务有什么帮助? 事务中要锁多个行,要尽量把最可能造成冲突、最可能影响并发度的语句往后放
死锁的概念是什么? 当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁
死锁的处理策略有哪两种? 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置。 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。 等待超时处理死锁的机制什么?有什么局限? 机制就是等待超时自动退出。但是超时时间设置过长会导致其它事务等待时间过长,超时时间设置太短又容易造成误伤,多会有损业务
死锁检测处理死锁的机制是什么? 有什么局限? 机制:每当一个事务被锁的时候,就要看看它所依赖的线程有没有被别人锁住,如此循环,最后判断是否出现了循环等待,也就是死锁。
局限:判断死锁的过程是一个时间复杂度为 O(n) 的操作,假如有 1000 个并发线程要更新同一行,那死锁检测是 100万量级,会占用大量CPU资源
有哪些思路可以解决热点更新导致的并发问题? 临时关闭死锁检测 控制并发度 将一行改成逻辑上的多行来减少锁冲突