分类 数据库 下的文章

Redis 的通信协议 RESP

Redis 作者认为数据库系统瓶颈不在网络流量, 而在数据库自身逻辑处理上, 所以使用了浪费流量的文本协议, 来换取即可的访问性能

RESP

RESP (Redis Serialization Protocol) 是一种直观的文本协议, 优势是过程简单, 解析极好, 劣势是耗费流量

RESP 将传输的结构数据分为 5 种最小单元类型, 单元结束时统一加上回车换行符 \r\n

  • 单行字符串以 + 符号开头
// 单行字符串 Hello World
+Hello World\r\n
  • 多行字符串以 $ 符号开头, 后跟字符串的长度
// 多行字符串 Hello World
$11Hello World\r\n
  • 整数值以 : 符号开头, 后跟整数的字符串形式
// 1024
:1024\r\n
  • 错误消息以 - 符号开头
-WRONGTYPE Operation against a key holding the wrong kind of value\r\n
  • 数组以 * 号开头, 后跟数组的长度
// 数组 [1,2,3]
*3\r\n:1\r\n:2\r\n:3\r\n
  • NULL
// NULL 用多行字符串表示, 长度写成 -1
$-1\r\n
  • 空字符串
// 空字符串用多行字符串表示, 长度填 0
// 两个 \r\n 之间表示空字符串
$0\r\n\r\n

客户端 -> 服务端

客户端向服务端发送执行只有一种格式, 多行字符串数组

// set author codehole
*3\r\n$3\r\nset\r\n$6author\r\n$8codehole\r\n

控制台输出的样式

*3
$3
set
$6
author
$8
codehole

服务端 --> 客户端

单行字符串响应

127.0.0.1:6379> set author codehole
OK

服务端响应内容

+OK

错误响应

// 试图对一个字符串进行自增的错误
127.0.0.1:6379> incr author
(error) ERR value is not an integer or out of range

服务端响应内容

-ERR value is not an integer or out of range

整数响应

127.0.0.1:6379> incr books
(integer) 1

服务端响应内容

:1

多行字符串

// 双引号括起来的字符串其实是多行字符串
127.0.0.1:6379> get author
"codehole"

服务端响应内容

$8
codehole

数组响应

127.0.0.1:6379> hset info name testname
(integer) 1
127.0.0.1:6379> hset info age 30
(integer) 1
127.0.0.1:6379> hgetall info
1) "name"
2) "testname"
3) "age"
4) "30"

hgetall 响应内容

*4
$4
name
$8
testname
$3
age
$2
30

嵌套

127.0.0.1:6379> scan 0
1) "0"
2)  1) "info"
    2) "books"
    3) "author"

服务端响应内容

*2
$1
0
*3
$4
info
$5
books
$6
author

总结

RESP 使用大量冗余的回车换行符, 已然是一个非常受欢迎的协议. 在技术领域, 性能并不是总是一切, 还有简单性, 易理解性和易实现性, 总之需要学会平衡.

Redis 的优胜劣汰 LRU 算法

本章主要介绍:

  1. redis 内存满了以后会怎样 ?
  2. redis 的近似 LRU 算法和严格 LRU 算法有什么区别 ?

Redis 的五种最大内存时的释放策略

场景

  • 当 redis 内存超出物理内存限制时, 会频繁和硬盘交换(swap), 极大影响性能
  • redis 可配置 maxmemory 参数来限制内存超出期望大小
  • 当实际内存超出 maxmemory 时, redis 提供了五种策略来让用户自己觉得如何腾出新空间提供读写服务

五种策略

  • noeviction: 默认策略. 不提供除 del 之外的写请求, 读请求可以继续进行. 保证不会丢失数据
  • volatile-lru: 尝试淘汰设置了过期时间且最少使用的 key.
  • volatile-ttl: 尝试淘汰设置了过期时间且 ttl 小的 key.
  • volatile-random: 尝试随机淘汰设置了过期时间的 key.
  • allkeys-lru: 在全体 key 中淘汰最少使用的.
  • allkeys-random: 在全体 key 中随机淘汰

严格 LRU 算法

  • 维护一个链表, 将所有设置了过期时间的 key 放在这个链表中
  • 当字典中某个元素被访问时, 它在链表中的位置会被移动到链表头部
  • 当空间满时, 就从链表尾部开始移除元素

近似 LRU 算法

为了不维护严格算法的链表, 节省内存

  • 给每个 key 增加一个额外 24bit 长度的小字段, 存储该 key 的最后一次访问时间戳
  • 当空间满时, 随机采样取出 5 个 key (数量可配置), 按时间戳淘汰掉最旧的 key
  • 循环第二步, 直到内存低于 maxmemory 值
  • 随机采样的范围取决于配置的策略是 volatile 还是 allkeys

Redis 3.0 开始, 增加了 淘汰池 进一步提升了近似 LRU 的效果:
上一次随机采样后未淘汰的 key, 会放入 淘汰池 留待下一次循环,
下一次随机采样的 key 会先和 淘汰池 中的 key 合并后, 再计算淘汰最旧的 key

Redis 过期策略

Redis 主要是惰性策略和定时删除两种策略结合, 来清理过期 key.

此处输入图片的描述

惰性策略

客户端访问 Key 时, 先进行过期判断, 如果已经过期立即删除.

定时删除

  1. Redis 将设置了过期时间的 key 放在一个独立的字典中
  2. 没 10s 扫描一次此字典, 随机取 20 个 key
  3. 删除这 20 个 key 中的已经过期 key
  4. 如果已经过期的 key 占比超过 1/4, 则重复步骤 2-4

问题一: 某一个时刻大量 key 过期, 影响单线程的 redis 读写操作怎么办 ?

  • 大量 key 过期一是堵塞线程,造成卡顿
  • 二是大量内存回收导致 cpu 飙升

优化方案

  • 每次定时扫描设置了 25ms 的上限, 当客户端请求正好遇到在扫描时, 至多等待 25ms, 当然如果客户端超时时间设置低于此阀值, 则会造成大量链接因为超时而关闭, 且还不会出现在 slowlog 中
  • 业务开发人员在设置过期时间时, 建议加上一个随机值, 避免在同一时间过期

问题二: del 删除大 key 卡顿主线程怎么办 ?

  • del 操作是立马释放内存, 小 key 的时候没没有明显延迟
  • 如果一个包含了上千万的 hash key, 删除操作就会造成主线程明显卡顿

优化方案

  • 使用上尽量避免大 key 的产生
  • Redis 4.0 版本引入了 unlink 指令, 对删除操作懒处理, 交给异步线程执行
  • unlink 是线程安全的. Redis 中所有数据比作一颗大树, unlink 是指令是砍掉这个key所属的树枝, 丢到火堆(异步线程池)里慢慢烧, 在离开大树的瞬间, 就不会被主线程访问到.

Redis 事务

Redis 的事务不同于关系型数据库, 事务模型很不严格, 我们对比着 MySQL 来看

基本用法

命令分为:

  • multi 事务的开始 (类似 MySQL 的 begin)
  • exec 执行事务 (类似 MySQL 的 commit)
  • discard 丢弃事务不执行 (类似 MySQL 的 rollback)
// exec
127.0.0.1:6379> get jw
(nil)
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set jw 666
QUEUED
127.0.0.1:6379> exec
1) OK
127.0.0.1:6379> get jw
"666"

这里要注意, 如果 exec返回非 OK (如nil )则表示执行失败

// discard
127.0.0.1:6379> get jw
(nil)
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set jw 888
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> get jw
(nil)

exec之前的命令都被缓存在事务队列里并为执行, QUEUED是一个简单的字符串, 表示指令已成功缓存到事务队列里.

原子性

文首我们说了 Redis 的事务模型不严格, 主要是指原子性, 请看下面代码

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set jw_str hehe
QUEUED
127.0.0.1:6379> incr jw_str
QUEUED
127.0.0.1:6379> set jw2 666
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
127.0.0.1:6379> get jw2
"666"

可以看出事务执行到第二个指令失败了(不能对字符串做+1的数学运算), 但是第三个指令仍然被执行. 所以 Redis 事务根本不具备原子性, 只仅仅满足了事务"隔离性"中的串行化--当前执行的事务不被其他事务打断.

watch

考虑一个业务场景, Redis 存储了账户余额, 现在需要对余额增减, 流程是先把值取出来, 修改后再写回去.

这时如果有多个客户端操作这个 key 就会出现并发问题, 而 Redis 提供的 watch 指令就可以解决这个问题., 使用方式如下:

127.0.0.1:6379> get jw
(nil)
127.0.0.1:6379> watch jw
OK
127.0.0.1:6379> set jw 666
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set jw 888
QUEUED
127.0.0.1:6379> exec
(nil)

从上面代码示例可以看出, watch 会在事务开始之前就盯住一个变量, Redis 在执行 exec 时会检查变量自 watch 后是否有被修改过, 包括当前客户端在内, 如果被修改过, 则事务执行失败.

注意, Redis 禁止在multiexec 之间执行 watch 指令, 必须在事务开启指令 multi 之前盯住变量

Redis 持久化原理

概述


Redis 是个内存数据库, 数据全部存储在内存中, 如果突然宕机, 数据就会全部丢失, 因此有了将数据刷到硬盘保存的持久化机制. Redis 持久化分两种方式, 一种是 RDB 快照, 另一种是 AOF 日志.

快照就是全量备份, 内存数据的二进制序列化形式, 在存储上非常紧凑. AOF 日志是连续的增量备份, 记录的是内存数据修改的指令记录文本.

快照


由于 Redis 是单线程程序, 还要同时负责多个客户端的并发读写操作和内存数据结构的逻辑读写. 所以快照持久化面临如下两个问题:

  • 快照需要大量 IO 操作可能会阻塞线上业务, 拖慢整体性能, 而且快照又不能使用多路复用
  • 持久化的同时, 还需要接受客户端请求, 正在持久的数据可能会被修改删除

Redis 使用 操作系统的多进程 COW ( Copy on Write ) 机制来实现快照持久化

原理

Redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程. 子进程做数据持久化, 不会修改现有的内存数据结构, 它只是对现有的数据结构遍历读取, 然后序列化写到磁盘中. 但父进程不一样, 它必须持续服务客户端请求, 然后对内存数据结构进行不间断修改.

这是就会使用到操作系统的 COW 机制来进行数据段页面的分离.

当父进程对一个页面的数据进行修改时, 会将被共享的页面复制一份分离出来, 然后对这个新复制的页面进行修改, 子进程继续复制原有页面, 不会受到影响, 也就是子进程被创建那一刻的数据, 所以为什么叫"快照".

随着父进程的持续修改, 越来越多的共享页面被复制分离出来, 内存就会持续增长, 不用担心内存会暴增, 因为不会超过原有数据内存的2倍. 而且往往冷数据占比较多, 很少会出现所有的页面都被复制分离的情况.

扩展一下, 因为快照要遍历整个内存, 同时大块写磁盘也是非常耗时的IO操作, 所以建议在从节点上进行. 由于从节点可能会因为网络等问题, 造成同步延迟, 就会操作快照不全, 所以监控也很重要.

AOF


aof 日志存储的是 Redis 服务器的顺序指令序列, aof 日志只记录对内存进行修改的记录.

aof 同样也面临着两个问题:

  • 随着实例的运行, 修改性指令序列越来越多, 在存储和重放(重启后恢复)上都存在着性能问题
  • aof 写日志文件的频率, 过快会因为IO拉升机器负载, 过慢如果机器突然宕机就会丢失数据

重写

为解决第一个问题, Redis 提供了 bgrewriteaof 指令用于对 aof 日志瘦身. 其原理是开启一个子进程对现有内存数据进行遍历, 转换成一些列 Redis 操作指令, 序列化到一个新的 aof 日志文件中. 序列化完毕后的再将操作期间发生的增量 aof 日志追加到新的日志中, 然后代替旧的日志文件.

fsync

进程对 aof 日志文件写操作时, 实际是将内容写到操作系统内核为文件描述符分配的一个内存缓存中, 然后内核 异步 将数据刷到磁盘.

为解决第二个问题, Redis 提供配置刷新周期, 通常是 1s 就会调用 Linux 的 glibc 提供的 fsync(int fd)函数, 它可以将指定文件的内容强制从内核缓存刷到磁盘.

扩展一下. Redis 还提供了另外两种可选方案, 生产环境中基本不会使用, 了解即可

  • 一是永不调用 fsync, 让操作系统来决定合适刷到磁盘
  • 另一个是每来一条指令就调一次 fsync

混合持久化


Redis 4.0 提供

快照和aof日志都有各自的痛点

  • 快照因为是每隔一段时间持久化一次, 就会丢失宕机时刻与上一次持久化之间的数据
  • aof 因为存储的是指令序列, 恢复重放时要花费很长时间

综合利弊, 使用 aof 还是更靠谱一点, 现在 Redis 4.0 提供了更好的混合持久化选项

混合持久化

  • 将 rdb 文件的内容和增量的 aof 日志放在一起
  • aof 日志只存储 rdb 持久化开始到当前发生的增量日志
  • 重启时, 先加载 rbd 内容, 再重放增量 aof 日志

这样就可以解决上面的两个痛点

删除kafka的consumer和topics

谨慎操作
kafka 版本 0.10

删除 consumers

原因: 重置 offset, 或者是强迫症想清空不用的 consumer
操作:

# 进入控制台
bin/zookeeper-shell.sh localhost:2181
# 查看所有消费者
ls /consumers
# fuck
rmr /consumers/hangou
# 再查看 没了
ls /consumers

删除 topics

原因: 腾出空间
操作:

  • 物理删除数据
# 进入 server.properties 里配置的数据目录 log.dirs=/data/kafka-logs 
cd /data/kafka-logs/
# 删除对应 topic 目录, 配置了多少分区这就有多少目录
rm -rf lion_sql-log*
  • 删除 zookeeper 里的记录
# 进入控制台
bin/zookeeper-shell.sh localhost:2181
# 查看当前的所有 topics, 发现刚删除的 topics 还在这里躺着
ls /brokers/topics/
# fuck you
rmr /brokers/topics/lion_sql-log
# 再查看 没了
ls /brokers/topics

Elasticsearch查询实现类似MySQL的in/not in

别问我为什么要单独一篇文章写这个,我会告诉你这问题困扰我很久了?搞定后的酸爽谁试谁知道

数据说明

假如es里有index为lion_db_vdoid, type为t_vdoid
有两条记录,字段vdoid分别为1和2

实现IN

查询vdoid字段包含2,3的数据

  "query": {
    "terms": {
      "vdoid": [
        "2", "3"
      ]
    }
  }

mysql_in

实现NOT IN

查询vdoid字段不等于2,3的数据

  "query": {
    "bool": {
      "must_not": [
        {
          "terms": {
            "vdoid": [
              "2", "3"
            ]
          }
        }
      ]
    }
  }

mysql_not_in

启发

灵活使用must_not,结合其他系统参数,达到不可思议的结果

Hive查询非Group By字段

示例表结构和数据:

hive> desc test2;
OK
id                      int                                         
value                   string                                      
Time taken: 0.024 seconds, Fetched: 2 row(s)

hive> select * from test2;
OK
1    a
1    b
2    c
3    d
Time taken: 0.042 seconds, Fetched: 4 row(s)


如下SQL语句在MySQL中是比较常见的写法,但是在Hive中缺不行:

select id, value from test2 group by id;

在Hive中执行会报错:
FAILED: SemanticException [Error 10025]: Line 1:10 Expression not in GROUP BY key 'value'
当使用group by字句,select语句,只能包含group by包含的列。当然,在select语句,可以有多个聚合函数(例如count)

-- 聚合函数是可以的
select id, count(*) from test2 group by id;

解决办法


  • 第一种方式: 妥协。一般group by后还要查非分组字段,如果业务上这个字段也是相同的,将这个字段也加入到group by中
select id,value from test2 group by id,value;
  • 第二种方式:collect_set()
hive> select id,collect_set(value) from test2 group by id;

1    ["b","a"]
2    ["c"]
3    ["d"]

更神奇的来了:

hive> select id, collect_set(value)[0] from test2 group by id;

1    a
2    c
3    d

炸裂,有没有,惊呼: 这样也可以....

Redis 内存优化案例

Redis的配置文件中有这么两项配置:

hash-max-ziplist-entries 512
hash-max-ziplist-value 64

其中的ziplist代表数据结构,是一种数据压缩方式,作用是减少内存的使用空间

在某个阀值范围内,hashtable会使用ziplist,对数据进行压缩,超出阀值后,会自动转为使用正常的hashmap结构

上面这两项就是定义这个阀值

hash-max-ziplist-entries 512

hashtable中的条目数量在512以下时,使用ziplist

hash-max-ziplist-value 64

hashtable中每个key/value的长度都小于64字节时,使用ziplist

以上2个条件中,任意一个条件超过设置值时,就不再使用ziplist了

案例

之前在网上看过一个案例,介绍了Instagram使用这项配置的实践经验

Instagram是一个超大型的图片类应用,他们有一个需求:
根据图片ID得到作者ID

最简单的实现方式就是使用string类型
图片ID为KEY,作者ID为VALUE,一条一条的set/get

经测试,图片量为3亿时,一共需要20G左右的内存

经过一些优化后,效果不明显,他们便向Redis的一个开发者咨询解决方案

得到的建议是:

对数据进行分段,使用hash结构

因为hash结构在一定数据量下会进行压缩存储,可以节约很多内存

经过反复测试,在他们的环境下,entries阀值为1000时最合适,超过的话,CPU的压力较大

所以,数据分段的方式为:

1000条数据为一段,放在一个hash表中

例如 图片ID为1234888

他在一个hash表中,这个hash表的key为 1234,里面有1000个field,其中就包括了 888,值为其作者ID

请输入图片描述

取1234888的作者ID时,就是取得key为1234的hashtable中field为888的值

经过测试,使用这种方式后,内存的使用量降到了5G左右,效果非常明显

类似的配置项还有:
list-max-ziplistzset-max-ziplist

检测MySQL是否宕机并重启

检测MySQL是否宕机并重启,建立脚本添加到crontab

#!/bin/bash
 
result=`/usr/bin/mysqladmin ping`
expected='mysqld is alive'
 
if [[ "$result" != "$expected" ]]
then
echo "It's dead - restart mysql"
 
# email subject
SUBJECT="[MYSQL ERROR] - Attempting to restart service"
 
# Email To ?
EMAIL="info@endyourif.com"
 
# Email text/message
EMAILMESSAGE="/tmp/emailmessage.txt"
echo "$result was received"> $EMAILMESSAGE
echo "when we were expected $expected" >>$EMAILMESSAGE
# send an email using /bin/mail
mail -s "$SUBJECT" "$EMAIL" < $EMAILMESSAGE
 
sudo /etc/init.d/mysql restart
fi

crontab中5分钟检测一次

*/5 * * * * /<path_to>/scripts/mysql.sh

原文出自http://www.codeproject.com/Articles/988967/Mysql-Uptime-Check-Script

sql使用for update控制并发

MySQLselect * from table where …... for update的用法

由于InnoDB预设是Row-Level Lock(行级锁),所以只有「明确」的指定主键,MySQL才会执行Row lock (只锁住被选取的资料例) ,否则MySQL将会执行Table Lock (表锁)

举个例子: 假设有个表单t,里面有id跟name二个栏位,id是主键。


CREATE TABLE t (
id int(11) NOT NULL DEFAULT '0',
name varchar(255) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

例1: (明确指定主键,并且有此笔资料,row lock)


  • 由此可见,当明确指定主键,并且有此资料时,锁的是where后面的记录,即这里的id= 1; 接下来来看下没有此资料的情况下会不会被锁。

窗口一:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t where id = 1 for update;
id name
1 bingo
1 row in set (0.00 sec)

窗口二:

mysql> select * from t where id = 1;
id    name
1    bingo
1 row in set (0.01 sec)
mysql> update t set name = 'xxm' where id = 2;
Query OK, 1 row affected 
(0.08 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> update t set name = 'icey' where id = 1;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

例2: (明确指定主键,若查无此笔资料,无lock)


  • 由此得出结论,在没有此资料的情况下,即使你for update也是不锁的

窗口1:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t where id = 11 for update;
Empty set (0.00 sec)

窗口2:

mysql> update t set name = 'qweq' where id = 1;
Query OK, 1 row affected (0.04 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> update t set name = 'qw' where id = 2;
Query OK, 1 row affected (0.06 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> update t set name = 'vqw' where id = 3;
Query OK, 1 row affected (0.05 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> update t set name = 'vqws' where id = 4;
Query OK, 1 row affected (0.04 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> update t set name = 'vqs' where id = 5;
Query OK, 1 row affected (0.03 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> update t set name = 'vqs' where id = 11;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0 Changed: 0 Warnings: 0
mysql> select * from t;
+----+------+

id    name
1    qweq
2    qw
3    vqw
4    vqws
5    vqs

例3: (无主键,table lock)


  • 由此可见,没有明确主键的情况下锁全表;

窗口1:

mysql> select * from t where name ='qweq' for update;
id    name
1    qweq
1 row in set (0.00 sec)

窗口2:

mysql> update t set name = 'vqs' where id = 1;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> update t set name = 'vqs' where id = 2;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

例4: (主键不明确,table lock)


  • 在主键不明确的情况下,锁全表
SELECT * FROM products WHERE id<>'3' FOR UPDATE;

窗口1:

mysql> select * from t where id <> 2 for update;
id    name
1    qweq
3    vqw
4    vqws
5    vqs
4 rows in set (0.00 sec)

窗口2:

mysql> update t set name = 'vqs' where id = 1;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> update t set name = 'vqs' where id = 2;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

例5: (主键不明确,table lock)


  • 锁全表

窗口1:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t where id like 3 for update;
id    name
3    vqw
1 row in set (0.00 sec)

窗口2:

mysql> update t set name = 'vqs' where id = 1;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> update t set name = 'vqs' where id = 2;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

注1: FOR UPDATE仅适用于InnoDB,且必须在交易区块(BEGIN/COMMIT)中才能生效。