数据淘汰策略
redis数据存储在内存中,对于内存的使用的大小是配置的固定的值(maxmemory <bytes>),当存储的数据达到阈值的时候,需要对一些数据进行淘汰(即从内存中剔除),保证内存可用。默认是无策略(noeviction)。
- noeviction
maxmemory-policy noeviction
无策略,不会进行删除操作,如果内存溢出则报错返回。
- volatile-lru(Least Recently Used)
设置过期时间的数据中,剔除最不常用的数据。
- allkeys-lru
从全部数据中(包括未设置过期时间的数据),根据时间删除最不常用的数据。
- volatile-lfu(Least Frequently Used)
从配置了过期的时间的键中淘汰使用频率最少的数据。
- allkeys-lfu
从全部数据中(包括未设置过期时间的数据),删除使用频率最少的数据。
- volatile-random
设置了过期时间的数据中随机删除。
- volatile-ttl
查询全部设置过期时间的数据排序,将马上要过期的数据进行删除操作。
类型扩展
pipeline
需要命令之间没有关联关系,快速提高操作性能(减少网络消耗)。
但pipeline的命令数量需要控制。
1. pipeline不是原子性操作,mset是原子性操作;
2. 有些数据结构没有一次发送多个执行命令的功能,如zset、hash不能一次添加多个key;
示例
@Test
public void test_basic_batch(){
Long start = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
jedis.set("basic:key" + i, "value");
}
Long end = System.currentTimeMillis();
// 1000 -> 49652ms
System.out.println("耗费时间:" + (end - start));
}
@Test
public void test_pipeline(){
Long start = System.currentTimeMillis();
Pipeline pipeline = jedis.pipelined();
for (int i = 0; i < 1000; i++) {
pipeline.set("pipeline:key" + i, "value");
}
pipeline.sync();
Long end = System.currentTimeMillis();
// 1000 -> 131ms
System.out.println("耗费时间:" + (end - start));
}
发布订阅
角色:publisher(发布者)、subscriber(订阅者)、channel(频道)。
示例
@Test
public void test_publisher(){
for (int i = 0; i < 100; i++) {
jedis.publish("1001", i + "");
}
}
@Test
public void test_subscriber(){
JedisPubSub jedisPubSub = new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
super.onMessage(channel, message);
System.out.println("channel: " + channel + "; message: " + message);
}
};
jedis.subscribe(jedisPubSub, "1001");
}
Bitmap
即Bitset,是一串连续的二进制数字,本质是字符串,每一位所在的位置为偏移(offset),在bitmap上可执行AND、OR、XOR以及其他位操作,可以表示2^32个位,大概46亿位,redis单个key最大是512M,所以使用bitmap存储40多亿的数也就占500多M而已,非常节约空间。
使用场景:
实现记录用户哪天进行了登录,每天只记录是否登录过,重复状态算已登录,不需要记录操作行为等详细数据。
示例:
@Test
public void init_data(){
jedis.setbit("user:login:20210101", 1, "1");
jedis.setbit("user:login:20210101", 3, "1");
jedis.setbit("user:login:20210101", 5, "1");
jedis.setbit("user:login:20210101", 8, "1");
jedis.setbit("user:login:20210102", 11, "1");
jedis.setbit("user:login:20210102", 3, "1");
jedis.setbit("user:login:20210102", 5, "1");
jedis.setbit("user:login:20210102", 18, "1");
jedis.setbit("user:login:20210103", 1, "1");
jedis.setbit("user:login:20210103", 3, "1");
jedis.setbit("user:login:20210103", 5, "1");
jedis.setbit("user:login:20210103", 20, "1");
}
@Test
public void count(){
// 20210101 有多少用户登录
System.out.println(jedis.bitcount("user:login:20210101")); // 4
// 最近三天 20210101 20210102 20210103 有多少用户登录
jedis.bitop(BitOP.OR, "user:login:31", "user:login:20210101", "user:login:20210102", "user:login:20210103");
System.out.println(jedis.bitcount("user:login:31")); // 7
// 统计连续登录三天的用户
jedis.bitop(BitOP.AND, "user:login:32", "user:login:20210101", "user:login:20210102", "user:login:20210103");
System.out.println(jedis.bitcount("user:login:32")); // 2
System.out.println(jedis.bitpos("user:login:32", true));
}
查看占用大小
>debug object set:data
"Value at:0x7f3ba8296f90 refcount:1 encoding:hashtable serializedlength:29874 lru:2664839 lru_seconds_idle:12"
>debug object bitmap:data
"Value at:0x7f3ba8296fb0 refcount:1 encoding:raw serializedlength:25 lru:2664857 lru_seconds_idle:6"
Hyperloglog
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog的优点是在输入元素的数量或体积非常大时,计算基数所需的空间总是固定的、并且是很小的;在Redis里面,每个HyperLogLog键只需要花费12KB内存,就可以计算接近2^64(约42亿)个不同元素的基数,这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
特点:
具有一定的错误率(0.81%);
无法获取单条数据;
使用很小的内存;
类型是字符串;
示例:
@Test
public void test_hyperloglog(){
Pipeline pipeline = jedis.pipelined();
for (int i = 0; i < 100000; i++) {
pipeline.pfadd("loglog:data", i + "");
}
pipeline.sync();
System.out.println(jedis.pfcount("loglog:data")); // 99565
}
约12k
>debug object loglog:data
"Value at:0x7f3ba8371030 refcount:1 encoding:raw serializedlength:10550 lru:2665502 lru_seconds_idle:21"
GEO
存储地理位置空间,类型是zset。
常用命令
将指定的地理位置空间(经度、维度、名称)保存
> genadd key longitude latitude member [longitude latitude member ...]
使用场景
地图的经纬度查询
https://lbs.amap.com/tools/picker
public class GEOPosition {
// 经纬度等信息的封装
/**
* 经度
*/
private Double longitude;
/**
* 纬度
*/
private Double latitude;
/**
* 名称
*/
private String name;
}
public class GeoTest {
private Jedis jedis;
/**
* 计算附近10公里的酒店
* 需要维护所有酒店信息(经纬度)
*/
@Test
public void init_geo(){
GEOPosition geoPosition1 = new GEOPosition(121.477939,31.229639, "a");
GEOPosition geoPosition2 = new GEOPosition(121.481672,31.23169, "b");
GEOPosition geoPosition3 = new GEOPosition(121.481447,31.229846, "b");
GEOPosition geoPosition4 = new GEOPosition(121.484623,31.232061, "d");
jedis.geoadd("hotel", geoPosition1.getLongitude(), geoPosition1.getLatitude(), geoPosition1.getName());
jedis.geoadd("hotel", geoPosition2.getLongitude(), geoPosition2.getLatitude(), geoPosition2.getName());
jedis.geoadd("hotel", geoPosition3.getLongitude(), geoPosition3.getLatitude(), geoPosition3.getName());
jedis.geoadd("hotel", geoPosition4.getLongitude(), geoPosition4.getLatitude(), geoPosition4.getName());
}
@Test
public void test_geo(){
// 统计x范围内的酒店
List<GeoRadiusResponse> res = jedis.georadius("hotel", 121.474945,31.228777, 3, GeoUnit.KM);
for (GeoRadiusResponse re : res) {
System.out.println(new String(re.getMember())); // a b d
}
// 查看距离信息
System.out.println(jedis.geodist("hotel", "a", "b", GeoUnit.KM)); // 334.4739
}
}
持久化
redis所有的数据都是保存在内存当中,对数据的更新将异步或同步的保存到磁盘上。
作用
- 保证数据的不丢失,重启服务redis内存当中的数据会清空;
- 数据备份,可以完成数据的指定时间恢复;
持久化的方式
- 快照(RDB)
将内存中的数据拷贝一份,以二进制格式存储RDB文件在磁盘中;重启服务后将该RDB文件装载到内存中。
主动触发方式:
1. save(同步):如果存在老的rdb文件,新的文件会替换老的文件;
2. bgsave(异步):已使用异步处理,使用linux的fork函数;
3. 自动配置(redis.conf中配置策略):会触发bgsave生成文件
# save {seconds} {keycount}
# 900秒内有一个key改变
save 900 1
# 200秒内有10个key改变
save 300 10
# 60秒内有10000个key改变
save 60 10000
自动触发机制:
1. 全量复制;
2. debug reload;
3. shutdown;
相关配置:
# 备份文件
dbfilename dump.rdb
# 备份目录
dir path/data
# 在出现错误时终止rdb备份
stop-writes-on-bgsave-error yes
# 是否进行压缩
rdbcompression yes
# 是否进行检查sum值校验
rdbchecksum yes
缺点:
耗时,耗性能(全量备份,消耗IO);不可控,可能存在丢失数据情况(如进程被kill掉)。
- 写日志(AOF)
保存的是客户端执行的命令,存储为AOF文件;装载的过程就是读取AOF文件去执行命令。它是增量备份的。
AOF的三种策略:
1. always:写命令刷新缓冲区 -> 每条命令fsync到磁盘 -> AOF文件;不丢失数据,IO开销大,一般sata盘只有几百TPS。
2. everysec:写命令刷新缓冲区 -> 每秒把缓冲区fsync到磁盘 -> AOF文件;每秒一次fsync,丢失1秒数据。
3. no:写命令刷新缓冲区 -> 操作系统决定,缓冲区fsync到磁盘 -> AOF文件;不可控。
AOF重写:
redis在AOF操作时,会把相关的命令进行优化、重写,减少命令的次数。
相关配置:
# 开启AOF
appendonly no
# 备份文件
appendfilename "appendonly.aof"
# AOF策略
appendfsync everysec
# 重写的时候是否执行AOF操作
no-appendfsync-on-rewrite no
# 自动重写的百分比
auto-aof-rewrite-percentage 100
# 重写的大小
auto-aof-rewrite-min-size 64mb
- 比较
RDB最佳策略:
1. 在实际工作中,建议关闭RDB,同时开启主从;
2. 集中管理:按天,按小时备份数据,不要使用自动配置的策略;
3. 主从配置,从节点打开;
AOF最佳策略:
1. 开缓存和存储,大部分情况都打开;
2. AOF重写集中管理;
3. everysec策略,通过每秒刷新备份;
4. 在重写的时候结合RDB备份进行快照;
混合持久化内容的AOF文件:
# 开启AOF
appendonly yes
# 开启混合模式
aof-use-rdb-preamble yes
高可用
单机环境可能出现单点故障、吞吐量有限和存储容量不够的情况。
主从复制
一个maser可以有多个slave;
一个slave只能有一个master;
数据流向是单向的,只能从master流向slave。
实现原理
1. 建立连接阶段
1.1 保存主节点信息
slave节点保存master节点的ip和port信息,即masterhost和masterport字段;
slaveof是异步命令,slave保存了master的节点信息后,该命令直接返回ok,实际的复制从这之后开始。
1.2 建立socket连接
slave节点每秒1次调用复制定时函数replicationCron(),发现有master节点,则根据ip和port创建socket连接。
slave节点:为该socket创建一个专门处理复制工作的文件事件处理器,负责后续的事件处理工作,如接收RDB文件、接收文件传播等。
master节点:创建socket对应的客户端状态,将slave节点看作是连接到master节点的一个客户端,后续操作有slave节点向master节点发送命令请求来进行。
1.3 发送ping命令
slave节点发送ping命令,检查socket连接是否可用,以及master节点当前能否处理请求。
1.4 身份验证
如果slave节点设置了masterauth选项,则slave节点需要向master节点进行身份认证,如果未设置,则不需要进行验证。
1.5 发送slave节点端口信息
slave将其监听的端口发送给master节点,master保存该节点端口到对应客户端的slave_listening_port字段;该字段仅显示,无其他作用。
2. 数据同步阶段
slave节点向master节点发送psync命令开始同步,同步数据可以分为全量复制和部分复制。
3. 命令传播阶段
master节点将自己执行的写命令发送给slave节点,slave节点接收命令并执行,保证数据的一致性。
除了发送写命令,主从节点还维持着心跳机制:PING和REPLCONF ACK。
命令传播是异步过程,即master节点不会等待slave节点的回复,数据不一致的程度,与主从节点之间的网络状况、主节点的写命令执行频率,以及主节点的repl-disable-tcp-nodelay配置等有关。
repl-disable-tcp-nodelay:该命令作用于命令传播阶段,控住master节点是否禁止与slave节点TCP_NODELAY,默认no;当设置为yes时,TCP会对包进行合并从而减少带宽,但是发送的频率会降低,从节点数据延迟增加,一致性变差;具体发送频率和linux的内核有关,默认配置40ms;当设置为no时,TCP会立即将master节点的数据发送给slave节点,带宽增加但数据延迟变小。
一般来说,只有当应用对Redis一致性容忍度较高,且主从节点之间的网络状况不好时,才会设为yes;多数情况使用默认的no。
复制
redis2.8及之后,slave可以发送psync命令请求同步数据,根据主从节点当时状态的不同,同步方式可能是全量复制或部分复制。
- 全量复制
用于初次复制或其他无法部分复制的情况,将master的数据全部发送给slave节点。
- 部分复制
用于网络中断等情况的复制,只将中断期间的master执行的写命令发送给slave,更加高效;如果中断时间较长,master没有保存中断期间完整的写命令,则仍使用全量复制。
复制偏移量
主节点和从节点各自维护一个offset,代表的是主节点向从节点传递数据的字节数,主节点向从节点传播n个字节数时,主节点offset+n,从节点接收n个字节数时,从节点offset+n。
offset用于判断主从节点数据库状态是否一致,若相等,则一致,若不同,则不一致,需要根据两个offset找出缺失的那部分数据;如果主节点的offset是n+m,从节点offset是n,那么部分复制就只需要吧n+1 ~ m的数据传递给从节点。
复制积压缓冲区
由主节点维护、固定长度的、FIFO队列,默认1MB,当主节点开始有从节点时创建,作用是备份主节点最近发送给从节点的数据,无论有多少个从节点,复制积压缓冲区只有一个。
在命令传播阶段,主节点除了将写命令发送给从节点,也会发送一份到积压缓冲区,复制积压缓冲区还存储了每个字节对应的offset,因为定长且先进先出,时间较早的写命令会被挤出缓冲区。
复制积压缓冲区可以备份的写命令有限,当主从节点offset差距过大超出缓冲区长度时,无法进行部分复制。repl-backlog-size配置缓冲区大小。如果需要复制的offset偏移量在积压缓冲区内,则进行部分复制,否则进行全量复制。
服务器运行id(runid)
每个redis节点(不分主从)在启动时都会生成一个随机ID(由40个随机的十六进制字符组成),runid用来唯一识别一个redis节点。(debug reload重启:重启后,主节点的runid和offset都不受影响,但会清空当前内存中的数据,重新从RDB文件中加载,这个过程会导致主节点的阻塞。)
主从节点初次复制时,主节点会将其runid发送给从节点存储起来,当断线重连时,从节点会将该runid发送给主节点,主节点判断是否进行部分复制:
1. 如果从节点保存的runid和主节点现在的runid一致,说明之前进行过主从同步,则会继续尝试是否进行部分复制(取决于offset和复制积压缓冲区);
2. 如果从节点保存的runid和主节点现有的runid不一致,说明该从节点断线前同步的redis节点并不是当前主节点,只能进行全量复制。
作用
- 单点故障;
- 读写分离(master写,slave读);
- 一主多从;
- 多副本;
复制配置
- slaveof
在需要作为slave的机器上执行命令 slaveof ip port 角色变为slave,会将本身已有的数据删除,完全同步master的数据。
slaveno one 关闭slave角色,重新作为master角色。
- 配置文件方式
文件中配置 replicaof ip port,启动时作为slave。
Redis Sentinel
主从复制高可用问题
1. 若master故障,需要手动故障转移。
2. 写能力和存储能力受限。
哨兵机制解决问题1,在master故障时,自动选举出一个新的master,而客户端不感知;如果旧master服务恢复,则会重新作为slave加入。
配置开启sentinel
1. 配置开启主从节点:redis-server redis.conf;
daemonize yes
bind 0.0.0.0
port 端口
dir /user/local/data
logfile /user/local/6379.log
# 作为slave绑定哪个master
replicaof ip port
2. 配置开启sentinel监控节点:redis-sentinel sentinel.conf;
port 23456
dir /usr/local/redis/data/
logfile 23456.log
# 告诉sentinel去监听地址为ip:port的一个master,这里的master-name可以自定义,quorum是一个数字,指明当有多少个sentinel认为一个master失效时,master才真正失效
sentinel monitor mymaster 127.0.0.1 7000 2
# 指定了需要多少失效时间,一个master才会被这个sentinel主观的认为不可用,单位是毫秒,默认30s
sentinel down-after-milliseconds mymaster 30000
指定在发生failover主备切换时,最多可以有多少个slave同时对新的master进行同步,这个数字越小,完成failover所需的时间就越长,但如果这个数字越大,就意味着越多的slave因为replication而不可用;可以通过将这个值设为1来保证每次只有一个slave处于不能处理命令请求的状态
sentinel parallel-syncs mymaster 1
# 失效之后多久重新检测一次master状态
sentinel failover-timeout mymaster 180000
客户端测试
public void test(){
String master = "mymaster";
Set<String> sentinels = new HashSet<>();
sentinels.add("127.0.0.1:23456");
sentinels.add("127.0.0.1:23457");
sentinels.add("127.0.0.1:23458");
JedisSentinelPool pool = new JedisSentinelPool(master, sentinels);
while (true){
Jedis jedis = null;
try {
jedis = pool.getResource();
jedis.incr("age");
System.out.println(jedis.get("age"));
TimeUnit.SECONDS.sleep(1);
}catch (Exception e){
e.printStackTrace();
}
}
}
Redis Cluster
解决
1. 存储容量不够情况;
2. 写数据QPS提高;
3. 提高网络流量。
数据分布
数据分片
分区规则
根据key的规则来分区
- 顺序分区
1. 数据容易倾斜;
2. 键值分布业务相关;
3. 支持批量操作;
4. 可以顺序访问。
- Hash分区
1. 数据分散度高;
2. 键值分布与业务无关;
3. 支持批量操作;
4. 无法顺序访问。
Hash分布
- 节点取余扩容
客户端分片。
在使用Hash取余运算的时候,当对服务器扩容时,比如由原来的3个服务扩容到4个服务节点时,会有90%的数据需要迁移操作;如果需要保证数据较少的迁移,可以考虑翻倍扩容,如3个节点扩容至6个节点,需要迁移的数据为50%。
- 一致性Hash
客户端分片。
采用Hash环的概念,在一个环上有0 - 2^32个数值,在环上均匀分配4个服务节点,每个服务节点负责对应的一片区域,当插入一个key时,首先对key进行Hash值运算,计算出在环上的对应位置,然后把对应的数据顺时针找到对应的存储节点,进行数据的存储和读取。
当插入一个新的服务节点5时,其中只会影响节点2负责的节点资源,需要把节点2负责的部分数据迁移(不会自动迁移)到节点5。
客户端分片:Hash+顺时针(服务端不限制插入,由客户端计算位置找到对应的节点);
节点伸缩:扩容或者删除只会影响相邻节点,不会影响其他节点;
翻倍伸缩:保证最小迁移和负载均衡;
- 虚拟槽分配
服务端分片。对于所有的服务节点,都知道集群中的哪些服务器节点负责哪些slot(槽)。槽的大小固定13686,服务节点不固定。
当客户端连接一个Node1服务节点时,进行插入数据,首先对key进行Hash运算,计算出对应的Hash值(0-65536),然后把Hash和13686进行与运算,得出的结果值(0-13686),服务端接收到对应的请求,如果在当前服务节点(不固定哪个节点),直接进行数据操作,如果不在当前服务节点,那么会连接到对应的服务节点进行数据操作。
搭建高可用集群
- 原生命令安装
1. 启动多个集群节点服务器;
集群配置参数 - 6个节点
port 7000
# 启用集群
cluster-enabled yes
# 保存节点配置文件的路径
cluster-config-file nodes.conf
# 集群节点通信超时时间,毫秒
cluster-node-timeout 5000
# 当节点出现问题的时候是否不让整个集群提供服务
cluster-require-full-coverage no
2. 使用meet操作,让各个服务器之间感知;
# 本节点和指定节点互相通信
> cluster meet ip port
3. 需要给每个master服务器指定slot,分配槽位;
# 分配槽位,需要一个一个指定,可通过脚本操作
> redis-cli -h {ip} -p {port} cluster addslots {slot}
4. 给master节点指定复制节点。
# 指定是哪个节点的slave服务
> redis-cli -h {slave_ip} -p {slave_port} cluster replicate {node_id}
# 查看集群节点信息,包含id信息
> cluster nodes
- 工具安装
redis5及以上支持。
1. 启动集群节点;
2. 通过命令进行安装
# --cluster-replicas 1 表示主从比例1:1
# --cluster-replicas 2 表示主从比例1:2
> redis-cli --cluster create {node1_ip:port} {node2_ip:port} {...} --cluster-replicas 1
一些扩展
常用Java客户端
jedis 接近命令行操作
redission 异步和非阻塞的api(netty框架)
lettuce 异步和非阻塞的api(netty框架)
常见问题
- 穿透
指查询一个一定不存在的数据,由于缓存是不命中的,需要从数据库查询,查不到数据不写入缓存,这将导致这个不存在的数据每次都需要去查询数据库,造成缓存穿透。
解决方案:
1. 单个key穿透
持久层查询不到就缓存空结果,查询时先判断缓存中是否存在,如果有直接返回空,没有则查询后返回,对于空值,设置一个较小的缓存过期时间。
2. 多个key穿透
先判断key是否是一个存在的key,如果key是一个合法的key,那么就查询数据,如果key不合法,那么就不查数据库。
数据量不大,可以把key都存放到redis的set集合中;数据量非常大时(上亿级别),可以考虑使用布隆过滤器来完成。
- 雪崩
如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落到了数据库上,造成数据库服务器的雪崩。
解决方案:
1. 让key的过期时间均匀分布;
2. 限流 - 控制同一个key的线程数量(读取数据库的线程数量),常见的限流算法有:计数算法,滑动窗口,令牌桶,漏桶。
- 热点key
某个key的访问非常频繁,导致redis服务器的压力剧增,没法保证redis可以正常提供服务。
因为是redis服务器的压力导致瘫痪的,所以解决方法主要集中在提高redis的qps和控制redis的访问。
解决方案:
1. 扩容,添加集群中的从服务器,提高读数据能力;
2. 考虑使用本地缓存,把redis中的热点数据直接缓存在本地的服务器内存中,减少对redis的一个访问。