分布式专题|吐血整理Redis 十四大知识点,帮助小白从0到1蜕变

文章目录

摘要

本篇博文将带领大家重新认识redis,文章先从redis的基本数据类型的简单入门使用讲起,再逐步进阶,讲解高级数据类型,学习完高级数据类型之后,将会让大家带着疑问,去学习更加深入的知识。如何实现分布式锁?为什么单线程的redis如此快?redis主从同步的原理和过程是什么样的?redis如何构建高可用的集群?这些都是学完redis之后,必知必会的知识,也是面试官必问的。本人所有的博客都会围绕着两个目标出发:a.帮助大家轻松应对面试,b.系统性的学习,不再为了面试而学习。

环境搭建

redis Docker方式安装

  • 下载redis镜像
     docker pull redis
    
  • 创建实例并启动
    mkdir -p /mydata/redis/conf/
    touch /mydata/redis/conf/redis.conf
    docker run -p 6379:6379 --name redis \
    -v /mydata/redis/data:/data \
    -v /mydata/redis/conf/redis.conf:/etc/redis/redis.conf \
    -d redis redis-server /etc/redis/redis.conf
    
    
  • 编辑配置文件
    vim /mydata/redis/conf/redis.conf
    
    ## 复制以下内容
    daemonize no
    pidfile /var/run/redis.pid
    port 6379
    timeout 0
    loglevel verbose
    logfile stdout
    databases 16
    save 900 1
    save 300 10
    save 60 10000
    rdbcompression yes
    dbfilename dump.rdb
    dir ./
    slave-serve-stale-data yes
    appendonly yes
    appendfsync everysec
    no-appendfsync-on-rewrite no
    auto-aof-rewrite-percentage 100
    auto-aof-rewrite-min-size 64mb
    slowlog-log-slower-than 10000
    slowlog-max-len 1024
    

使用源码方式安装redis

  • 下载redis安装包 (https://cloud.189.cn/t/Q7F3amNnEJZb(访问码:y9ke))
    wget http://download.redis.io/releases/redis-4.0.6.tar.gz
    
  • 解压压缩包
    tar -zxvf redis-4.0.6.tar.gz
    
  • 安装gcc依赖
    yum install gcc
    
  • 跳转到redis解压目录下
    cd redis-4.0.6
    
  • 编译安装
    make MALLOC=libc
    
  • 安装
    cd src && make install
    
  • 启动redis
    #切换到src目录下,执行以下命令
    ./redis-server
    
  • 连接redis
    ./redis-cli
    

一、五种基本数据类型的操作与应用场景

string 字符串类型

基本指令

  • 添加数据
    set name lezai
    
  • 删除数据
    del name
    
  • 获取数据
    get name
    
  • 整数自增
    # 默认自增1
    incry age 
    #自定义自增值
    incrby age -1
    
  • 单独设置有效期,单位为秒
    expire name 10 
    
  • 创建key的同时设置超时时间
    setex name 10 lezai
    
  • key不存在则创建成功否则创建失败,用来实现分布式锁的基本原理
    setnx name lezai
    

应用场景

缓存、计数器、分布式锁

hash(哈希)

redis中的hash类似于java中的hashmap,里面存储了大量的键值对,底层实现和java的hashmap一样,也是使用数组+链表的结构。
但是hash里面的值都只能是字符串型,而且他们的rehash方式是不一样的,redis的hash并不是一次性将全部的数据进行rehash,而是采用渐进式的方式,也就是说会存在两个相同的结构,等到就得数据全部迁移完成,才会将旧的数据结构删除,使用新的代替。

基本操作指令

  • 添加数据
    hset student name lezai age 26
    
  • 删除数据
    hdel student name age
    
  • 获取单个字段
    hget student name
    
  • 获取所有字段
    hgetall student
    
  • 获取map长度
    hlen student
    
  • 自增
    hincrby student age 1
    

应用场景

缓存一个实体对象、购物车

list(链表)

基本指令

redis的链表是一个双向链表,左右两边都可以插入和去除数据,其实redis底层存储的不仅仅是简单的链表结构,而是称之为一个快速列表的结构
,在列表元素较少的时候,redis会使用一块连续的内存存储,这个结构是ziplist,成为压缩列表。当数据内容较多的情况下才会改成quicklist,因为普通列表存储一个内容需要再存储两个额外的指针,很浪费空间,所以在数据量大的时候,才将多个压缩列表使用双向列表链接起来,组成快速列表。

  • 右边进左边出(先进先出)-队列
    #添加列表
    rpush datas 1 2 3
    #获取列表长度
    llen datas
    #取出数据
    lpop datas
    
  • 右边进右边出(先进后出)-栈
    #添加列表
    rpush datas 1 2 4
    #取出数据
    rpop datas
    
  • 查询元素
    #获取指定位置的元素(遍历所有元素,底层采用链表结构)
    lindex datas -1[-1代表倒数第一个]
    #查询指定区间元素,以下取出所有数据
    lrange datas 0 -1
    #截取指定区间的元素
    ltrim datas 0 2
    

应用场景

消息队列、排行榜

set(集合)

set集合相当于java中的hashset,它内部是无序的,唯一的,它内部相当于一个特殊的字段,字典中所有的value都是一个NULL,当集合中最后一个元素被移除的时候,内存被回收。set可以存储活动中中奖用户的ID,因为有去重功能,可以保证一个用户不会中奖两次

基本指令

  • 添加数据
    sadd records r1 r2
    
  • 获取所有数据
    smembers records
    
  • 判断元素是否存在
    sismember records r1
    
  • 获取集合长度
    scard records
    

应用场景

好友、随机推荐、黑白名单

zset(有序列表)

zset其实就是sortedset和hashmap的结合体,它具备set的唯一性。又具备map的key,value性质,但是这个value存储的是他的分值,用来排序。他的内部采用一个“跳跃列表的数据结构”。
zset可以用来存储粉丝列表,value值是用户ID,score是关注时间,我们可以对粉丝列表按照关注时间进行排序。

基本指令

  • 添加元素
    zadd grades 100 zhangsan 99 lisi
    
  • 获取指定范围内的元素
    #默认按照score升序
    zrange grades 0 -1
    降序排序
    zrevrange grades 0 -1
    
  • 获取指定元素的score
    zscore grades zhangsan
    
  • 获取元素的排名
    zrank grades lisi
    
  • 获取两个分数区间的数据
    zrangebyscore grades -inf(负无穷大) inf(正无穷大) widthscores
    
  • 删除指定元素
    zrem grades zhangsan
    

扩展知识

  • 什么是跳跃列表?

    首先跳跃列表是按照分层制的,最后面一层会把所有元素按照顺序串起来,然后在这些底层元素中再选部分数值作为代表,抽取到上一次,然后再从这些抽取中的数据再选择部分数据作为代表,抽取到顶层,这样进行数据查找的时候只需要从顶层查到大致所在的范围,然后逐渐往下渗透,找到具体的位置,添加和删除也是一样步骤。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m4gUw1yk-1596036210383)( https://lezaimall.oss-cn-beijing.aliyuncs.com/redis/%E5%9B%BE%E7%89%871.png)]

  • 设置了key的过期时间,如何查看?

    ttl key
    

二、如何使用redis实现附近的人的功能

当两个元素相距不是很远,可以直接用勾股定理就能算的元素之间的距离,但是当我们的坐标是经纬度这种数据时,使用勾股定理就不容易计算了,那么如何计算两个经纬度之间的距离呢,筛选附近的人呢?假如我们现在想要获取(x0,y0)坐标 附近为r的元素,可以这样去查询:
select id from pos where x0-r < x <x0+r and y0-r <y <y0+r
但是把所有数据全部放到数据库中,肯定不是很好的解决方案,量大了就无法使用了。业界比较通用的计算距离的方法是geohsh算法,刚好redis也支持这种算法

在redis中,geo将二维经纬度使用52位的整数进行编码,然后放入zset集合中,zset的value是key,scroe存储的是52位的整数值,然后通过score排序,算出附近的人。

基本指令

  • 添加位置信息
    geoadd anhuiprovince 117.283043 31.861191 hefei
    geoadd anhuiprovince 117.043549 30.508829 anqing
    geoadd anhuiprovince 117.489159 30.656036 chizhou
    geoadd anhuiprovince 118.317322 29.709238 huangshan
    
  • 删除位置信息
    zrem anhuiprovince hefei anqing
    
  • 获取元素间的距离
    # km,m ,ml,ft 代表距离单位
    geodist anhuiprovince anqing hefei km[m,ml,ft]
    
  • 获取某个元素的未知
    geopos anhuiprovince hefei
    
  • 获取元素hash值
    geohash anhuiprovince hefei
    
  • 获取附近的元素
    georadiusbymember key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
    #示例 距离安庆152km的最多4个元素 升序排序
    georadiusbymember anhuiprovince anqing 152 km count 4 asc
    

※:单个一般都很大,一般部署geo的redis不建议做集群,key迁移时会很耗时间,另外当单个key非常大时,
建议按不同维度进行查分。

三、布隆过滤器实现原理

可以把布隆过滤器理解为一个不怎么精确的set结构,当你使用它的contains方法判断某个对象是否存在时,他可能会误判,但是布隆过滤器也不是特别不精确,只要参数设置的合理,它的精确度也是可以得到控制的,只会有小小的误判率。

当过滤器说某个值存在时,这个值可能不存在,当他说某个值不存在时,那就肯定不存在的,这个是由于它的存储方式决定的,

布隆过滤器的安装(docker方式)

# 安装redis布隆过滤器插件(4.0之后)
docker pull redislabs/rebloom
docker run -p6379:6379 redislabs/rebloom
docker exec -it loving_meitner redis-cli 

基本指令

  • 添加元素
    bf.add key member
    
  • 判断元素是否存在
    bf.exists key member
    
  • 批量添加元素
    bf.madd key member1 member2 ...
    
  • 批量判断元素是否存在
    bf.mexists key member1 member2 ...
    

布隆过滤器的原理

每个过滤器对应到redis里面就是一个大型的位数组合几个不一样的hash函数,当我们往过滤器中添加key时,会使用多个hash函数对key进行hash,得到一组索引值,然后将这个索引值与数组长度进行取模运算得到最终在数据上的位置,然后将对应的位置置为1,就完成了add操作。
想过滤器询问key是否存在时,也是通过相同运算得到一组索引位置,然后判断这些位置上的bit位是不是全是1 ,如果全1,则说明有可能存在,因为这些位置置为1有可能是被其他的key设置的,如果数组长度比较短,这个概率就会比较大,所以尽量不要让过滤器中的元素超过预置大小

※:布隆过滤器的err_rate设置的越小,需要的存储空间就越大,所以不需要太精确的判断的业务场景下,建议不要设置的太小。

四、redis持久化

为什么需要持久化?

redis的数据都是存放到内存中的,如果突然宕机,数据就会全部丢失,因此必须有一种机制来保证redis在内存中的数据不会丢失,这种机制就叫redis持久化机制。

持久化的方式

redis持久化分为两种,第一种是快照,第二种是AOF日志,快照是一次全量备份,AOF是连续的增量备份image

快照的原理

我们知道Redis是单线程程序,这个线程要同时负责多个客户端套接字的并发读写擦欧总和内存数据结构的逻辑读写。
在服务线上请求的同时,redis还需要进行内存快照,进行持久化,需要进行文件IO操作,文件IO操作不能使用多路复用API的。
那么单线程的redis如何既能保证处理客户端的请求又能一边持久化呢?在持久化的同时,产生的新数据怎么办?
redis使用cow(copy on write)机制来实现。

fork(多进程)

redis在持久化的时候会调用glibc的函数fork产生一个子进程,快照持久化交给子进程来处理,不影响父进程继续处理客户端的请求,子进程持久化的时候,不会修改现有内存的数据结构,他只会对数据进行遍历读取,父进程会不断对数据进行修改。
在持久化的那一刻,子进程和父进行共享的数据将会使用Cow机制进行数据段页面分离,所以父进程对数据的修改不会影响持久化的数据,父进程修改数据会复制一份当前的页面进行修改,也就是在产生子进程的那一刻,需要持久化的数据已经固定下来了。
image

AOF原理

AOF日志存储的是redis服务器的顺序指令序列,AOF日志只记录对内存进行修改的指令记录,如果需要对一个空的redis实例进行恢复,那就只需要重放这些指令就可以,来恢复当前的状态。
reids在收到客户端指令修改后,先进行参数校验、逻辑处理,如果没有问题,就立刻将为本存储到AOF日志中,redis在长期的运行过程中,AOF的日志会越来越长,如果实例宕机重启,重放整个AOF会非常慢,导致redis长时间无法对外提供服务,所以就需要对aof日志进行瘦身。

AOF重写

redis提供了bgrewriteaof指令用于对AOF日志进行瘦身,其原理就是开辟一个子进程对内存进行遍历,转换成一系列redis操作指令,序列化到一个新的aof日志中。序列化完成之后,会将这期间新增的指令追加到aof日志后面,最后就可以用新的日志文件代替旧的日志文件了

fsync

AOF是以文件的形式存在的,当程序对AOF日志进行操作时,是把数据写到内存缓存中,之后由内核异步将数据刷到磁盘中,如果这时候机器宕机,AOF的内容还没来得及刷到磁盘中,这段时间产生的数据就会全部丢失,这该怎么办呢?
Linux的glibc提供了fsync函数可以强制将内存缓存中的内容刷到磁盘,不再需要等待内核异步去刷新。fsync是一个磁盘IO操作,所以会很慢,如果每执行一条redis指令,就调用下fsync函数,那就会导致redis不再那么高性能了。
所以在生产环境下,reids是每隔1秒就执行一次fsync,而且这个是可以配置的。

redis4.0混合持久化

重启redis时,我们很少使用rdb来恢复内存状态,因为这样会丢失大量的数据,我们通常会使用AOF进行重放指令操作,这将是一个漫长的过程,需要花费很长的时间。
redis4.0引入了一个新的持久化选项——混合持久化,先读取快照进行恢复,然后读取增量的AOF日志,这里的AOF是继上一次产生快照后,产生的增量日志,不再是以前的全量日志。image

五、redis的弱事务

edis事务不同于其他数据库的事务,他是一种弱事务,它的用法很简单,不需要记太多的事务隔离级别和事务的传播性,这就要求我们不能把redis事务当作一个正常的事务去操作。
multi(开启事务)、exec(执行事务)、discard(回滚事务)

示例用法

set name zhangsan
# 开启事务
multi
set name lisi
set name wangwu
# 提交事务
exec

redis的事务具备原子性么?

set name zhangsan
multi
set name lisi
# 原子性的事务将会回滚
incr name #错误的命令
exec
# 如果具备原子性,则name还是之前的值,如果不是则说明事务没有进行回滚,不具备原子性。
get name 

※:redis的事务只满足事务的隔离型,不保证原子性

watch指令

watch指令一般都是搭配multi一起使用的,用来监控一个变量在事务执行过程中,是否被外界修改了,如果修改的话,exec将会执行失败,watch要在事务开启之前执行

watch name
multi
exec

redis为什么不支持原子性(回滚)

只有当被调用的Redis命令有语法错误时,这条命令才会执行失败(在将这个命令放入事务队列期间,Redis能够发现此类问题),或者对某个键执行不符合其数据类型的操作:实际上,这就意味着只有程序错误才会导致Redis命令执行失败,这种错误很有可能在程序开发期间发现,一般很少在生产环境发现。
Redis已经在系统内部进行功能简化,这样可以确保更快的运行速度,因为Redis不需要事务回滚的能力

六、Redis管道

当我们使用客户端对redis进行一次操作的时候,中间的过程是什么样的?
image
当我们使用客户端对redis进行多次操作的时候,中间的过程是什么样的?
image

进行一次操作,消耗一个网络来回,多个操作消耗多个网络来回,是不是有点耗费时间?

我们可以将过程转换成下图

image

过程优化之后如下图

image

两个连续的写操作和两个连续的读操作,只需要花费一次网络来回,这就是管道的本质,客户端通过改变指令的顺序就可以大幅节省IO时间

管道压力测试指令 -P 表示管道内并行的请求数量

redis-benchmark -t set -P 200 -q

管道的本质

  1. 客户端调用write将消息写到操作系统内核为套接字分配的发送缓冲中sendbuffer
  2. 系统内核将缓冲区的内容发送到网卡,网卡硬件将数据通过路由送到服务器的网卡
  3. 服务器网卡将数据放到内核为套接字分配的接收缓冲中recivebuffer
  4. 服务器掉哟个read从接收缓冲中取出消息进行处理
  5. 服务器调用write将响应内容发送到sendbffer中
  6. 服务器内核将缓冲区的内容通过路由发送到客户端的网卡中
  7. 客户端内核将网卡中的数据放到接收缓冲中recivebuffer
  8. 客户端调用read从缓冲中读取数据

我们开始以为write方法必须到数据发送给对方后才能返回,其实只要将数据放到缓冲区中就可以返回,如果缓冲区满了,则需要等待缓冲区空闲,read方法只有检测不到缓存中有数据才会一直在那等待

七、Redis的订阅

消息多播

消息多播允许生产者只产生一次消息,由中间件负责将消息复制到多个消息队列,每个消息队列由相应的消费组进行消费,,他是分布式系统常用的一种解耦方式

image

基本指令

subscribe  channel[主题名] ....
publish channel[主题名] messgae【消息

※:redis5.0中新增了stream数据结构,支持消息队列的持久化

八、redis主从同步

CAP原理

CAP原理就好比分布式领域的牛顿定律,他是分布式存储的理论基石。
C:Consistent,一致性
A:Availabilty,可用性
P:Partition tolerance,分区容错性
分布式系统的节点往往都是分布在不同的机器上,进行网络隔离的。这意味着必然有网络断开的风险,这个网络断开的名字叫网络分区。

在网络分区发生的时候,两个节点无法进行通信,我们对一个节点进行操作,将无法同步到另外的机器上,所以数据的一致性无法得到保证,除非我们牺牲可用性,在网络分区发生时,将不再提供对外的服务,直到网络恢复。

用一句话概括cap原理就是:当网络分区发生时,一致性和可用性难两全。

最终一致

Redis的主从数据是异步同步的,所以分布式的Redis系统并不满足一致性要求,当客户端在redis的祝节点修改了数据后,立即返回,即使在主从断开,主节点依然可以对外提供服务。所以redis满足可用性。
Redis保持最终一致性,从节点在网络恢复后,努力追赶主节点,最终从节点和主节点状态保持一致,所以redis是保持最终一致性

主从同步与从从同步

redis同步支持主从同步和从从同步,从从同步功能是redis后续版本增加的功能,以减轻主节点的同步负担。image

增量同步

redis同步的是指令流,主节点会将那些对自己的状态产生修改的指令记录在本地的内存buffer中,然后异步将buffer中的指令同步到从节点,从节点一边同步,一边向主节点反馈自己同步到哪里了(偏移量)
因为buffer是有限的,所以redis不能将所有的节点保存在buffer中,redis复制的内存buffer是一个定长的环形数组,如果数组满了,会从头开始覆盖,这时候就需要进行快照同步了。

快照同步

快照同步是一个非常消耗资源的操作,他首先需要在主节点上进行一次bgsave,将当前内存的数据全部快照到磁盘文件中,然后再将快照文件发送给从节点,从节点接收完毕,立即执行一次全量加载,加载之前需要将当前内存的数据清空,加载完毕后,再通知主节点进行增量同步。
如果在执行全量同步的时候,新增加的指令被覆盖了,有需要再次进行全量同步,若如此下去会造成死循环,所以需要设置一个合理的buffer大小参数。image

无盘复制

当进行快照复制的时候,会将内存中的内容保存到文件中,这将是一个非常耗时的IO操作,将会影响主节点的服务效率。
所以从2.8.18开始,redis支持无盘复制,主节点直接通过套接字将快照内容发送到从节点,从节点接收到内容后存到磁盘文件中,再进行一次性加载

wait指令

redis的复制是异步进行的,wait指令可以让异步复制变成同步复制,确保系统的强一致性(不严格),wait指令是redis3.0版本以后才出现的

wait 1 0

参数解析:
第一个参数代表同步的从节点数量,第二个参数是等待的时间,以秒为单位;
等待wait之前的数据同步到n个节点,等待时间为n秒,如果第二个参数为0,则代表一直等待

九、redis-Sentinel(哨兵机制)

Redis Sentinel是一个分布式系统,为Redis提供高可用性解决方案。可以在一个架构中运行多个 Sentinel 进程(progress), 这些进程使用流言协议(gossip protocols)来 接收关于主服务器是否下线的信息, 并使用投票协议(agreement protocols)来决定是否执行自动故 障迁移, 以及选择哪个从服务器作为新的主服务器。

工作过程

Sentinel负责持续监控主从节点的健康,当主节点挂掉时,自动选择一个最优的从节点切换成为主节点。客户端来连接集群时,会首先连接Sentinel,通过Sentinel来查询主节点的地址,然后再连接主节点进行数据交互。当主节点发生故障时,客户端会重新向Sentinel要地址,Sentinel会将最新的主节点告诉客户端,如此应用程序无需重启即可自动完成节点切换。

宕机前
宕机的master下线
宕机的master上线后

消息丢失

Redis采用异步复制,意味着当主节点挂掉时,从节点可能没有收到全部的同步消息,这部分未同步的消息丢失了,如果主从延迟特别大,那么丢失的消息就会特别多。Sentinel无法保证消息不丢失,但是也能尽量保证消息少丢失,提供了如下两个参数
min-slaves-to-write 1
min-slaves-max-lag 10
第一个参数表示主节点必须至少有一个从节点在进行正常复制,否则就停止对外写服务,丧失可用性。
何为正常服务?何为异常服务?这是由第二个参数决定的,它的单位是秒,如果在10秒以内,没收到从节点反馈,就意味着不正常。

Sentinel 实战

  • 环境配置

第一:准备3台服务器,此处我的sentinel就直接放在原先的服务器上,关闭防火墙
systemctl stop firewalld.service

主机说明 主机IP 端口 sentinel端口
master 172.16.51.128 6379 26379
slave 172.16.51.129 6379 26379
slave 172.16.51.130 6379 26379
  • 修改redis.conf。先配置redis主从
172.16.51.128(master)
bind 0.0.0.0
protected-mode no
172.16.51.129(slave)
bind 0.0.0.0
protected-mode no
slaveof 172.16.51.127 6379
172.16.51.30(slave)
bind 0.0.0.0
protected-mode no
slaveof 172.16.51.127 6379
依次启动三个节点
  • 配置sentinel文件
172.16.51.128(master)
bind 0.0.0.0
protected-mode no
# 主redis 节点信息
sentinel monitor mymaster 172.16.51.127 6379 2
172.16.51.129(slave)
bind 0.0.0.0
protected-mode no
sentinel monitor mymaster 172.16.51.127 6379 2
172.16.51.30(slave)
bind 0.0.0.0
protected-mode no
sentinel monitor mymaster 172.16.51.127 6379 2	
依次启动三个节点,然后随机关闭redis master。看看会不会重新选择主节点

Sentinel基本原理

  • 监控阶段

用于同步各个节点的状态信息
获取sentinel的状态(是否在线)
获取master状态
获取所有slave状态
master保存sentinel信息,作用是让其他sentinel来发现其他的sentinel,建立订阅通道
通知阶段
每个sentinel进行互通

  • 故障转移阶段
sentinel发现master关挂了,标记master为主观下线,然后发送命令给奇特哨兵,其他哨兵也会去看看状态,如果一半以上的哨兵认为master挂了,那就直接标记为客观下线,执行故障转移操作
sentinel通过竞选,然后获得此次处理master的权利,成为领头sentinel
    去除不在线的
    去除响应慢的
    与原master断开时间久的
    优先原则
    优先级
    offset
    runid
发送指令
向新的master发送slave no one指令
向其他slave发送slaveof新的ip端口

十、redis集群

Codis

Codis使用Go语言开发,它是一个代理中间件,和Redis一样也使用Redis协议对外提供服务,当客户端向Codis发送指令时,Codis负责将指令转发到后面的Redis实例来执行,并将结果返回给客户端
image

  • Codis分片原理

Codis默认将所有的key划分为1024个槽位(slot),它首先对客户端传进来的key进行crc32运算计算hash值,再将hash值对1024取模得到一个余数,这个余数就是对应的槽位
如图每个槽位都会对应多个redis实例之一,Codis在内存中维护槽位和
redis实例的映射关系。
槽位默认数量是1024,它是可以设置的。
image

  • 不同的Codis实例之间槽位关系如何同步?

Codis将槽位关系存储在zookeeper中,并且提供了一个dashboard可以用来观察和修改槽位关系,当槽位关系发生变化时,Codis Proxy会监听到变化并重新同步槽位,从而实现多个Codis proxy
之间共享相同的槽位关系。image

  • java集成Codis
1.安装Go环境
解压go1.14.4.linux-amd64.tar.gz 到 /usr/local
2.设置编译环境变量
export PATH=$PATH:/usr/local/go/bin
export GOPATH=/usr/local/codis/gopath
export PATH=$PATH:$GOPATH/bin
source /etc/profile(刷新环境变量)
3.下载Codis源码
mkdir -p $GOPATH/src/github.com/CodisLabs
cd $_ && git clone https://github.com/CodisLabs/codis.git -b release3.2
4.编译 Codis 源代码
cd $GOPATH/src/github.com/CodisLabs/codis
make
5.修改默认配置文件

1.修改dashboard.toml
# Set Coordinator, only accept "zookeeper" & "etcd"
coordinator_name = "zookeeper"
coordinator_addr = "127.0.0.1:2181"
# Set Codis Product {Name/Auth}.
product_name = "codis-demo"
product_auth = "123456"
# Set bind address for admin(rpc), tcp only.
admin_addr = "0.0.0.0:18080"


2.修改proxy.toml
# Set Codis Product Name/Auth.
product_name = "codis-demo"
product_auth = "123456"
# Set auth for client session
#   1. product_auth is used for auth validation among codis-dashboard,
#      codis-proxy and codis-server.
#   2. session_auth is different from product_auth, it requires clients
#      to issue AUTH <PASSWORD> before processing any other commands.
session_auth = ""
# Set bind address for admin(rpc), tcp only.
admin_addr = "0.0.0.0:11080"


3.配置多个codis-server
protected-mode no
port 6379
daemonize yes
pidfile "/tmp/redis_6379.pid"
logfile "/tmp/redis_6379.log"
requirepass 123456
cp redis.conf redis-6379.conf
cp redis.conf redis-6378.conf
cp redis.conf redis-6377.conf
sed -i 's/6379/6377/g' redis-6377.conf
sed -i 's/6379/6378/g' redis-6378.conf

4.启动
_x0008_启动Codis Dashboard
nohup ../bin/codis-dashboard --ncpu=4 --config=dashboard.toml  --log=dashboard.log --log-level=WARN &
启动Codis Proxy
nohup ../bin/codis-proxy --ncpu=4 --config=proxy.toml  --log=proxy.log --log-level=WARN &
启动Codis Server
  nohup ../bin/codis-server redis-6377.conf &
  nohup ../bin/codis-server redis-6378.conf &
  nohup ../bin/codis-server redis-6379.conf &
启动Codis FE
 nohup ./bin/codis-fe --ncpu=4 --log=fe.log --log-level=WARN  --zookeeper=127.0.0.1:2181 --listen=0.0.0.0:8081 &


进入管理页面后添加proxy和server



使用jedis连接proxy
 public void testCodis(){
        // 连接池配置
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(50);
        config.setMaxIdle(50);
        config.setMinIdle(1);
        config.setMaxWaitMillis(30000);
        // 创建连接池,ip端口为codis-proxy的ip端口
        JedisPool jedisPool = new JedisPool(config, "172.16.51.127", 19000);
        // 从连接池中获取连接
        try (Jedis jedis = jedisPool.getResource()) {
            jedis.set("foo1", "bar1");
            String value = jedis.get("foo1");
            System.out.println(value);
        }
    }

什么是Cluster ?

Redis Cluster将所有的数据划分为16384个槽,它比Codis的1024个槽位划分的还要细,每个节点负责其中一部。分槽位,槽位的信息存储于每个节点中,它不同于Codis,不需要另外的分布式存储空间来存储节点槽位信息。
当Redis客户端来连接集群的时候,也会得到一份集群的槽位配置信息,这样当客户要找某个key时,可以直接定位到目标节点。这一点不同于Codis.Codis需要通过Proxy来定位目标节点,RedisCluster则直接定位。
image

槽位定位算法

Redis Cluster默认会对key值使用crc16算法进行hash,得到一个整数值,然后用这个整数值对16384进行取模来得到具体槽位。
Redis Cluster还允许用户强制把某个key挂在特定槽位上,通过在key字符串里面潜入tag标记,这样就可以强制把key存入到指定的槽位上

跳转

当客户端像一个错误的节点发出了指令后,该节点会发现指令的key所在的槽位并不归自己管理,这时候他会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端去这个节点获取数据。

-MOVED 3999 127.0.0.1:1911
#第一个参数代表槽位,后面代表地址

迁移

Redis迁移的单位是槽,Redis一个槽一个槽进行迁移,当一个槽正在迁移时,这个槽就处于中间过渡状态,这个槽在原节点的状态位migrating,在目标节点的状态为importing。image

为什么在B请求的时候,需要先发送一个asking指令而不是直接获取key呢?
因为在迁移没有完成之前,按理说这个槽位还是不归新节点管理,如果这时候向目标节点发送该槽位的指令,节点是不认的,它会向客户端返回一个跳转到原节点的指令,如此就会造成重定向循环,ASKing指令就是打开目标节点的选项,告诉他下一条指令不能不理,而要当成自己的槽位来处理

容错

Redis Cluster可以为每个主节点设置若干个从节点,当主节点发生故障的时候,集群会自动将其中某个从节点提升为主节点,如果某个主节点没有从节点,那么发生故障时,集群将会完全不可用,不过Redis提供了一个参数cluster-require-full-coverage可以允许部分节点发生故障,其他节点还可以继续对外提供服务

十一、单线程的redis为何如此快?

因为redis所有的数据都是存放到内存中,所有的运算都是内存级别的,所以是非常快的,但是因为redis是单线程的,所以要小心使用redis指令,对于那些复杂度特别高的指令谨慎使用,否则会导致redis卡顿。
既然redis是单线程,为何能处理那么高的并发呢?因为它采用的是多路复用技术。

非阻塞IO

当我们调用套接字的时候,默认是阻塞的,比如当我们使用read方法的时候,我们会传递一个n,表示最多读取到n个字节再返回,如果一个字都没有读到,则一直阻塞,直到有新的数据到来或者连接关闭,read才返回,线程才能继续处理,write方法一般不会阻塞,除非缓冲区满了,才会阻塞等到有空闲的空间。

事件轮训(多路复用)

非阻塞有一个问题,就是线程要读数据,结果读了一部分就返回了,那么程序如何知道应该继续读取呢,也就是当数据到来的时候,线程如何收到通知,写也是一样的。
事件轮询API就是解决这个问题的,最简单的事件轮询API是select函数,输入是读写的文件描述符列表,输出是与之对应的可读可写事件,同时还提供了一个timeout参数,如果没有任何事件到来,那么就最多等待timeout,线程处于阻塞状态,一旦期间有任何事件到来,就可以立即返回,事件处理完成继续轮询。
image

指令队列

redis会将每个客户端套接字关联一个指令队列,客户端的指令通过队列排列顺序处理,先到先服务。

定时任务

当线程阻塞在select系统调用上,定时任务是怎么得到准时调用的呢?
定时任务是被记录在一个被成为做小堆的数据结构中,在这个堆中最快要执行的任务排在堆的最上方,在每个循环周期中,redis都会堆最小堆里面的到时间点的任务进行处理,处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是select系统调用的timeout参数.

十二、redis的过期策略

定期删除

策略

每100ms随机抽查删除过期数据

缺点

可能残留大量没有抽查过的数据

惰性删除

策略

每次访问数据时,先判断是否过期,过期则删除

缺点

可能残留大量用户没有访问过的过期数据

内存淘汰机制

noeviction

当内存不足写入新数据时,直接报错

allkeys-lru

当内存不足写入新数据时,移除最近最少使用的key

allkeys-random

当内存不足写入新数据时,随机移除key

volatile-lru

当内存不足写入新数据时,在设置了过期时间的key里面, 移除最近最少使用的key

volatile-random

当内存不足写入新数据时,在设置了过期时间的key里面, 随机移除key

volatile-ttl

当内存不足写入新数据时,在设置了过期时间的key里面, 移除即将要过期的key

十三、Redis实现分布式锁

为什么要使用分布式锁?

我们在多线程开发过程中,肯定没避免不了使用锁,jdk中也提供了大量的锁功能,但是我们为什么还要手动开发一个分布式锁呢,原因在于我们在传统项目中使用的锁实在同一个进程中的,他们能够相互访问到彼此的资源信息,但是在分布式中,每个项目都是跑在不同的进程中的,他们无法共享资源信息,所以就需要一个能够在不同进程之间进行“通信”的第三方来实现这个功能,那么redis其实就具备这种功能的。

redis实现分布式锁的原理

其实redis实现的原理主要就是某个线程现在redis里面占个坑,然后后面的人进来的时候看见这个坑被占用的话,就一直等待别人释放这个坑或者放弃,释放之后,他再去抢占。

分布式锁的简单实现

#抢占一个坑,使用setnx指令,如果别人创建过,则设置失败,即对应获取锁失败
setnx lock:user_yang true#实现我们的业务逻辑,逻辑处理完之后,调用del指令释放锁
#如果抢占锁的线程挂掉了,锁就会一直不能释放,就会造成资源一直被占用,所以可以设置一个超期时间,到点自动释放锁
expire lock:user_yang 10
del lock:user_yang

问题: 如果在设置锁和设置超时时间中间出现问题了,没有设置上超时怎么办,也会无法释放锁?是不是只要保证原子性操作就好了。

为了解决上面的问题,redis2.8中,作者加入了set命令的扩展操作,使得setnx和expire可以一起执行,保证他们的原子操作。

加强版分布式锁

set lock:user_yang true ex 5 nx
...
del lock:user_yang

超时问题

超时问题指的是当我们业务代码执行的时间超过了设置锁的时间,就会导致锁释放被其他线程占用,就会导致临界区的代码不能得到严格的执行,而且当主业务代码执行后,就会去释放这个锁,这个会导致将别人的锁释放,又会引发一系列的问题。

  • 解决方案

在设置锁得时候,随机塞入一个值,删除锁之前先比较这个值,如果和之前设置的值相同,则删除,否则不删除。

  • 原子性问题

因为比较和删除不是原子性操作,会不会引发新的问题,但是redis有没有提供这样的原子性操作指令

  • 解决方案2

使用LUA脚本

# delifequals
if redis.call(“get”,KEYS[1]) == ARGV[1] then
	return redis.call(“del”,KEYS[1])
else
	return 0
end

java实现分布式锁

  • 引入依赖
 <dependency>
     <groupId>redis.clients</groupId>
     <artifactId>jedis</artifactId>
     <version>2.9.0</version>
 </dependency>
  • 公用常量
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 锁
     * @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));
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
  • 客户端演示
 @Test
    public void getLock(){
        Jedis jedis = new Jedis("172.16.51.129",6379);
        String requestId = UUID.randomUUID().toString();
        boolean boolSuccess = RedisUtils.tryGetDistributedLock(jedis, "lock:user_yang",requestId, 10);
        if(boolSuccess){
            System.out.println("成功获取锁");
        }else{
            System.out.println("获取锁失败");
        }
        boolSuccess = RedisUtils.tryGetDistributedLock(jedis, "lock:user_yang", requestId, 10);
        if(boolSuccess){
            System.out.println("成功获取锁");
        }else{
            System.out.println("获取锁失败");
        }
        boolSuccess = RedisUtils.releaseDistributedLock(jedis, "lock:user_yang", requestId);
        if(boolSuccess){
            System.out.println("释放锁成功");
        }else{
            System.out.println("释放锁失败");
        }
    }

十四、缓存雪崩、缓存穿透、缓存击穿

缓存穿透

用户请求的key再数据库中并不存在,是一个违法的key,导致每次都从数据库中查找,从而可能压垮数据库。如果黑客利用大量的违法key进行请求,将会导致数据库瞬间崩塌

解决方案

可以使用redis的布隆过滤器,将所有可能的key存放到bitmap中,一个一定不存在的key将会被布隆过滤器拦截掉,从而截断对数据库的访问,如果key没有被布隆过滤器拦截下来,查询库中,没有返回结果,我们可以将一个空值存放到缓存中,缓存时间不宜过长。

缓存击穿

某个被高频访问的key在某一时间点过期,这时出现大量对这个key的访问,这时候缓存中又没有数据,导致同时去库中查询数据,造成数据库压力瞬间增大。

解决方案

  • 设置互斥锁
  • 设置key永不过期(不推荐)

缓存雪崩

指大批量的key在同一时刻过期,导致数据库查询压力瞬间增大,与缓存击穿不同的是,缓存雪崩是大批量的key同时过期。

解决方案

  • 缓存key的过期时间设置为随机
  • 设置数据用不过期

总结

花了三周的时间将《Redis深度历险》研读完,又花了一周的时间,将自己的笔记和理解整理成了这篇博文,通过这一个月的学习与总结,让自己对redis有了一个更深层次的理解,并把自己的总结的知识分享给有需要的人,文中如果讲叙有不对的地方,希望在评论区留言,感谢,下一阶段将会拆分解读《高性能mysql》这一红宝书,这个应该会需要更长的时间,如果想要提前学习的话,可以关注我的公众号领取。


微信搜一搜【AI码师】关注帅气的我,回复【干货领取】领取2021最新面试资料一份

猜你喜欢

转载自blog.csdn.net/weixin_34311210/article/details/107678100