redis的基本知识

295 阅读19分钟

问题1:Redis的基本介绍

1.Redis的特点

  • 开源
  • 多种数据结构

image-20200428133732778

现在也有 BitMaps:位图、HyperLogLog:超小内存唯一值计数、GEO:地理信息定位(属于衍生)

  • 基于键值的存储服务系统

  • 高性能10W/s 读写,数据存在内存中、使用C语言、单线程(非阻塞IO,避免线程切换和竞态消耗)

  • 功能丰富(发布订阅、事务、Lua脚本、pipeline)

  • 持久化(Redis所有数据保持在内存中,对数据的更新将异步地保存到磁盘上。)

  • 高可用、分布式

  • 支持多种编辑语言

2.Redis的应用场景

  • 缓存系统
  • 排行榜
  • 计数器
  • 社交网络
  • 消息队列系统
  • 实时系统

3.Redis的文件说明

redis-server:Redis服务器

redis-cli:Redis命令行客户端

redis-benchmark:Redis性能测试

redis-check-aof:AОF文件修复工具

redis-check-dump:RDB文件检查工具

redis-sentinel:Sentinel服务器

4.Redis常用配置

daemonize:是否是守护进程(nolyes)

port:Redis对外端口号

logfile:Redis系统日志

dir:Redis工作目录

问题2:API的理解和使用

1.数据结构与内部编码

image-20200428163453937

2.通用命令

keys #遍历所有key(支持正则表达式,一般不在生产环境使用) O(n)

dbsize #计算key的总数,O(1)

exists key #检查key是否存在

del key #删除指定key-value

expire key seconds #key在seconds秒后过期

ttl key #查看key剩的过А时间

(-2代表key已经不存在了)

persist key #去掉key的过期时间

-1(-1代表key存在,并且没有过期时间。)

type key #返回key的类型

3.字符串类型

缓存、分布式锁、计数器(单线程,天然成立)

1.get key#获取key对应的value

​ mget #批量获取(传输1次)

2.set key value#不管key是否存在,都设置

​ setnx key value#key不存在,才设置

​ set key value xx#key存在,才设置

​ mset #批量设置 O(n)

3.del key#删除key-value

4.incr key #key自增1,如果key不存在,自增后get(key)=1

​ decr key #key自减1,如果key不存在,自减后get(key)=-1

​ incrby key k #key自增k,如果key不存在,自增后get(key)-k

​ decr key k #key自减k,如果key不存在,自减后get(key)=-k

5.getset key newvalue #set key newvalue并返回旧的value

​ append key value #将value追加到旧的value

​ strlen key #返回字符串的长度(注意中文)

​ incrbyfloat key 3.5 #增加key对应的值3.5

​ getrange key start end #获取字符串指定下标所有的值

​ setrange key index value #设置指定下标所有对应的值

tips:计算机的字符与编码集

ASCII码 使用7个bits就可以完全表示ASCI码,包含95个可打印字符、33个不可打印字符(包括控制字符)

Extended ASCII码 使用8个bits,主要为了扩展不同地区的符号,如数学运算符

中文编码集--GB2312 一共收录了7445个字符包括,6763个汉字和682个其它符号,不符合国际的标准

中文编码集--GBK 向下兼容GB2312,向上支持国际ISO标准,收录了21003个汉字,支持全部中日韩汉字·,汉字占2个字节

兼容全球的字符集:Unicode Unicode:统一码、万国码、单一码Unicode定义了世界通用的符号集,UTF-*实现了编码,UTF-8以字节为单位对Unicode进行编码,(字符集定义了文字和二进制的对应关系,为字符分配了唯一的编号,而字符编码规定了如何将文字的编号存储到内存中。)UTF-8 使用一至四个字节为每个字符编码

有的编码方式采用 1~n 个字节存储,是变长的,例如 UTF-8、GB2312、GBK 等;如果一个字符使用了这种编码方式,我们就将它称为多字节字符,或者窄字符。

变长带来的问题就是怎么定位字符的起止:1) 一是从字符集本身下手,在设计字符集时,刻意让不同的字符编号有不同的特征。2) 二是从字符编号下手,可以设计一种转换方案,字符编号在存储之前先转换为有特征的、容易定位的编号,读取时再按照相反的过程转换成字符本来的编号。

有的编码方式是固定长度的,不管字符编号大小,始终采用 n 个字节存储,例如 UTF-32、UTF-16 等;如果一个字符使用了这种编码方式,我们就将它称为宽字符。

  • UFT-8:一种变长的编码方案,使用 1~6 个字节来存储;

    UTF-8 的编码规则很简单:如果只有一个字节,那么最高的比特位为 0;如果有多个字节,那么第一个字节从最高位开始,连续有几个比特位的值为 1,就使用几个字节编码,剩下的字节均以 10 开头。只有 UTF-8 兼容 ASCII,UTF-32 和 UTF-16 都不兼容 ASCII,

  • UFT-32:一种固定长度的编码方案,不管字符编号大小,始终使用 4 个字节来存储;所以直接存储 Unicode 编号即可,不需要任何编码转换。浪费了空间,提高了效率

  • UTF-16:介于 UTF-8 和 UTF-32 之间,使用 2 个或者 4 个字节来存储,长度既固定又可变。

4.哈希类型

hget key field #获取hash key对应的field的value

hset key field value #设置hash key对应field的value

hdel key field #删除hash key对应field的value

hexists key field #判断hash key是否有field

hlen key #获取hash key field的数量

hexists key field #判断hash key是否有field

hlen key #获取hash key field的数量

hmget key field1 field2...fieldN #批量获取hash key的一批field对应的值

hmset key field1 value1 field2 value2...fieldN valueN #批量设置hash key的一批field value

hgetall key #返回hash key应所有的field和value

hvals key #返回hash key对应所有field的value

hkeys key #返回hash key对应所有field

hsetnx key field value #设置hash key对应field的value(如field已经存在,则失败)

hincrby key field intCounter #hash key对应的field的value自增intCounter

hincrbyfloat key field floatCounter #hincrby浮点数版

image-20200428215016938

5.列表类型

rpush key valuel value2...valueN #从列表右端插入值(1-N个)

Ipush key valuel value2..valueN #从列表左端插入值(1-N个)

linsert key beforelafter value newValue #在list指定的值前后插入newValue

lpop key #从列表左侧弹出一个item

rpop key #从列表右侧弹出一个item

Irem key count value #根据count值,从列表中删除所有value相等的项 (1)count>0,从左到右,删除最多count个value相等的项 (2)count<0,从右到左,删除最多Math.abs(count)个value相等的项 (3)count-0,删除所有value相等的项

Itrim key start end #按照索引范围修剪列表

Iset key index newValue #设置列表指定索引值为newValue

Irange key start end(包含end) #获取列表指定索引范围所有item

lindex key index #获取列表指定索引的item

llen key #获取列表长度

阻塞

blpop key timeout #lpop阻塞版本,timeout是阻塞超时时间,timeout=0为永远不阻塞

brpop key timeout #rpop阻塞版本,timeout是阻塞超时时间,timeout-0为永远不阻塞

blpop key timeout #lpop阻塞版本,timeout是阻塞超时时间,timeout-0为永远不阻塞

LRUSH + LPOP = Stack

LPUSH + RPOP = Queue

LPUSH + LTRIM = Capped Collection

LPUSH + BRPOP = Message Queue

6.集合类型

sadd key element #向集合key添加element(如果element已经存在,添加失败)

srem key element #将集合key中的element移除掉

scard user:1:follow=4#计算集合大小

sismember user:1:follow it=1(存在)#判断it是否在集合中

srandmember user:1:follow count=his#从集合中随机挑count个元素

spop user:1:follow=sports#从集合中随机弹出一个元素

smembers user:1:follow=music his sports it#获取集合所有元素

sdiff user:1:follow user:2:follow =music his#差集

sinter user:1:follow user:2:follow =it sports#交集

sunion user:1:follow user:2:follow=it music his sports news ent#并集

sdiff|sinter|suion+store destkey..#将差集、交集、并集结果保存在destkey中

image-20200429085417533

7.有序集合类型

add key score element可以是多对 #添加Score和element

zrem key element(可以是多个) #删除元素

zscore key element #返回元素的分数

zincrby key increScore element #增加或减少元素的分数

zcard key #返回元素的总个数

zrank player:rank ronaldo #获取排名(从0开始,从小到大)

zrange player:rank 0-1 withscores #带分数的范围查询

zcount key minScore maxScore #返回有序集合内在指定分数范围内的个数

zremrangebyrank key start end #删除指定排名内的升序元素

zremrangebyscore key minScore maxScore #删除指定分数内的升序元素

问题3:Jedis

image-20200429100734366

public class JedisTest {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.40.131",6382);
        jedis.set("hello11", "world11");
        String hello = jedis.get("hello11");
        System.out.println(hello);
    }
}
public class JedisTest {
    public static void main(String[] args) {
        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
        JedisPool jedisPool = new JedisPool(poolConfig, "192.168.40.131", 6382);
        Jedis jedis =null;
        try {
            jedis = jedisPool.getResource();
            jedis.set("h111", "gsyzh");
        } catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            if (jedis != null) {
                jedis.close();
            }
        }
    }
}

问题4:Redis的功能

1.慢查询

Redis生命周期

image-20200429101632676

特点

1.先进先出队列 2.固定长度 3.保存在内存内

配置

slowlog-max-len 慢查询阈值(单位:微秒)

slowlog-log-slower-than =0,记录所有命令

(支持动态配置)config set slowlog-max-len 1000、config set slowlog-log-slower-than 1000

命令

1.slowlog get[n]:获取慢查询队列 2.slowlog len:获取慢查询队列长度 3.slowlog reset:清空慢查询队列

运维经验

1.slowlog-max-len不要设置过大,默认10ms,通常设置1ms 2.slowlog-log-slower-than不要设置过小,通常设置1000左右。 3.定期持久化慢查询。

2.pipeline

image-20200429103556860

public class JedisTest {
    public static void main(String[] args) {
        long a = System.currentTimeMillis();
        Jedis jedis = new Jedis("192.168.40.131", 6382);
        for (int i = 0; i < 10000; i++) {
            jedis.hset("hashkey:"+i,"field"+i,"value"+i);
        }
        long b = System.currentTimeMillis();
        System.out.println(b-a); //1115ms
    }
}
public class JedisTest {
    public static void main(String[] args) {
        long a = System.currentTimeMillis();
        Jedis jedis = new Jedis("192.168.40.131", 6382);
        for (int i = 0; i < 100; i++) {
            Pipeline pipelined = jedis.pipelined();
            for (int j = i*100; j <(i+1)*100 ; j++) {
                pipelined.hset("hashkey:" + j, "field" + j, "value" + j);
            }
            pipelined.syncAndReturnAll();
        }
        long b = System.currentTimeMillis();
        System.out.println(b-a); //106ms
    }
}

3.发布订阅

发布者(publisher)publish channel message

订阅者(subscriber)subscribe[channel]#订阅一个或多个 unsubscribe[channel]#取消一个或多个

频道(channel)

4.Bitmap

setbin、getbin、bitcount

bitop op destkey key[key..] 做多个Bitmap的and(交集),or(并集),not(非)、xor(异或)、操作并将结果保存在destkey中

5.HyperLogLog

极小空间完成独立数量统计。 pfadd key element[element..]:向hyperloglog添加元素

pfcount key[key..]计算hyperloglog的独立总数

pfmerge destkey sourcekey[sourcekey..:合并多个hyperloglog

1.是否能容忍错误?(错误率:0.81%)

2.是否需要单条数据?(不能取出来)

6.GEO

GEO(地理信息定位):存储经纬度,计算两地距离,范围计算等 使用zset实现 redis3.2以上

geo key longitude latitude member [longitude latitude member...] #增加地理位置信息

geopos key member[member...] #获取地理位置信息

geodist key member1 member2[unit] #获取两个地理位置的距离 #unit:m(米)、km(千米)、mi(英里),ft(尺)

问题5:Redis持久化的取舍和选择

什么是持久化 redis所有数据保持在内存中,对数据的更新将异步地保存到磁盘上。

快照(1.MySQL Dump、2.Redis RDB)

触发机制-主要三种方式 save(同步)、bgsave(异步)、自动

image-20200429152534694

image-20200429152715055

在这些情况下会自动保存:1.全量复制、2.debug reload、3.shutdown

写日志(1.MySQL Binlog、2.Hbase HLog、3.Redis AOF)

image-20200429154247491

AOF是有重写功能的:减少硬盘占用量、加速恢复速度

bgrewriteaof命令

image-20200429154839582

image-20200429154903061

对比

image-20200429155921872

一般的策略是关闭rdb,开启aof

常见的持久化问题

1.fork操作

与内存量息息相关:内存越大,耗时越长(与机器类型有关)info:latest_fork_usec

改善:

1.优先使用物理机或者高效支持fork操作的虚拟化技术 2.控制Redis实例最大可用内存:maxmemory 3.合理配置Linux内存分配策略:vm.overcommit-memory=1 4.降低fork频率:例如放宽AOF重写自动触发时机,不必要的全量复制

2.子进程开销和优化

image-20200429164038063

3.AOF追加阻塞

和硬盘的优化有关

问题6:主从复制

复制的配置:可以使用slaveof命令或者config配置

全量复制和部分复制

全量复制的开销:

1.bgsave时间、2.RDB文件网络输时间、3.从节点清空数据时间

4.从节点加载RDB的时间、5.可能的AOF重写时间

image-20200429170701213

部分复制:

image-20200429171154641

故障处理

以一主两从为原型:

image-20200429171657313

image-20200429171810616

开发运维常见问题

1.读写分离

  • 复制数据延迟
  • 读到过期数据
  • 从节点故障

2.主从配置不一致

  • 例如maxmemory不一致:丢失数据
  • 例如数据结构优化参数(例如hash-max-ziplist-entries):内存不一致

3.规避全量复制

image-20200429172617729

4.规避复制风暴

image-20200429172856667

问题7:Redis Sentinel

Redis Sentinel是Redis的高可用实现方案:故障发现、故障自动转移、配置中心、客户端通知。

主从复制的一些问题(手动故障转移、写能力和存储能力受限)

Redis Sentinel(默认端口26379)架构

image-20200429173323917

故障转移的步骤(master宕机)

1.多个sentinel发现并确认master有问题。 2.选举出一个sentinel作为领导。 3.选出一slave作为master. 4.通知其余slave成为新的master的slave. 5.通知客户端主从变化 6.等待老的master复活成为新master的slave.

sentinel的三个定时任务

1.每10秒每个sentinel对master和slave执行info

  • 发现slave节点
  • 确认主从关系

image-20200429210924243

2.每2秒每个sentinel通过master节点的channel交换信息(pub/sub)

  • 通过_ _ sentinel _ _:hello频道交互
  • 交互对节点的“看法”和自身信息

image-20200429211448479

3.每1秒每个sentinel对其他sentinel和redis执行ping(心跳监测)

image-20200429211330019

主观下线与客观下线---领导者选举---故障转移(选择合适的slave结点)

1选择slave-priority(slave节点优先级)最高的slave节点,如果存在则返回,不存在则继续。 2选择复制偏移量最大的slave节点(复制的最完整),如果存在则返回,不存在则继续。 3选择runld最小的slave节点。

常见的运维问题

1.节点运维(上下线)

  • 机器过保质期下线
  • 机器性能不足
  • 节点服务不稳定

节点上线:主节点:sentinel failover进行替换。从节点:slaveof即可,sentinel节点可以感知。 sentinel节点:参考其他sentinel节点启动即可。

2 .高可用读写分离

sentinel只会对master进行故障转移

应该实现客户端对从节点的监测

+switch-master:切换主节点(从节点晋升主节点) +convert-to-slave:切换从节点(原主节点降为从节点) +sdown:主观下线。

问题8:Redis Cluster

1.为什么引入集群

并发量、数据量、网络流量问题

--使用分布式:加机器

Redis cluster数据分区规则采用虚拟槽方式(16384个槽),每个节点负责一部分槽和相关数据,实现数据和请求的负载均衡。

2.分布式数据库-数据分区

顺序分区

哈希分区(例如取余)

image-20200429215555408

  • 节点取余--扩容时会造成批量移动---建议使用成倍的扩容(50%)
  • 一致性哈希--节点比较多的时候使用(客户端分片:哈希+顺时针)翻倍伸缩:保证最小迁移数据和负载均衡
  • 虚拟槽分区

image-20200429220833535

3.集群的安装

原生命令安装

1.配置开启节点 cluster-enabled yes 2.meet cluster meet ip port 3.指派槽 cluster addslots slot [slot ...] 4.设置主从 cluster replicate node-id

ruby安装

可视化部署

4.集群伸缩

伸缩原理:集群伸缩=槽和数据在节点之间的移动

扩容集群:准备新节点、加入集群(meet)、迁移槽和数据(如下图)

image-20200429225201264

缩容集群:下线迁移槽、忘记节点、关闭节点

5.客户端路由

moved重定向 ask重定向(两者都是客户单重定向、moved:槽已经确定迁移、ask:槽还在迁移中)

smart客户端:使用直连,客户端是一个智能的客户端,客户端内部负责计算维护键->槽->节点的映射,用于快速定位到目标节点。

6.批量操作怎么实现?

mget mset必须在一个槽

1.串行mget 2.串行1O 3.并行10 4.hash_tag

image-20200430093419807

7.开发运维常见问题

集群完整性:cluster-require-full-coverage默认为yes 带宽消耗:

避免"大"集群:避免多业务使用一个集群,大业务可以多集群。 尽量均匀分配到多机器上:保证高可用和带宽

Pub/Sub广播

publish在集群每个节点广播:加重带宽 可以使用单独“走"一套Redis Sentinel解决

image-20200430101730363

数据倾斜

原因:节点和槽分配不均、不同槽对应键值数量差异较大、包含bigkey、内存相关配置不一致

请求倾斜

热点key:重要的key或者bigkey

优化:避免bigkey、热键不要用hash_tag、当一致性不高时,可以用本地缓存+ MQ

读写分离

只读连接:集群模式的从节点不接受任何读写请求。 (-重定向到负责槽的主节点、-readonly命令可以读:连接级别命令)

数据迁移

官方迁移工具:redis-trib.rb import

只能从单机迁移到集群 不支持在线迁移:source需要停写 不支持断点续传 单线程迁移:影响速度 在迁移过程中,使用redis,可能导致数据丢失

在线迁移:redis-migrate-tool、redis-port

集群vs单机

集群具有一定的限制:

  • key批量操作支持有限:例如mget,mset必须在一个slot
  • Key事务和Lua支持有限:操作的key必须在一个节点
  • key是数据分区的最小粒度:不支持bigkey分区
  • 不支持多个数据库:集群模式下只有一个db 0
  • 复制只支持一层:不支持树形复制结构

问题9:缓存的使用与设计

1.缓存的受益与成本

**受益:**1.加速读写 2.降低后端负载

成本:

数据不一致:缓存层和数据层有时间窗口不一致,和更新策略有关。

代码维护成本:多了一层缓存逻辑。

运维成本:例如Redis Cluster

场景

1.降低后端负载:对高消耗的SQL:join结果集/分组统计结果缓存。

2·加速请求响应:利用Redis/Memcache优化10响应时间

3·大量写合并为批量写:如计数器先Redis累加再批量写DB

2.缓存更新策略

image-20200430102729976

3.缓存粒度控制

1.通用性:全量属性更好。 2.占用空间:部分属性更好。 3.代码维护:表面上全量属性更好。

4.缓存穿透问题

缓存穿透问题-大量请求不命中缓存

解决方法1-缓存空对象--问题:需要更多的键、缓存层和存储层数据“短期"不一致。

解决方法2-布隆过滤器拦截--问题:要自己编写

image-20200430104023582

本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。

相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。

如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置 1,例如针对值 “baidu” 和三个不同的哈希函数分别生成了哈希值 1、4、7,则上图转变为:

image-20200430114305826

Ok,我们现在再存一个值 “tencent”,如果哈希函数返回 3、4、8 的话,图继续变为:

image-20200430114316079

值得注意的是,4 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此它被覆盖了。现在我们如果想查询 “dianping” 这个值是否存在,哈希函数返回了 1、5、8三个值,结果我们发现 5 这个 bit 位上的值为 0,说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说 “dianping” 这个值不存在。而当我们需要查询 “baidu” 这个值是否存在的话,那么哈希函数必然会返回 1、4、7,然后我们检查发现这三个 bit 位上的值均为 1,那么我们可以说 “baidu” 存在了么?答案是不可以,只能是 “baidu” 这个值可能存在。

过小的布隆过滤器很快所有的 bit 位均为 1,那么查询任何值都会返回“可能存在”,起不到过滤的目的了。布隆过滤器的长度会直接影响误报率,布隆过滤器越长其误报率越小。

image-20200430114434923

k 为哈希函数个数,m 为布隆过滤器长度,n 为插入的元素个数,p 为误报率。

5.缓存雪崩

缓存雪崩是指,缓存层出现了错误,不能正常工作了。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。

2、解决方案

(1)redis高可用

这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。

(2)限流降级

这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。

(3)数据预热

数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

6.无底洞问题优化

机器增加--但是性能没有提高

image-20200430104419193

优化IO的几种方法

1.命令本身优化:例如慢查询keys.hgetall bigkey 2.减少网络通信次数 3.降低接入成本:例如客户端长连接/连接池、NIO等

7.热点key重建优化

1.三个目标:

减少重缓存的次数 数据尽可能一致 减少潜在危险--死锁

2.两个解决:

互斥锁(mutex key)

image-20200430105736217

永远不过期(可能在一段时间使用老的值)

1.缓存层面:没有设置过期时间(没有用expire) 2.功能层面:为每个value添加逻辑过期时间,发现超过逻辑过期时间,会使用单独的线程去构建缓存。

image-20200430110140961