跳到文章开头
  1. Entire-notes/

Mysql-Redis

·8 分钟
主从部分
 ·  页面点击量:
目录

Redis详解
#

1. Redis简介
#

  • 使用C语言开发的内存数据库,读写速度快,广泛用于缓存方向
  • 可以用于分布式锁,甚至是消息队列
  • 提供多种数据类型来支持不同的业务场景
  • 支持事务、持久化、Lua脚本、多种集群方案

2. Redis 和 Memcached 对比
#

2.1 相同点
#

  • 都基于内存,一般都是用来当缓存使用
  • 都有过期策略
  • 两者性能都非常高

2.2 区别
#

  • Redis 支持更丰富的数据类型【key-value类型、提供l ist set zset hash等数据结构的存储】、更复杂的应用场景。Memcached只支持简单的 key-value 类型;
  • Redis支持数据的持久化,有灾难恢复机制,Memcached不支持;
  • Redis在服务器内存不足时,可以将不用的数据放到磁盘上;Memcached则直接报异常;
  • Redis原生支持集群模式,Memcached原生不支持,需要依靠客户端实现往集群分片写入数据;
  • Memcached是多线程的非阻塞IO复用的网络模型,Redis使用单线程的多路IO复用模型【6.0以后引入了多线程】
  • Redis支持发布订阅模型、Lua脚本、事务等功能,而Memcached不支持。并且,Redis支持更多的编程语言
  • Memcached过期数据只使用惰性删除,而Redis同时使用惰性删除和定期删除

3. 缓存数据处理流程
#

  1. 若用户请求的数据在缓存中则直接返回
  2. 若缓存中不存在,则查询数据库
  3. 若数据库中存在,则更新缓存
  4. 若数据库不存在,则返回空数据

4. 为什么使用Redis
#

4.1 高性能考虑
#

  • 用户每次请求都查询数据库,涉及磁盘IO操作,速度慢。
  • 若把高频访问且不经常改变的数据放入缓存,能极大提高查询速度。
  • 注意要保持数据库和缓存中的数据的一致性。如果数据库中的对应数据改变,需要同步改变缓存中的数据

4.2 高并发考虑
#

  • 如MySQL数据库的QPS大概在1w左右【4核8G】,但是使用Redis缓存之后,很容易能达到10w+,甚至最高能到达30w+【单机情况】
  • 故,直接操作缓存能承受更多的请求,进而提供系统的并发能力

5. Redis常见数据结构及应用场景
#

5.1 string
#

  • string数据结构是简单的 key-value类型,是Redis自行构建的动态字符串。可以保存文本数据、二进制数据,获取长度的复杂度为O(1),不会造成缓冲区溢出
  • 常用命令:set, get, strlen, exists, decr, incr, setex
  • 应用场景:需要计数的场景,如用户访问次数、热点文章的点赞转发数量等等

5.1.1 基本操作
#

127.0.0.1:6379> set key value #设置 key-value 类型的值
OK
127.0.0.1:6379> get key # 根据 key 获得对应的 value
"value"
127.0.0.1:6379> exists key  # 判断某个 key 是否存在
(integer) 1
127.0.0.1:6379> strlen key # 返回 key 所储存的字符串值的长度。
(integer) 5
127.0.0.1:6379> del key # 删除某个 key 对应的值
(integer) 1
127.0.0.1:6379> get key
(nil)
127.0.0.1:6379> mset key1 value1 key2 value2 # 批量设置 key-value 类型的值
OK
127.0.0.1:6379> mget key1 key2 # 批量获取多个 key 对应的 value
1) "value1"
2) "value2"

5.1.2 计数器
#

127.0.0.1:6379> set number 1
OK
127.0.0.1:6379> incr number # 将 key 中储存的数字值增一
(integer) 2
127.0.0.1:6379> get number
"2"
127.0.0.1:6379> decr number # 将 key 中储存的数字值减一
(integer) 1
127.0.0.1:6379> get number
"1"

5.1.3 过期时间设置
#

127.0.0.1:6379> expire key  60 # 数据在 60s 后过期
(integer) 1
127.0.0.1:6379> setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire)
OK
127.0.0.1:6379> ttl key # 查看数据还有多久过期
(integer) 56

5.2 list
#

  • Redis自行实现了自己的链表结构,为双向链表
  • 常用命令:rpush, lpop, lpush, rpop, lrange, llen
  • 应用场景:发布订阅【消息队列】,慢查询

通过rpush/lpop实现队列
#

127.0.0.1:6379> rpush myList value1 # 向 list 的最右边添加元素
(integer) 1
127.0.0.1:6379> rpush myList value2 value3 # 向list的最右边添加多个元素
(integer) 3
127.0.0.1:6379> lpop myList # 将 list的最左边元素取出
"value1"
127.0.0.1:6379> lrange myList 0 1 # 查看对应下标的list列表, 0 为 start,1为 end
1) "value2"
2) "value3"
127.0.0.1:6379> lrange myList 0 -1 # 查看列表中的所有元素,-1表示倒数第一
1) "value2"
2) "value3"
127.0.0.1:6379> llen myList # 查看链表长度
(integer) 2

5.3 hash
#

  • 内部实现为【数组 + 链表】
  • 适合存储对象,可以直接仅仅修改这个对象中某个字段的值。如,存储用户信息、商品信息
  • 常用命令:hset, hmset, hget, hgetall, hkeys, hvals
  • 应用场景:系统中对象数据的存储
127.0.0.1:6379> hmset userInfoKey name "guide" description "dev" age "24"
OK
127.0.0.1:6379> hexists userInfoKey name # 查看 key 对应的 value中指定的字段是否存在。
(integer) 1
127.0.0.1:6379> hget userInfoKey name # 获取存储在哈希表中指定字段的值。
"guide"
127.0.0.1:6379> hget userInfoKey age
"24"
127.0.0.1:6379> hgetall userInfoKey # 获取在哈希表中指定 key 的所有字段和值
1) "name"
2) "guide"
3) "description"
4) "dev"
5) "age"
6) "24"
127.0.0.1:6379> hkeys userInfoKey # 获取 key 列表
1) "name"
2) "description"
3) "age"
127.0.0.1:6379> hvals userInfoKey # 获取 value 列表
1) "guide"
2) "dev"
3) "24"
127.0.0.1:6379> hset userInfoKey name "GuideGeGe" # 修改某个字段对应的值
127.0.0.1:6379> hget userInfoKey name
"GuideGeGe"

5.4 set
#

  • 存储无重复元素
  • 容易实现交集、并集、差集操作。实现如共同关注、共同粉丝、共同喜好等功能
  • 常用命令:sadd, spop, smembers, sismembers, scard, sinterstore, sunion
  • 应用场景:需要存放的数据不能重复、需要获取多个数据源交集和并集等场景
127.0.0.1:6379> sadd mySet value1 value2 # 添加元素进去
(integer) 2
127.0.0.1:6379> sadd mySet value1 # 不允许有重复元素
(integer) 0
127.0.0.1:6379> smembers mySet # 查看 set 中所有的元素
1) "value1"
2) "value2"
127.0.0.1:6379> scard mySet # 查看 set 的长度
(integer) 2
127.0.0.1:6379> sismember mySet value1 # 检查某个元素是否存在set 中,只能接收单个元素
(integer) 1
127.0.0.1:6379> sadd mySet2 value2 value3
(integer) 2
127.0.0.1:6379> sinterstore mySet3 mySet mySet2 # 获取 mySet 和 mySet2 的交集并存放在 mySet3 中
(integer) 1
127.0.0.1:6379> smembers mySet3
1) "value2"

5.5 sorted set
#

  • 介绍:和 set比,增加了一个权重参数 score,使得集合中的元素能够按score进行有序排序,也可通过score的范围来获取元素的列表。
  • 常用命令:zadd, zcard, zscore, zrange, zreverange, zrem
  • 应用场景:需要根据权重对数据进行排序的场景。如,直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息等信息
127.0.0.1:6379> zadd myZset 3.0 value1 # 添加元素到 sorted set 中 3.0 为权重
(integer) 1
127.0.0.1:6379> zadd myZset 2.0 value2 1.0 value3 # 一次添加多个元素
(integer) 2
127.0.0.1:6379> zcard myZset # 查看 sorted set 中的元素数量
(integer) 3
127.0.0.1:6379> zscore myZset value1 # 查看某个 value 的权重
"3"
127.0.0.1:6379> zrange  myZset 0 -1 # 顺序输出某个范围区间的元素,0 -1 表示输出所有元素
1) "value3"
2) "value2"
3) "value1"
127.0.0.1:6379> zrange  myZset 0 1 # 顺序输出某个范围区间的元素,0 为 start  1 为 stop
1) "value3"
2) "value2"
127.0.0.1:6379> zrevrange  myZset 0 1 # 逆序输出某个范围区间的元素,0 为 start  1 为 stop
1) "value1"
2) "value2"

5.6 bitmap
#

  • 介绍:bitmap存储连续二进制数字【0和 1】,一个二进制位对应某个元素的值或者状态,key 对应元素本身。
  • 能极大节省存储空间
  • 常用命令:setbit, getbit, bitcount, bitop
  • 应用场景:适合需要保存状态信息并需要进一步对这些信息进行分析的场景。如,用户签到、活跃用户行为统计【如是否点赞过某个视频】
# SETBIT 会返回之前位的值(默认是 0)这里会生成 7 个位
127.0.0.1:6379> setbit mykey 7 1
(integer) 0
127.0.0.1:6379> setbit mykey 7 0
(integer) 1
127.0.0.1:6379> getbit mykey 7
(integer) 0
127.0.0.1:6379> setbit mykey 6 1
(integer) 0
127.0.0.1:6379> setbit mykey 8 1
(integer) 0
# 通过 bitcount 统计被被设置为 1 的位的数量。
127.0.0.1:6379> bitcount mykey
(integer) 2

5.6.1 使用场景一
#

用户行为分析

# 记录你喜欢过 001 号小姐姐
127.0.0.1:6379> setbit beauty_girl_001 uid 1

5.6.2 使用场景二
#

统计活跃用户

# 对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。
# BITOP 命令支持 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种参数
BITOP operation destkey key [key ...]

使用时间作为 key,然后用户 ID 为 offset,如果当日活跃过就设置为 1

初始化数据:

127.0.0.1:6379> setbit 20210308 1 1
(integer) 0
127.0.0.1:6379> setbit 20210308 2 1
(integer) 0
127.0.0.1:6379> setbit 20210309 1 1
(integer) 0

统计 20210308~20210309 总活跃用户数: 1

127.0.0.1:6379> bitop and desk1 20210308 20210309
(integer) 1
127.0.0.1:6379> bitcount desk1
(integer) 1

统计 20210308~20210309 在线活跃用户数: 2

127.0.0.1:6379> bitop or desk2 20210308 20210309
(integer) 1
127.0.0.1:6379> bitcount desk2
(integer) 2

5.6.3 使用场景三

对于获取或者统计用户在线状态,使用 bitmap 是一个节约空间效率又高的一种方法。

只需要一个 key,然后用户 ID 为 offset,如果在线就设置为 1,不在线就设置为 0。

6. Redis单线程模型详解
#

  • Redis 基于 Reactor 模式来设计开发一套高效的事件处理模型,这套事件处理模型对应的是Redis中的文件事件处理器。
  • 由于文件事件处理器是单线程运行的,所以一般都说Redis是单线程模型
  • Redis通过IO多路复用程序来监听来自客户端的大量连接【监听多个socket】,将特定的事件及类型【读、写】注册到内核中并监听每个事件是否发生
  • IO多路复用技术使得Redis不需要额外创建多余的线程来监听客户端的大量连接,降低资源的消耗
  • Redis服务器是一个事件驱动程序,服务器需要处理两类事件:文件事件时间事件

Redis基于Reactor模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器。该处理器使用IO多路复用程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。

当前被监听的套接字准备好执行连接应答【accept】、读取【read】、写入【read】、关闭【close】等操作时,若与操作相对应的文件事件产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。

文件事件处理器主要包含四部分:

  • 多个socket
  • IO多路复用程序
  • 文件事件分派器
  • 事件处理器

7. Redis 6.0 之前为什么不使用多线程
#

  • 实际上,Redis 在 4.0 之后的版本中就已经加入了对多线程的支持
  • 不过,Redis 4.0 增加的多线程主要是针对一些大键值对的删除操作的命令,使用这些命令就会使用主处理之外的其他线程来“异步处理”
  • 大体上来说,Redis 6.0 之前主要还是单线程处理

Redis 6.0 之前为什么不使用多线程:

  1. 单线程编程容易且易于维护
  2. Redis的性能瓶颈不在CPU,主要在内存和网络
  3. 多线程存在死锁、线程上下文切换等问题,有可能会影响性能

8. Redis 6.0 之后为什么引入多线程
#

  • 为了提高网络IO读写性能,这是Redis的一个性能瓶颈之一
  • Redis的多线程只是在网络数据的读写这一类耗时较多的操作上使用,执行命令时仍然是单线程执行,故也不需要担心线程安全问题

Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis 配置文件 redis.conf

io-threads-do-reads yes

开启多线程后,还需要设置线程数,否则是不生效的。同样需要修改 redis 配置文件 redis.conf :

io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程

9. Redis 过期时间的作用
#

  • 服务器内存有限,若缓存的所有数据都一直保存,最终将导致OOM

  • Redis自带了给缓存数据设置过期时间的功能:

  • 127.0.0.1:6379> exp key  60 # 数据在 60s 后过期
    (integer) 1
    127.0.0.1:6379> setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire)
    OK
    127.0.0.1:6379> ttl key # 查看数据还有多久过期
    (integer) 56
    

* Redis 中除了字符串类型有自己独有设置过期时间的命令 `setex` 外,其他方法都需要依靠 `expire` 命令来设置过期时间 。另外, `persist` 命令可以移除一个键的过期时间

* 某些业务场景需要某个数据只在某一时间段内存在,比如我们的短信验证码可能只在 1 分钟内有效,用户登录的 token 可能只在 1 天内有效

10. Redis 如何判断数据是否过期
#

  • Redis通过一个叫做 过期字典【相当于 hash 表】 的结构来保存数据过期时间。
  • 过期字典的建指向Redis数据库中的某个 key
  • 过期字典的值是一个 long long类型的整数,保存了 key 所指向的数据库建的过期时间【毫秒精度的UNIX时间戳】

过期字典是存储在 redisDb 这个结构里的:

typedef struct redisDb {
    ...

    dict *dict;     //数据库键空间,保存着数据库中所有键值对
    dict *expires   // 过期字典,保存着键的过期时间
    ...
} redisDb;

11. 过期的数据的删除策略
#

常用的过期数据删除从策略:

  • 惰性删除:只会在取出 key 的时候才对数据进行过期检查。对 CPU 友好,但是可能会造成过多的过期数据堆积
  • 定期删除:每隔一段时间抽取一批 key 执行删除过期数据操作。Redis底层会通过限制删除操作执行的时长和频率来减少对 CPU 的影响

定期删除对内存友好,惰性删除对 CPU 友好。Redis 采用定期删除 + 惰性删除

12. 内存淘汰机制
#

采用 定期删除 + 惰性删除 仍有可能导致 OOM。解决策略是:内存淘汰机制

Redis 提供 6 种数据淘汰策略:

  • volatile-lru【least recently used】:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
  • valatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
  • volatile-random:从已设置过期时间的数据集中任意挑选数据淘汰
  • allkeys-lru【least recently used】:当内存不足以容纳新写入数据时,在建空间中,移除最近最少使用的 key
  • allkeys-random:从数据集中任意选择数据淘汰
  • no-eviction:禁止驱逐数据,也就是当前内存不足以容纳新写入数据时,新写入操作会报错。

4.0 版本新增加以下两种:

  1. valatile-lfu【least frequently used】:从已设置过期时间的数据集中挑选最不经常使用的数据淘汰
  2. allkeys-lfu【least frequently used】:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

13. Redis 持久化机制
#

13.1 两种持久化方式
#

快照持久化(RDB)和追加文件持久化(AOF)

Redis支持两种持久化方式:

  • 快照持久化【RDB】
  • 只追加文件【AOF】

13.1.1 快照持久化
#

快照持久化:

  • Redis 通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。
  • 快照创建后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本【Redis主从结构,主要用来提高Redis性能】
  • 可以将快照留在原地以便重启服务器的时候使用。

快照持久化时Redis 默认采用的持久化方式,在Redis.conf配置文件中默认有以下配置:

save 900 1           #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

save 300 10          #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

save 60 10000        #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

13.1.2 AOF持久化
#

AOF【append-only file】持久化

与快照持久化相比,AOF 持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启:

appendonly yes

开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入硬盘中的 AOF 文件。AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof。

在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:

appendfsync always    #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec  #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no        #让操作系统决定何时进行同步

为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec 选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能几乎没受到任何影响。而且这样即使出现系统崩溃, 用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。

13.2 Redis 4.0 对于持久化机制的优化
#

Redis 4.0 开始支持RDB和AOF的混合持久化【默认关闭,可以通过配置项 aof-use-rdb-preamble开启】

混合持久化在 AOF重写的时候就直接把RDB的内容写到AOF文件开头。好处是可以结合RDB和AOF优点,快速加载的同时避免丢失过多的数据。缺点:AOF里的RDB部分是压缩格式而不再是AOF格式,可读性较差。

13.3 AOF重写
#

  • AOF重写可以产生一个新的AOF文件,这个新文件和原有AOF文件所保存的数据库状态一样,但体积更小
  • AOF重写通过读取数据库的键值对来实现,程序无需对现有AOF文件进行任何读入、分析或者写入操作
  • 在执行 BGREWRITEAOF 命令时,Redis服务器会维护一个AOF重写缓冲区,该缓冲区会在子进程创建新的AOF文件期间,记录服务器执行的所有写命令。当紫禁城完成创建新AOF文件的工作之后,服务器会将重写缓冲区所有内容追加到新的AOF文件的末尾,使得新旧的两个AOF文件所保存的数据库状态一致。最后,服务器用新的AOF文件替换旧的AOF文件,以此来完成AOF文件重写操作。

14. Redis事务
#

 Redis可以通过MULTI, EXEC, DISCARD, WATCH等命令来实现事务功能

> MULTI
OK
> SET USER "张三
QUEUED
> GET USER
QUEUED
> EXEC
1) OK
2) "张三"

使用 MULTI命令后可以输入多个命令。Redis不会立即执行这些命令。而是将它放到队列,当调用EXEC命令,将执行队列中所有命令。

过程如下:

  • 开始事务【MULTI】
  • 命令入队【先进先出顺序执行】
  • 执行事务【EXEC】

DISCARD命令可以取消一个事务,将清空该事务对应队列所保存的命令

> MULTI
OK
> SET USER "张三"
QUEUED
> GET USER
QUEUED
> DISCARD
OK

WATCH命令用于监听指定的键,当调用EXEC命令执行事务时,如果发现被监听键被修改时,整个事务将执行失败。

> WATCH USER
OK
> MULTI
> SET USER "张三"
OK
> GET USER
张三
> EXEC
ERR EXEC without MULTI

Redis 不支持回滚,不满足一般事务定义的原子性和持久性

15. 缓存穿透
#

15.1 简介
#

大量请求的 key 不存在缓存中,导致请求全部落在了数据库上。

15.2 解决方案
#

15.2.1 做好参数校验
#

​ 一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。

15.2.2 缓存无效的 key
#

如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下: SET key value EX 10086 。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。

一般情况下我 key 的格式: 表名:列名:主键名:主键值

15.2.3 布隆过滤器
#

15.2.3.1 简介
#
  • 可以方便地判断一个给定的数据是否存在于海量的数据中,即判断 key 是否合法。
  • 具体操作:把所有可能存在的请求值都存放在布隆过滤器,当用户请求过来时,先判断用户发来的请求的值是否存在与布隆过滤器。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会正常执行业务流程。
  • 布隆过滤器认为某元素存在时,有小概率会误判;但是认为某元素不存在时,则该元素一定不存在。
15.2.3.2 布隆过滤器原理
#

当一个元素加入布隆过滤器时:

  1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值【有几个哈希 函数就得到几个哈希值】
  2. 根据哈希值,在位数组中把对应下标的值置为 1

当需要判断一个元素是否存在与布隆过滤器时:

  1. 对给定元素再次进行相同的哈希计算
  2. 得到值后,判断位数组中的每一个元素是否都为 1,如果值都为 1,说明这个值在布隆过滤器,如果存在一个值不为 1,说明该元素不在布隆过滤器中。

若不同的字符串哈希出来的位置相同,则会造成布隆过滤器的误判,但是概率比较小。可以适当增加位数组大小或者调整哈希函数来降低概率

16. 缓存雪崩
#

16.1 简介
#

  • 缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受了大量的请求。
  • 例子:秒杀开始 1 个小时前,统一存放了一批商品到 Redis 中,设置的缓存过期时间也是 1 个小时,秒杀开始时,这些秒杀的商品的访问直接失效。导致相应的请求直接就落到了数据库上,就像雪崩一样可怕。

16.2 解决方案
#

16.2.1 服务器不可用情况
#

  • 采用Redis集群,避免单机出现问题导致整个缓存服务器无法使用
  • 限流,避免同时处理大量的请求

16.2.2 热点缓存失效情况
#

  • 设置不同的失效时间,如随机设置缓存的失效时间
  • 缓存永不失效

17. 如何保证缓存和数据库的数据一致性
#

17.1 数据不一致情况
#

读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。

举一个例子: 1.如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。 2.如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。 因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。

17.2 解决方案
#

17.2.1 采用延时双删策略
#

在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。

伪代码如下:

public void write(String key,Object data){
 redis.delKey(key);
 db.updateData(data);
 Thread.sleep(500);
 redis.delKey(key);
 }

具体的步骤就是:

  • 先删除缓存;
  • 再写数据库;
  • 休眠500毫秒;
  • 再次删除缓存。

那么,这个500毫秒怎么确定的,具体该休眠多久呢?

需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

当然这种策略还要考虑redis和数据库主从同步的耗时。最后的的写数据的休眠时间:则在读数据业务逻辑的耗时基础上,加几百ms即可。比如:休眠1秒。

设置缓存过期时间

从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。

该方案的弊端

结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。

17.2.2 异步更新缓存【基于订阅binlog的同步机制】
#

技术整体思路:

MySQL binlog增量订阅消费+消息队列+增量数据更新到redis

  • 读Redis:热数据基本都在Redis
  • 写MySQL:增删改都是操作MySQL
  • 更新Redis数据:MySQ的数据操作binlog,来更新到Redis

Redis更新

1)数据操作主要分为两大块:

  • 一个是全量(将全部数据一次写入到redis)
  • 一个是增量(实时更新)

这里说的是增量,指的是mysql的update、insert、delate变更数据。

2)读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。

这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。

其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。

这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。

当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis。

认识
#

有序集合和普通集合set很相似,是一个没有重复元素的字符串集合。

每个成员都关联了一个评分(score),这个评分被用来按照最低分到最高分的方式排序集合种的成员。集合的成员是唯一的,但是分数可以重复

底层:
#

Redis提供的一个非常特别的数据结构,一方面它等价于java的数据结构Map<String, double>,可以给每一个元素value赋予一个权重score, 另一方面它又类似于TreeSet,内部的元素会按照权重进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表

实现:

hash;hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到score 跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表

相同分数的时候

有序集合里面的成员是不能重复的都是唯一的,但是,不同成员间有可能有相同的分数。当多个成员有相同的分数时,他们将是有序的字典(ordered lexicographically)(仍由分数作为第一排序条件,然后,相同分数的成员按照字典规则相对排序)。

字典顺序排序用的是二进制,它比较的是字符串的字节数组。

Mysql索引
#

MySQL索引详解
#

1. 什么是索引
#

  • 索引是一种用于快速查询和检索数据的数据结构。常见的索引结构有: B 树, B+树和 Hash

2. 索引优缺点
#

优点:

  • 能极大提高查询效率
  • 创建唯一索引可以保证数据库表中每一行数据的唯一性

缺点:

  • 创建和维护索引比较耗时,当数据表频繁增删改时,对应的索引也要动态改变,会降低SQL执行效率
  • 索引需要使用物理文件存储,会耗费一定存储空间

大多数情况下,索引查询都比全表扫描快。但是数据库数据量不大的话,使用索引不一定能提升查询效率

3. 索引底层数据结构
#

3.1 MySQL为何不使用Hash作为索引
#

  • Hash存在哈希冲突
  • Hash索引不支持顺序查找和范围查找

3.2 B树 和 B+树
#

  • B 树的所有节点既存放键(key) 也存放 数据(data),而 B+树只有叶子节点存放 key 和 data,其他内节点只存放 key。
  • B 树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点
  • B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显
  • 在 MySQL 中,MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是,两者的实现方式不太一样。
  • MyISAM 引擎中,B+Tree 叶节点的 data 域存放的是数据记录的地址。在索引检索的时候,首先按照 B+Tree 搜索算法搜索索引,如果指定的 Key 存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”。
  • InnoDB 引擎中,其数据文件本身就是索引文件。相比 MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按 B+Tree 组织的一个索引结构,树的叶节点 data 域保存了完整的数据记录。这个索引的 key 是数据表的主键,因此 InnoDB 表数据文件本身就是主索引。这被称为“聚簇索引(或聚集索引)”,而其余的索引都作为辅助索引,辅助索引的 data 域存储相应记录主键的值而不是地址,这也是和 MyISAM 不同的地方。在根据主索引搜索时,直接找到 key 所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,在走一遍主索引【回表】。 因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂

4. 索引类型
#

4.1 主键索引
#

  • 数据表的主键列使用的就是主键索引
  • 一张数据表有且只能有一个主键,并且主键不能为 null,不能重复
  • 在 MySQL 的 InnoDB 的表中,当没有显示的指定表的主键时,InnoDB 会自动先检查表中是否有唯一索引的字段,如果有,则选择该字段为默认的主键,否则 InnoDB 将会自动创建一个 6Byte 的自增主键。

4.2 辅助索引
#

  • 二级索引又称为辅助索引,是因为二级索引的叶子节点存储的数据是主键。也就是说,通过二级索引,可以定位主键的位置。
  • 唯一索引,普通索引,前缀索引等索引属于二级索引 > * 唯一索引(Unique Key) :唯一索引也是一种约束。唯一索引的属性列不能出现重复的数据,但是允许数据为 NULL,一张表允许创建多个唯一索引。 建立唯一索引的目的大部分时候都是为了该属性列的数据的唯一性,而不是为了查询效率
    • 普通索引(Index)普通索引的唯一作用就是为了快速查询数据,一张表允许创建多个普通索引,并允许数据重复和 NULL
    • 前缀索引(Prefix) :前缀索引只适用于字符串类型的数据。前缀索引是对文本的前几个字符创建索引,相比普通索引建立的数据更小, 因为只取前几个字符
    • 全文索引(Full Text) :全文索引主要是为了检索大文本数据中的关键字的信息,是目前搜索引擎数据库使用的一种技术。Mysql5.6 之前只有 MYISAM 引擎支持全文索引,5.6 之后 InnoDB 也支持了全文索引

5. 聚集索引和非聚集索引
#

5.1 聚集索引
#

  • 索引结构和数据一起存放的索引。主键索引属于聚集索引
  • 在 Mysql 中,InnoDB 引擎的表的 .ibd文件就包含了该表的索引和数据,对于 InnoDB 引擎表来说,该表的索引(B+树)的每个非叶子节点存储索引,叶子节点存储索引和索引对应的数据
  • 优点:聚集索引的查询速度非常的快,因为整个 B+树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。
  • 缺点: > * 依赖于有序的数据 ,因为 B+树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,整型数据还好,若类似于字符串或 UUID 这种又长又难比较的数据,会造成主索引频繁分裂,插入或查找的速度肯定比较慢
    • 更新代价大,如果对索引列的数据被修改时,那么对应的索引也将会被修改, 而且聚集索引的叶子节点还存放着数据,修改代价肯定是较大的, 所以对于主键索引来说,主键一般都是不可被修改的

5.2 非聚集索引
#

  • 即索引结构和数据分开存放的索引
  • 二级索引属于非聚集索引
  • MYISAM 引擎的表的.MYI 文件包含了表的索引, 该表的索引(B+树)的每个叶子非叶子节点存储索引, 叶子节点存储索引和索引对应数据的指针,指向.MYD 文件的数据。
  • 非聚集索引的叶子节点并不一定存放数据的指针, 因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。
  • 优点:更新代价比聚集索引要小 。非聚集索引的更新代价就没有聚集索引那么大了,非聚集索引的叶子节点是不存放数据的
  • 缺点: > * 非聚集索引也依赖于有序的数据
    • 可能会二次查询(回表) : 当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。

6. 覆盖索引
#

  • 一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为“覆盖索引
  • 在 InnoDB 存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次。这样就会比较慢
  • 覆盖索引就是把要查询出的列和索引是对应的,不做回表操作

7. 创建索引注意事项
#

  • 选择合适的字段创建索引: > * 不为 NULL 的字段 :索引字段的数据应该尽量不为 NULL,因为对于数据为 NULL 的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为 NULL,建议使用 0,1,true,false 这样语义较为清晰的短值或短字符作为替代。
    • 被频繁查询的字段 :我们创建索引的字段应该是查询操作非常频繁的字段。
    • 被作为条件查询的字段 :被作为 WHERE 条件查询的字段,应该被考虑建立索引。
    • 频繁需要排序的字段 :索引已经排序,这样查询可以利用索引的排序,加快排序查询时间。
    • 被经常频繁用于连接的字段 :经常用于连接的字段可能是一些外键列,对于外键列并不一定要建立外键,只是说该列涉及到表与表的关系。对于频繁被连接查询的字段,可以考虑建立索引,提高多表连接查询的效率。
  • 被频繁更新的字段应该慎重建立索引
  • 尽可能的考虑建立联合索引而不是单列索引。因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗 B+树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升
  • 注意避免冗余索引。冗余索引指的是索引的功能相同,能够命中索引(a, b)就肯定能命中索引(a) ,那么索引(a)就是冗余索引。如(name,city )和(name )这两个索引就是冗余索引,能够命中前者的查询肯定是能够命中后者的 在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引
  • 考虑在字符串类型的字段上使用前缀索引代替普通索引。前缀索引仅限于字符串类型,较普通索引会占用更小的空间,所以可以考虑使用前缀索引带替普通索引

8. 使用索引的注意事项
#

  • 对于中到大型表索引都是非常有效的,但是特大型表的话维护开销会很大,不适合建索引
  • 避免 where 子句中对字段施加函数,这会造成无法命中索引
  • 在使用 InnoDB 时使用与业务无关的自增主键作为主键,即使用逻辑主键,而不要使用业务主键
  • 删除长期未使用的索引,不用的索引的存在会造成不必要的性能损耗 MySQL 5.7 可以通过查询 sys 库的 schema_unused_indexes 视图来查询哪些索引从未被使用
  • 在使用 limit offset 查询缓慢时,可以借助索引来提高性能

9. MySQL如何为表字段添加索引
#

  1. 添加主键索引
alter table table_name add primary key(column)
  1. 添加唯一索引
alter table table_name add unique(column)
  1. 添加普通索引
alter table table_name add index index_name(column)
  1. 添加全部索引
alter table table_name add fulltext(column)
  1. 添加多列索引
alter table table_name add index index_name(column1, column2, column3)

10. 最左前缀匹配
#

  • 在MySQL建立联合索引时会遵守最左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配。
  • MySQL创建联合索引的规则是首先会对联合索引的最左边第一个字段排序,在第一个字段的排序基础上,然后在对第二个字段进行排序。

相关文章

Mac-Dock
·1 分钟
笔记
爬虫浅记
·2 分钟
笔记
本站评论
·1 分钟
笔记