0%

Redis

bing

redis 是速度非常快的开源 nosql 键值型数据库,可以存储 String 键和5种不同类型的值之间的映射。redis 内置了复制、LUA脚本、LUA驱动事件、事务、数据持久化,并通过 redis 哨兵(Sentinel)和redis 分区来保证高可用性

数据类型

Redis内部使用一个redisObject对象来表示所有的 key 和 value

pic

  • string:内部存储结构,包含字符数组的结构体

    1
    2
    3
    4
    5
    struct sdshdr {
    int len; //len表示buf中存储的字符串的长度
    int free; //free表示buf中空闲空间的长度
    char buf[]; //buf用于存储字符串内容
    };

    字符串类型的值能存储的最大容量是 512M

  • list

    String 类型的双向链表

  • set

    1
    SADD key value1 [value2 value3...] // 添加数据

    底层使用 hashMap 的 key 来存储 set 中不重复的数据;hashMap 使用拉链法解决哈希冲突

  • hash

    • 当HashMap的成员比较少时,redis为了节省内存会采用类似一维数组的方式来紧凑存储,对应 value 的redisObject的encoding为zipmap
    • 当成员数量增大时,会自动转成真正的HashMap,此时encoding为ht
  • zset

    1
    2
    3
    ZADD key score1 value1 [score2 value2...] // 添加数据
    ZCARD key // 获取成员数
    ZCOUNT key min max // 指定区间分数的成员数

    底层数据结构使用 hashMap 和跳跃表实现,hashMap 键存储成员,值存储 score,跳跃表中存放成员数据

底层数据结构

字典(hashMap)

  • Redis 的字典 dict 中包含两个哈希表 dictht,这是为了方便进行 rehash 操作
  • 在扩容时,将其中一个 dictht 上的键值对 rehash 到另一个 dictht 上面,完成之后释放空间并交换两个 dictht 的角色
  • 采用渐进式 rehash,避免一次性执行过多的 rehash 操作给服务器带来过大的负担
  • 在 rehash 期间,每次对字典执行添加、删除、查找或者更新操作时,都会执行一次渐进式 rehash

跳跃表

pic

Redis 内存

理解Redis的内存

Sql & NoSql

  • Sql 是关系型数据库;NoSql 是非关系型数据库
  • Sql 数据存在特定结构的表中;NoSql 的数据存储比较灵活并支持动态数据,存储格式包括文档和键值对等
  • Sql 必须定义表才能添加数据;Nosql 不需要预先定义表
  • Sql 支持连表查询;Nosql 除 MongoDB 外暂时不支持连表查询
  • Sql 提供了丰富的查询语句;Nosql 只有 MongoDB 提供近似于 Sql 的丰富的查询操作

Redis & Memcached

Redis 和 Memcached 都是 nosql 键值型数据库

  • Redis 的值支持5种数据类型,Memcached 只支持字符串

  • Redis 使用单线程实现,Memcached 使用多线程实现

  • Redis 支持数据持久化,Memcached 不支持

    RDB 和 AOF 持久化的原理

    • RDB 持久化:将某个时间点的所有数据存入磁盘中
    • AOF 持久化:将写命令添加到 AOF 文件(Append Only File)的末尾
  • Redis 支持分布式,Memcached 不支持分布式

    • Redis 支持分区, Redis Cluster 实现了分布式的支持,支持主从复制
    • Memcached 使用客户端一致性哈希实现分布式
  • Redis 提供了丰富的淘汰策略,Memcached 的数据淘汰策略是基于 LRU 的

  • 在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘;Memcached 的数据则会一直在内存中

Redis & MongoDB

  • redis 是 nosql 键值型数据库,MongoDB 是 nosql 文档型数据库
  • redis 的数据存储格式是键值对,MongoDB 的数据存储格式是 json 格式
  • redis 的编写语言是 C,MongoDB 的编写语言是C++
  • redis 主要存储少量并发性要求高的数据,MongoDB 主要存储海量读写要求较高的数据
  • redis 支持事务,MongoDB 不支持事务
  • redis 数据结构简单高效,MongoDB支持索引并支持丰富的查询操作
  • redis 不支持数据分析,MongoDB 内置数据性能分析功能
  • redis 主要应用于较小数据量的高并发场景,MongoDB 主要应用于海量数据的访问效率提升

redis 为什么快

  • redis 是内存型数据库,不需要进行磁盘 IO 读写

  • redis 数据结构简单,并进行专门的设计,比如跳跃表插入简单,不需要旋转操作

  • redis 是单线程的,避免了线程切换的开销以及加锁释放锁的操作

    • 单线程指使用一个线程处理网路请求
  • redis 使用多路 IO 复用,非阻塞 IO

    同步IO、异步IO、阻塞IO、非阻塞IO之间的联系与区别

    • 多路 IO 复用:利用 select、poll、epoll 可以同时监测多个连接的 I/O 事件。空闲时,redis 线程阻塞;当一个连接或多个连接有 IO 事件时,redis 去轮询发生 IO 操作的连接并依次处理
    • 非阻塞 IO:当没有数据或数据未准备好时会一直阻塞等待数据叫做阻塞 IO;当没有数据或数据未准备好时不会等待数据而可以做其他的事叫做非阻塞 IO

使用场景

计数器

1
2
3
4
incr key // +1
decr key // -1
incrby key value // +value
decrby key value // -value

缓存

将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率

同为分布式缓存,为何Redis更胜一筹

缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级等问题

pic

二八定律

网站访问数据的特点大多呈现二八定律:80% 的业务访问集中在20% 的数据上

热数据和冷数据

  • 热数据:频繁访问的数据,对服务器要求较高,通常存放在性能较强的服务器中
  • 冷数据:不经常访问的数据,通常存放在性能较低的服务器中

缓存一般流程

  • 用户请求数据
  • 进行缓存查询,如果存在有效缓存返回缓存数据
  • 如果缓存不存在或已过期,再进行数据库查询
  • 数据库返回查询结果并把查询结果写入缓存,数据不存在不存入缓存

缓存穿透

缓存穿透是指查询数据库一定不存在的数据,缓存不命中会查询数据库,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透

解决方案:

缓存击穿

对特定值进行高并发查询,当缓存过期时,会有大量查询集中在数据库上,造成缓存击穿

解决方案:

和缓存雪崩类似

缓存雪崩

缓存同一时刻失效或集中在一段时间内失效,发生大量缓存穿透,所有查询都集中在数据库上,造成缓存雪崩

解决方案:

  • 保证缓存服务高可用:redis 哨兵、redis 集群

  • 分散缓存时间:比如在原有缓存时间基础上加入随机值

  • 限流

    • 滑动窗口
    • 令牌桶算法
    • 漏桶算法
  • 队列:保证不会有大量线程对数据库进行一次性读写

  • 加锁排队:应用在并发量不高的场景中,很少使用

  • 资源熔断

  • 降级

  • 二级缓存

缓存预热

通过缓存 reload 机制,预先去加载或更新缓存。再即将发生高并发访问前手动触发加载缓存不同的 key,设置不同的过期时间,让缓存失效的时间点尽量均匀

实现:

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

缓存更新

redis 提供 6 种缓存淘汰策略:

  • volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
  • volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
  • volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
  • allkeys-lru:从所有数据集中挑选最近最少使用的数据淘汰
  • allkeys-random:从所有数据集中任意选择数据进行淘汰
  • noeviction:禁止驱逐数据

redis 4.0 中引入:

  • volatile-lfu:从已设置过期时间的数据集中挑选访问频率最少的数据淘汰
  • allkeys-lfu:从所有数据集中挑选访问频率最少的数据淘汰

定期删除+惰性删除:

  • 定期清理过期的缓存:redis 默认每隔 100ms 检查是否有过期的 key,redis 并不是检查所有的 key 是否过期,而是随机抽取 key 并检查是否存在过期的 key
  • 接受用户请求后判断缓存是否过期:当用户获取 key 时,redis 会检查是否过期,如果过期会删除;不过期就返回给用户

写缓存更新:

实现缓存最终一致性的两种方案

数据库和缓存一致性解析

  • 同时写入缓存和数据库

    pic

  • 缓存和数据库解耦

    pic

降级

服务降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务正常运行。降级往往会指定不同的级别,面临不同的异常等级执行不同的处理。根据服务方式:可以拒接服务,可以延迟服务,也可以随机服务

限流

  • 计数器
    • 使用计数器限制一秒内能够通过的请求数,到达阈值后拒绝其他请求,一秒结束后再重新计数
    • 缺点:如果请求集中在前几毫秒,之后将无法响应请求,这种现象称为“突刺现象”
  • 漏桶算法
    • 类比:相当于水倒入漏斗,不管倒入多少水,下边流出的速度始终保持不变
    • 可以维护一个消息队列,将请求放入消息队列,然后通过一个线程池定期的从消息队列中读取请求。当桶达到最大容量时,拒绝请求
    • 缺点:无法处理突发流量并响应时间要求比较短的场景
  • 令牌桶算法
    • 令牌桶能限制平均请求速度的同时允许一定程度的突发请求
    • 使用一个桶存放固定数量的令牌,算法以一定的速率向桶中存放令牌,存放令牌持续不断的进行,当桶满时丢弃令牌。每次请求需要先获取令牌,拿到令牌才能执行,否则等待或拒绝请求
    • 令牌桶算法运行一定程度的突发请求,比如桶初始化时允许存放100个令牌,那么这时运行100个请求同时调用

查找表

查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源

消息队列

List 是一个双向链表,可以通过 lpushrpop 实现消息队列,但还是推荐使用 Kafka、RabbitMQ 等消息中间件

会话缓存

会话跟踪技术

HTTP 是无状态协议,必须使用会话跟踪技术来为不同的用户提供不同的服务

  • cookie

    cookie 存储在客户端,既可以在客户端生成,也可以在服务端生成;不可跨域名

    pic

    若客户端不支持 cookie , 可以使用 URL 重写技术进行会话跟踪,HttpServletResponseencodeURL(String url) 可以对不支持 cookie 的浏览器,通过把 session id 加入 URL 的方式进行会话跟踪

  • session

    创建 session 的同时会生成唯一的 session id,session 被创建后可以在 session 中增加键值对,只有 session id 会发送到客户端,客户再次发送请求时,将 session id 一同发往服务器

    session 存储在服务端,每个用户都会有 session,当大量用户访问服务器时,内存开销会不断增大;可扩展性不强,集群需要进行 session 复制或使用特定服务器存储 session 来实现 session 共享

  • token

    token 存储在客户端

    pic

会话共享

session 共享技术能够解决负载均衡中不同服务器节点对应不同 session 的问题,可以将 session 存储到 redis 的 hash 数据类型中

实现方式:

  • 修改配置文件
  • 重写读写 session 的代码
  • 使用 springspring-session.jar

分布式锁

在单机场景下,可以使用语言内置的锁来实现多进程 / 线程的同步问题,但是在分布式情况下,语言内置锁无法使用,必须使用分布式锁来保证进程 / 线程同步

对于操作系统而言,锁可以使用互斥量来实现,互斥量为 0 处于锁定状态,互斥量为 1 处于未锁定状态;分布式锁同样可以借鉴这种思想,将锁存储于数据库中,获取锁插入一条记录,释放锁删除一条记录

  • 数据库乐观锁
  • redis 分布式锁
  • ZooKeeper 分布式锁

可靠性

  • 互斥性:任意时刻只有一个客户端能持有锁
  • 不会发生死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁
  • 具有容错性:只要大部分的Redis节点正常运行,客户端就可以加锁和解锁
  • 解铃还须系铃人:加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了

setnx 指令

Redis分布式锁的正确实现方式

1
2
3
4
5
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class RedisTool {

// 加锁
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX"; // 保证互斥性
private static final String SET_WITH_EXPIRE_TIME = "PX"; // 保证不会发生死锁

// 解锁
private static final Long RELEASE_SUCCESS = 1L;

/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁,唯一key
* @param requestId 请求标识,保证解铃还须系铃人
* @param expireTime 超期时间,保证不会发生死锁
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}

/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); // jedis.eval() 保证执行的原子性
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}

redLock 算法

RedRock:Redis分布式锁最牛逼的实现

使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时仍然可用

假设在分布式场景下有N个 redis master,步骤如下:

  • 尝试从 N 个相互独立 Redis 实例获取锁
  • 计算获取锁消耗的时间,只有当这个时间小于锁的过期时间,并且从大多数(N / 2 + 1)实例上获取了锁,那么就认为锁获取成功了
  • 如果锁获取失败,就到每个实例上释放锁

实现:

1
2
3
4
5
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.3.2</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://192.168.0.1:5378")
.setPassword("a123456").setDatabase(0);
RedissonClient redissonClient1 = Redisson.create(config1);

Config config2 = new Config();
config2.useSingleServer().setAddress("redis://192.168.0.1:5379")
.setPassword("a123456").setDatabase(0);
RedissonClient redissonClient2 = Redisson.create(config2);

Config config3 = new Config();
config3.useSingleServer().setAddress("redis://192.168.0.1:5380")
.setPassword("a123456").setDatabase(0);
RedissonClient redissonClient3 = Redisson.create(config3);

String resourceName = "REDLOCK_KEY";

RLock lock1 = redissonClient1.getLock(resourceName);
RLock lock2 = redissonClient2.getLock(resourceName);
RLock lock3 = redissonClient3.getLock(resourceName);
// 向3个redis实例尝试加锁
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
boolean isLock;
try {
// isLock = redLock.tryLock();
// 500ms拿不到锁, 就认为获取锁失败。10000ms即10s是锁失效时间。
isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
System.out.println("isLock = "+isLock);
if (isLock) {
//TODO if get lock success, do something;
}
} catch (Exception e) {
} finally {
// 无论如何, 最后都要解锁
redLock.unlock();
}

复制

主从复制

redis 不支持主主复制,redis 的主从复制是异步的

全量复制:

  • 从节点设置主节点 slaveof
  • 从节点内部的定时任务发现有主节点信息,开始建立 Socket 连接
  • 建立连接后,发起 ping 命令,希望得到 pong 响应,否则进行重连
  • 如果主节点设置了权限,进行权限校验,校验失败复制终止
  • 主服务器创建 RDB 数据快照发送给从服务器,并将发送期间到达的写命令存储到缓冲区中,发送完 RDB 文件开始发送缓冲区的写命令
  • 从服务器丢弃所有旧数据,载入主服务器发来的 RDB 快照文件,之后从服务器开始接受主服务器发来的写命令
  • 主服务器每执行一次写命令,就向从服务器发送相同的写命令

部分复制:

如果在主从复制期间,从节点宕机后恢复,不会进行全量复制,而是进行部分复制。RDB 数据之间的同步非常耗时。所以,Redis 在 2.8 版本退出了类似增量复制的 psync 命令,当 Redis 主从直接发生了网络中断,不会进行全量复制,而是将数据放到缓冲区(默认 1MB)里,在通过主从之间各自维护复制 offset 来判断缓存区的数据是否溢出,如果没有溢出,只需要发送缓冲区数据即可,成本很小,反之,则要进行全量复制,因此,控制缓冲区大小非常的重要

  • 当从节点出现网络中断,超过了 repl-timeout 时间,主节点就会中断复制连接
  • 主节点会将请求的数据写入到“复制积压缓冲区”,默认 1MB
  • 当从节点恢复,重新连接上主节点,从节点会将 offset 和主节点 id 发送到主节点
  • 主节点校验后,如果偏移量的数后的数据在缓冲区中,就发送 cuntinue 响应 —— 表示可以进行部分复制
  • 主节点将缓冲区的数据发送到从节点,保证主从复制进行正常状态

主从链

随着负载不断上升,主服务器可能无法很快地更新所有从服务器,或者重新连接和重新同步从服务器将导致系统超载。为了解决这个问题,可以创建一个中间层来分担主服务器的复制工作。中间层的服务器是最上层服务器的从服务器,又是最下层服务器的主服务器

pic

redis 分片

  • 范围分片
  • 哈希分片

根据执行分片的位置,可以分为三种分片方式:

  • 客户端分片:客户端使用一致性哈希等算法决定键应当分布到哪个节点
  • 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上
  • 服务器分片:Redis Cluster

配置主从复制:

1
2
slaveof 127.0.0.1 6379
masterauth <master-password>

redis 集群

Redis进阶实践之十一 Redis的Cluster集群搭建

深入浅出Redis-redis哨兵集群

Redis集群配置

Redis集群伸缩性-扩容&删除

能力:

  • 将数据自动切分到多个节点
  • 当集群子节点发生故障,其余节点正常提供服务

要求:

  • 开启 redis 客户端服务端口:6379
  • 开启集群总线端口:16379

分片:

  • redis 集群中有16384个散列槽,redis 集群中的每个节点负责散列槽的一个子集
  • 当添加 redis 节点时,只需要把其他节点的散列槽移动到该节点
  • 删除 redis 节点时,只需要把当前节点的散列槽交由其他节点管理

redis 集群一致性保证:redis 集群无法保证数据强一致性,主要原因是主从复制是异步复制

redis 哨兵

Sentinel(哨兵)可以监听集群中的服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器

redis 哨兵

配置哨兵

1
2
port 26379
sentinel monitor mymaster 127.0.0.1 6379 2

启动哨兵

1
redis-sentinel sentinel.conf

事件

pic

  • redis 基于 Reactor 模式开发网络事件处理器
    • Reactor 模式:当请求抵达后,服务处理程序使用I/O多路复用策略,然后同步地派发这些请求至相关的请求处理程序
  • 文件事件:redis 使用 IO 多路复用模型处理网络请求,并将这些请求同步的传送给文件事件分配器,分配器根据事件描述符调用相应的事件处理器
  • 时间事件:redis 将时间事件存储在无序链表中,通过遍历链表的方式去查找已经到达的时间事件,并交给相应的事件处理器