Redis入门和使用实践v2018

Redis入门和使用实践v2018

  • 软件版本

Redis版本:redis-5.0.5.tar.gz

Linux发行版:CentOS-7-x86_64-DVD-1804.iso

Linux基础操作:https://blog.csdn.net/u011424614/article/details/94555916

  • 标识符说明

“ #-- ” 表示注释

“ # ” 表示终端命令

“ > " 表示 redis-cli 命令

一、数据结构分析

1.分布式缓存技术的应用

  • CPU和主内存之间的高速缓存,CPU之间读取高速缓存的指令,提高CPU的执行效率
  • 数据结构如何选择?如何保证数据的一致性?如果保存缓存的高可用?如何保存热点数据?
  • 缓存经常出现,如前端的浏览器缓存,网络的CDN缓存,后端的应用缓存,数据库的缓存,CPU的缓存
  • 应用场景:
    • 项目初期,单应用+单数据库就可以支撑
    • 当运营到一定程度,数据量暴增,这时应用系统查询就会很慢(读写分离,SSD),比如电商网站,数据量是几何递增的,这时数据库无法满足日常的吞吐性能
    • 解决方法:除了分库分表外,是可以将热点数据存储到内存中,作为缓存数据(80%的时间都在访问20%的热点数据)
    • 内存缓存中间件:减少机器成本,增加系统效率
    • 流程:应用请求数据时,先经过Redis查询缓存数据,如果没有数据,再查询数据库,在数据库中查询到数据库,除了返回数据给应用,还会回写给Redis,后续相同的查询,可以直接查询Redis
    • 写入Redis策略:1.插入数据时,写入Redis;2.查询数据时,写入Redis;3.定时同步数据到Redis

2.Redis的魅力

  • Redis 是一个key/value的内存缓存数据库

  • 结构:

    • Redis 实例 -> DB(默认16个;0 - 15;类似命名空间,未完全隔离)-> key -> value (不同的数据结构)
      • string:字符串
      • list:列表
      • hash:散列,类似Java的hashmap
      • set:集合
      • sorted-set:有序集合
    • 1.相对于 memcache ,Redis 提供了更加丰富的数据结构
    • 2.支持数据的持久化
    • 3.基于 redis 的数据结构自定义功能
    • 4.支持发布订阅模式

3.Redis安装指引

# cd /home/user1/下载
# cp redis-5.0.5.tar.gz /opt/redis/
# cd /opt/redis/
# tar -xvzf cp redis-5.0.5.tar.gz

# yum install gcc
# cd redis-5.0.5

#-- 第一种安装方式
# make & make install
# cd src

#-- 第二种安装方式
# make MALLOC=libc
# make test
# cd src && make install

#-- 启动服务端和客户端
# ./redis-server
# ./redis-cli

#-- 需要配置文件可以拷贝 redis根目录下的 redis.conf 文件到 src
  • Windows
  1. 下载:https://github.com/MicrosoftArchive/redis/releases

  2. 将 Redis 添加到服务的命令:redis-server --server-install redis.windows.conf
    –loglevel verbose

4.Redis的数据类型、底层数据结构简述及常用命令

Redis命令:http://redisdoc.com/index.html

  • 针对不同的数据结构,执行不同的命令
  • key命名规范,方便定位问题,防止命名重叠,导致数据错误
#-- 查看redis实例的有哪些key(不建议生产系统使用,会阻塞redis实例)
> keys *
#-- 查看 key 对应 value 的数据类型
> type [key]
#-- 查询 key 是否存在
> exists [key]
# -- 删除 key
> del [key]
1)字符串类型(string)
  • 应用场景:session统一存储、ip限制(incr 原子递增)

  • 结构:key / value

  • 可存储类型:字符串、整数、浮点

  • Redis 内部数据结构:

    • src/sds.c(C语言)

    • int / SDS (simple dynamic string,可修改的字符串,采用预分配冗余空间的方式来减少内存的频繁分配),整数使用int存储,字符串和浮点使用sds存储

    • 符合二进制安全和任意长度(根据数据长度自动选择数据格式, sdshdr8的长度是 28-1,sdshdr16的长度是216-1,一共5种类型)(sdshdr8:len、alloc、flag、buf[]),字符串结尾 ’ \0 ’

    • sdsnewlen 方法,根据数据长度,判断出结构类型

#-- 添加值
> set [key] [value]
#-- 获取值
> get [key]
2)列表类型(list)
  • 应用场景:消息队列(生产者 lpush数据到 redis 中,消费者使用 brpop 获取数据 - FIFO)
  • 结构:key / [value, value, value] 双向链表(从两边获取或添加元素)
  • 可存储类型:有序的字符串列表
  • Redis 内部数据结构:
    • Redis 3.2 之前(linkedlist [存储数据复杂,便于插入和查询] 和 ziplist [存储简单数据,带压缩特性])
    • Redis 3.2 之后(quicklist [结合 linkedlist 和 ziplist 的特性,由ziplist组成的双向链, 每一个节点都是ziplist])
    • src/quicklist.c
    • typedef struct quicklist{quicklistNode *head; quicklistNode *tail ;…}
    • typedef struct quicklistNode{quicklistNode *prev; quicklistNode *next ; char *zl ; …}
    • quicklistNode中两种数据结构:ziplist(压缩) 和 quicklistLZF(非压缩)
#-- 左添加
> lpush mylist
#-- 右添加
> rpush mylist
#-- 左获取数据,并删除
> lpop mylist
#-- 右获取数据,并删除
> rpop mylist
#-- 从左边开始,查询范围内的值
> lrange mylist 0 -1
#-- 同时添加多个数据
> lpush 1 2 3 4
3)散列类型(hash)
  • 应用场景:存储对象信息
  • 结构:key / {field:value, field:value}(类似数据库的表结构)
  • Redis 内部数据结构:
    • hashtable 和 ziplist
    • src/dict.h
    • typedef struct dict{} / dictEntry{}
    • rehash 扩容,将 dictht[0] 的 dictEntry(0-3) 数据迁移到 dictht[1] 中
#-- 添加数据
> hset userinfo name=zcheng
> hset userinfo age=18
#-- 获取key中的数据
> hget userinfo name
#-- 批量添加数据
> hmset [key] [field] [value] [field] [value]
#-- 获取key中的所有数据
> hgetall userinfo
#-- 查询是否存在字段
> hexists userinfo birthday
4)集合类型(set)
  • 应用场景:标签(key为用户名称,value为各种标签)
  • 结构:key / [value, value, value]
  • 特性:无序不重复
  • Redis 内部数据结构:
    • intset 和 hashtable,int 数据使用 intset 存储,其它数据使用 hashtable(key / value) 的 key 进行存储, value设置为null,key 是不允许重复的;inset 把整数数据按顺序存储,并且使用二分法来,降低时间复杂度,来进行查询
    • src/intset.h
#-- 添加数据
> sadd website "csdn.com" "baidu.com"
#-- 获取数据
> smembers website

> deff [key]
5)有序集合(sortedset)
  • 应用场景:根据点击量排序文章或新闻
  • 结构:key / (score, value)
  • Redis 内部数据结构:
    • ziplist 或 skiplist + hashtable
    • skiplist 是有序链表
    • src/t_zset.c
    • int zslRandomLevel(void){}
    • 随机分层 level(1-32),比较对应分层中的数据,逐层对比完成排序,再连接同层前后的关系(一层一层的比较查询,同层的数据不一定是按顺序排序的,跳跃表)(类似数据分片,无路由机制,只能跳跃)
    • [外链图片转存失败(img-FBxsuYT9-1567219901236)(.\assets\img001.png)]
#-- 设置值,搜索引擎的分数
> zadd page_rank 9 google.com 8 baidu.com 7 bing.com
#-- 获取值,google.com 的分数
> zscore page_rank google.com
#-- 获取范围内的数据
> zrange page_rank 0 3
> zrange page_rank 0 3 withscores
#-- 排序,上面是升序,下面是降序
> zrevrange page_rank 0 3 withscores

二、内部原理揭秘

1.过期时间设置及原理分析

  • 一般情况下,我们都是做缓存、验证码、限时优惠等数据的存储,再这些情况下,可以使用 redis 的过期时间设置,当时间过期后,redis 自动删除 key
  • 官网文档:https://redis.io/commands/expire
#-- 设置超时时间
> expire [key] [seconds]
> setex(string key, int seconds, string value)
#-- 查询 key 的过期时间变化(-2 表示过期, -1 未设置过期时间)
> ttl [key]
#-- 取消过期时间设置
> persist [key]
  • 原理:

    • 消极方法(passive way):当应用访问 key 时,发现 key 是过期的,这时才删除 key

    • 积极方法(active way):周期性的检测设置可过期时间的 key,检测到 key 过期时,就会删除一部分,这里只删除一部分是因为,检测的 key 是随机选择的,并不会检测全部

    • 1.随机检测 20 个带有 timeout 标识的 key;2.如果超过 25% 的key,被删除,则重复执行整个流程

    • How Redis expires keys

      Redis keys are expired in two ways: a passive way, and an active way.

      A key is passively expired simply when some client tries to access it, and the key is found to be timed out.

      Of course this is not enough as there are expired keys that will never be accessed again. These keys should be expired anyway, so periodically Redis tests a few keys at random among keys with an expire set. All the keys that are already expired are deleted from the keyspace.

      Specifically this is what Redis does 10 times per second:

      1. Test 20 random keys from the set of keys with an associated expire.
      2. Delete all the keys found expired.
      3. If more than 25% of keys were expired, start again from step 1.

2.发布订阅模式

  • pub / sub 模式
  • producer --> channel(名称:全名称匹配、正则匹配) <-- consumer
#-- 通过频道发布消息
> publish channel.hello world
#-- 订阅频道获取消息
> subscribe channel.hello
  • 如果需要支持多协议、持久化、数据回滚、可靠性的保障等特性,就需要专门的消息中间件才能实现,而 redis 是不支持的

3.Redis持久化及原理分析

  • 把 redis 当成 NoSQL 来使用

  • 当 redis 在分布式下大规模应用时,防止由于缓存失效,大量的请求直接打到数据库上,导致数据库无法承受(雪崩效应)

  • RDB

    • 快照:当符合条件的时候,fork子进程,把数据备份到临时文件,持久化完成后,会生成一个快照文件 dump.rdb;如果以及存在快照文件,会对快照文件进行覆盖

    • 由于是fork子进程进行操作,所以对主进程的数据操作不会产生影响

    • 四种条件:

    • 1.配置规则

      • redis.conf 搜索 /save
      #-- save [seconds] [changes] 在900秒内,如果有1个更改,就会触发一次快照
      #-- 这里的三个快照是 或 的关系,只有满足配置就会触发
      save 900 1
      save 300 10
      save 60 10000
      
    • 2.主动执行 save 命令 (会阻塞所有客户端请求) 或者 bgsave 命令 (异步执行,不会阻塞客户端请求)

      > save
      > bgsave
      
    • 3.flushall (清空所有内存数据,如果配置规则存在,则执行一次快照操作)

    • 4.执行复制操作 (主从数据的复制,完成数据库同步)

    • 缺点:在触发下一次快照期间产生了数据,如果这时 redis 服务停止运行,会导致这期间的数据丢失,而且,fork 子进程会消耗服务器性能

  • AOF

    • 日志:实时的将 redis 的变更数据操作进行备份

    • 启动 AOF 的方式:redis.conf 搜索 /append

      #-- 修改 appendonly no
      appendonly yes
      
    • 停止 redis 服务端,重新启动

      # ./redis-server redis.conf
      
    • AOF 启动后,默认会从 AOF 恢复数据和持久化数据;可以通过 vim 指令查看文件内容

      # vim appendonly.aof
      
    • 问题:如果操作过多的话,会导致AOF文件越来越大

    • 解决方法:redis.conf 搜索 /auto

      #-- 当前AOF文件超过上一次AOF文件的百分之几时,进行重写
      auto-aof-rewrite-percentage 100
      #-- 如果小于设置值时,没有必要重写
      auto-aof-rewrite-min-size 64mb
      
    • 在重写时,会fork子进程进行重新,重写时不会对原来的AOF文件进行重写,而是对内存的数据进行重写,如果在重写期间,有新的指令,redis 会把这些指令先加到重写缓存中,内存数据重写完毕后,将重写缓存中的指令追加到重写文件中,既生成新的AOF文件

    • redis.conf 搜索 /everysec

      #-- 操作指令什么时候同步到磁盘文件里面
      #--(由于操作系统有自己的缓存机制,操作指令是不会直接添加到磁盘文件中,而是保存到硬盘缓存)
      #-- 默认每秒同步一次(到磁盘),always表示每次操作都进行同步,no表示不主动同步
      appendfsync everysec
      
    • 缺点:原来只要把数据写入内存就可以,现在还有把变更操作保存到AOF文件,会有性能损耗

    • 两种方式选择:

    • 两种方式都配置,会同时生效(同时兼并两种方式的优点)

    • AOF可以保障数据的安全性(记录每一次变更操作)

    • RDB的数据恢复速度会更快

    • 文件损坏恢复:

    • redis-check-aof、redis-check-rdb

4.Redis的内存回收机制

  • 大多数情况下,redis 的数据都是保存在内存中的,当数据量达到了物理内存的容量时,虽然,操作系统会划分部分磁盘空间来构建虚拟内存,这时,除了而可以增加内存条外,还可以使用内存的回收策略,来淘汰符合条件的数据
  • redis.conf 搜索 /maxmemory-policy
#-- 内存回收策略,noeviction表示当达到最大内存,在申请内存时会报错;redis.conf中配置maxmemory
#-- allkeys-lru 表示从内存库中挑选最少使用的key进行回收
#-- allkeys-random 表示对所有的key进行随机回收
#-- volatile-random 表示从设置了过期时间的key集里面进行随机回收
#-- volatile-lru 表示从设置了过期时间的key集里面,选择最少使用的key进行回收
#-- volatile-ttl 表示挑选即将过期的key进行回收
# maxmemory-policy noeviction
  • LRU算法:由于 redis 有大量的数据,为了保证效率,并不会对每一个数据进行最少使用的判断,而是,使用了随机采样的方式进行判断

5.Redis单线程为什么性能很高

  • redis 的瓶颈不在CPU的多核心资源,主要是内存和网络

  • 同步和异步指用户线程和内核的交互方式,阻塞和非阻塞指用户线程调用内核进行IO操作时产生

  • OSI(Open System Interconnection)七层协议中,传输层以上(不包括传输层)为用户空间,传输层以下(包括传输层)为内核空间

  • 异步阻塞IO(IO多路复用):用户线程请求内核数据时,如果内核空间没有准备好数据,用户线程会启动一个监听,当内核数据准备好了后,通知用户线程的监听(多路:同一个通道多次使用,不用担心阻塞)

  • 问题:单线程redis为什么不能保证原子性?多个客户端同时读取一个数据进行修改时,redis对执行顺序是没有约束的,最终的修改结果是无法预判的

  • 1.redis 使用了多路复用机制(事件机制)

  • 2.redis 中大部分是内存操作

  • 3.单线程避免了线程竞争和上下文切换的开销(缺点:无法利用好CPU资源)(每个机器是可以部署多个redis的,毕竟每个redis只用了一个核心处理任务)

6.Lua脚本在Redis中的应用

  • reids pipline 管道模型:一次执行多个指令,但是,指令之间是没有顺序的
  • 1.减少网络开销,执行多个指令
  • 2.满足原子性
  • 3.满足复用性(复合指令:多个指令组合形成新的指令)
  • 可以使用redis的客户端,编写带业务功能的lua脚本
-- 调用 redis 指令, 添加 string 类型值
redis.call('set','name','zcheng')
-- 获取返回值
local val = redis.call('get','name')
  • redis 使用 eval 指令执行 lua脚本
> eval "return redis.call('get','name')" 0
#-- 设置动态参数
> eval "return redis.call('set',KEYS[1],ARGV[1])" 1 id 123456
  • vim test.lua
return redis.call('get','name') 
  • 运行脚本:./redis-cli --eval test.lua

  • 限制某个 IP 的访问次数

-- 定义 key 变量,'..'表示拼接,KEYS[1]表示第一个KEYS参数
local key = "ratelinit:"..KEYS[1]
-- 限制次数
local limit = tonumber(ARGV[1])
-- 超时时间,秒
local expireTime = ARGV[2]
-- 获取key的执行次数,如果key不存在,会自动创建,并返回1
local times = redis.call('incr',key)
-- 如果第一次进来,需要对key设置过期时间
if times == 1 then
    redis.call('expire',key,expireTime)
end
-- 如果访问次数超过限制次数,则返回0
if times > limit then
    return 0
end
-- 正常访问
return 1
  • 运行脚本(10秒内10次):./redis-cli --eval ratelimit.lua 192.168.11.53 , 10 10

  • (开头的要求1)问题:由于 lua 脚本中可能存在大量的指令和逻辑,当在客户端执行lua脚本时,会将lua脚本通过网络传输到服务端,这个网络传输是非常消耗网络资源的,那么该如何解决?

  • 答案:对lua脚本设置摘要(sha-1算法)

#-- 生成 lua 脚本的摘要,lua 脚本会保存到 redis 的脚本缓存中
> script load "return redis.call('get','name')"
#-- 通过摘要,调用lua脚本,没有参数 0
> evalsha "52da8c7de39385e305fb1af2a8ffd21534af996f" 0
  • 问题:脚本执行时间过长,怎么处理?
#-- 死循环的 lua 脚本(其它客户端执行redis指令时,会提示正在执行脚本,也就是阻塞了)
> eval "while true do end" 0
#-- 终止脚本执行
> script kill
  • (开头的要求2)问题:如何保证原子性?
-- 修改值
redis.call('set','name','zchengl')
while true do end
  • cli-1 运行脚本,然后会阻塞,cli-2 再去运行 get 指令,会提示阻塞,这时如果执行上面的终止指令,会发现行不通了,这是因为redis保证原子性的方式,这里需要执行:
#-- 终止 redis 服务,lua脚本是执行失败的
> shutdown nosave
#-- 需要重新启动 redis 服务

三、分布式Redis

1.Redis主从复制

  • 解决单点故障的问题
  • Redis的解决方案:主从复制(master-slave)
  • 集群遇到的问题:1.一致性问题(数据同步);2.主节点选择(master选举)
  • master节点为写节点,负责接送写操作,slave节点同步master节点数据,负责提供读操作

具体操作

  • 环境:3台虚拟机(IP:192.168.31.52、192.168.31.23、192.168.31.64)
  • 主从机器IP:52 为 master,23 和 64 为 slave
  • 在 slave节点配置replicaof:redis.conf 搜索 /replicaof(由于版本差异,slaveof 变成了 replicaof)
# replicaof <masterip> <masterport>
replicaof 192.168.31.52 6379
  • redis.conf 其它配置(master-slave都要修改,后面哨兵机制需要相互通信)
# bind 127.0.0.1 #(允许其它机器访问)注释掉不修改内容也可以
bind 0.0.0.0

# protected-mode yes #(关闭保护模式)
protected-mode no
  • CentOS 7的防火墙设置开放端口
# firewall-cmd --zone=public --list-ports
# firewall-cmd --zone=public --add-port=6379/tcp --permanent
# firewall-cmd --reload
  • 进入 redis 客户端,查看节点信息

(marster 操作)

# ./redis-cli
> info replication
------------------------ 输出信息
# Replication
role:master
connected_slaves:2
slave0:ip=192.168.31.23,port=6379,state=online,offset=938,lag=0
slave1:ip=192.168.31.64,port=6379,state=online,offset=938,lag=0
master_replid:10f86211304c21ec4862687c30d5b000074e9231
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:938
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:938

(slave 操作)

# ./redis-cli
> info replication
------------------------ 输出信息
# Replication
role:slave
master_host:192.168.31.52
master_port:6379
master_link_status:up
master_last_io_seconds_ago:7
master_sync_in_progress:0
slave_repl_offset:1106
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:10f86211304c21ec4862687c30d5b000074e9231
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1106
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:1106
  • 这时如果在 master 节点添加数据,数据会自动同步到 slave 节点
  • 如果在 slave节点上添加数据,会有相应的错误提示
(error) READONLY You can't write against a read only replica.

原理

  • 1.全量复制:

    • 启动一个 slave 节点后,slave 节点会发送同步命令(SYNC) 到 master 节点,master 节点执行 bgsave 命令生成 rdb 快照,master 节点将 rdb 快照文件返回给 slave 节点,最后 slave 节点加载快照;至于执行 bgsave 指令之后的数据,master 会先添加到缓存,然后,把数据发送给 slave 节点,类似一个增量复制

    • 如果配置了主从复制,就算禁用了 rdb,也会生成 rdb 快照进行数据同步

    • slave 节点开启监听

    > replconf listening-port 6379
    > sync
    
    • marster 节点设置数据
    > set test 123456
    
    • slave 节点就可以查看到同步指令日志 (PING:连接slave和master的心跳包)

SYNC with master, discarding 213 bytes of bulk transfer…
SYNC done. Logging commands from master.
“set”,“test”,“123456”
“PING”


* 问题:master 同步完多少个slave节点后,才会对外提供服务?
* 答案:master 的 redis.conf 配置 `min-slave-to-write 3`  表示最少3个slave节点连接到master节点,master节点才能进行写操作,这时master才会对外服务,`min-slave-max-lag 10` 表示允许slave最长的丢失连接时间,这里如果10秒内master没有接送都salve的心跳包反馈,就会认为slave断开连接



* 2.增量复制

* 问题:当网路延迟,导致数据不一致时,怎么同步数据?
* 答案:增量复制
* 增量复制需要保证,当前同步的位置是上一次同步中断的位置

```terminal
> info replication

#--以下内容记录同步的位置(增量复制出现在redis 2.8以后)
#--(master和slave都会保存,如果slave断开连接后,重新连接,会从offset位置开始同步)
master_repl_offset:1106
repl_backlog_active:1
  • 3.无磁盘复制

    • 问题:全量复制中,生成rdb快照时,如果磁盘性能不高,导致 redis 的性能下降
    • 答案:使用无磁盘复制,不会生成 rdb 快照
    • redis.conf 搜索 /repl-diskless-sync
    #-- no表示不生成磁盘快照,直接数据传输(无磁盘复制出现在redis 2.8以后)
    repl-diskless-sync no
    
  • 选择:(前期)全量复制和(后期)增量复制是同时使用的,而无磁盘复制是可选的

2.哨兵机制

  • 通常中间件集群的高可用:1.内部leader选举,2.依托外部的机制,而redis是第二种,既哨兵机制

  • 作用:1.监控master和slave的运行情况,2.当master出现故障时,从集群的slave中选举一个新的master

  • 为了保证哨兵的高可用,哨兵支持集群,哨兵之间相互监控和感知

    • 哨兵集群内部通过raft协议(算法)进行内部选举,解决多个哨兵监控到 master 节点出现故障,由谁处理
      • raft协议(算法):分布式一致性算法(http://thesecretlivesofdata.com/raft/)
      • 投票,大于一半的节点同意
    • 哨兵之间不需要配置关系,他们通过监控master和slave认识对方(共同好友),使用了节点订阅的概念(pub/sub),既所有的 sentinel 都会订阅 master 节点发布的一个频道(channel:sentinel:hello),新的 sentinel 加入到集群时,会发送消息(当前sentinel节点的IP、端口等信息)到 master 节点,master节点会通知订阅了频道的 sentinel 有新的sentinel 加入到集群了,最后 sentinel 通过ip和端口进行相互的连接和通信

具体操作

  • 找到 ./redis-5.0.5/sentinel.conf 配置文件(复制到src目录)
#-- 端口
port 26379

#-- 监控master节点,IP:对应master的IP,端口默认,
#-- 2表示当master出现故障时,多少个sentinel达成一致,才会认为master是故障的
# sentinel monitor mymaster 127.0.0.1 6379 2
sentinel monitor mymaster 192.168.31.52 6379 1
  • ./redis-5.0.5/sentinel.conf 其它可选配置
#-- 设置 master 停止运行后多久触发
# sentinel down-after-milliseconds <master-name> <milliseconds>(可能需要注意顺序)
sentinel down-after-milliseconds mymaster 5000

#-- 在设置的时间内,master没有复活,触发failover机制,重新选举master
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 15000
  • 运行脚本:./redis-sentinel ./sentinel.conf
  • sentinel启动后,关闭master,到达规定时间后,触发failover机制,重新选举master
#-- sentinel主观认为master停止运行
# +sdown master mymaster 192.168.31.52 6379

#-- sentinel客观认为master停止运行(真正认为停止运行)
# +odown master mymaster 192.168.31.52 6379 #quorum 1/1

# +new-epoch 3
# +try-failover master mymaster 192.168.31.52 6379
# +vote-for-leader a1c63e0f95bbf58660980ff3028ded2b83efbf74 3
# +elected-leader master mymaster 192.168.31.52 6379
# +failover-state-select-slave master mymaster 192.168.31.52 6379
# +selected-slave slave 192.168.31.64:6379 192.168.31.64 6379 @ mymaster 192.168.31.52 6379
* +failover-state-send-slaveof-noone slave 192.168.31.64:6379 192.168.31.64 6379 @ mymaster 192.168.31.52 6379
* +failover-state-wait-promotion slave 192.168.31.64:6379 192.168.31.64 6379 @ mymaster 192.168.31.52 6379
# +promoted-slave slave 192.168.31.64:6379 192.168.31.64 6379 @ mymaster 192.168.31.52 6379
# +failover-state-reconf-slaves master mymaster 192.168.31.52 6379
* +slave-reconf-sent slave 192.168.31.23:6379 192.168.31.23 6379 @ mymaster 192.168.31.52 6379
* +slave-reconf-inprog slave 192.168.31.23:6379 192.168.31.23 6379 @ mymaster 192.168.31.52 6379
* +slave-reconf-done slave 192.168.31.23:6379 192.168.31.23 6379 @ mymaster 192.168.31.52 6379
# +failover-end master mymaster 192.168.31.52 6379
# +switch-master mymaster 192.168.31.52 6379 192.168.31.64 6379
* +slave slave 192.168.31.23:6379 192.168.31.23 6379 @ mymaster 192.168.31.64 6379
* +slave slave 192.168.31.52:6379 192.168.31.52 6379 @ mymaster 192.168.31.64 6379
# +sdown slave 192.168.31.52:6379 192.168.31.52 6379 @ mymaster 192.168.31.64 6379
  • 哨兵选举失败提示:(建议检测:1.redis是否运行网络访问,2.redis是否开启保护模型,3.是否开放防火墙端口)
# Next failover delay: I will not start a failover before Sun Jul 21 15:06:58 2019
  • 之前的master重启后,转换为 slave 节点
* +convert-to-slave slave 192.168.31.52:6379 192.168.31.52 6379 @ mymaster 192.168.31.23 6379

3.Redis-Cluster

  • 问题:每个节点都保存数据,而且数据量大,并无限的存储
  • 答案:数据分片,作用:1.缓解存储压力;2.缓解访问压力
  • 当数据量非常大时,虽然redis的内存存储效率非常高,但是每一种数据结构都存在数据复杂度,数据复杂度体现了在数据量非常大时,依然会存在查询慢的问题
  • 数据分片是按照一定规则,将数据分散到不同的节点上
  • 分片规则(key的路由规则):
    • 一致性hash(客户端和服务端之间做一个访问代理)
    • codis(豌豆荚开发)(在redis源码的基础上,构建了一个代理层,实现了分片路由和动态扩容)
    • redis-cluster:redis的分片集群

[外链图片转存失败(img-TJUe3R1B-1567219901238)(.\assets\img002.jpg)]

  • 最小规模分片集群:6台机器,3个master和3个slave,一个master对应一个slave

  • 最最最小分片集群:3台机器,3个master(没有了热备,降低了集群的可用性)

  • gossip 协议的无中心化节点的集群

  • redis 分片集群通信组件:redis cluster bus

  • 虚拟槽的概念:0 ~ 16383(slot)

    • 假设:部署3个master,slot的分布:05000,500110000,10001~16383,也就是预先做了虚拟槽的分配
    • 假设:现在设置一个数据, set test 123456,这是会使用 CRC16(key)%16383 计算出一个值,表示路由到对应虚拟槽区间的机器上,保证数据可以合理的保存到多个节点上,例如:计算值=1000,表示数据路由到虚拟器区间为0~5000的机器上
  • 问题:做了redis-cluster集群后,如果想把相同业务的数据路由到同一台机器上,需要怎么做?

  • 答案:HashTag,例如:user:{user123}:id,user:{user123}:name,user:{user123}:age,以上数据中,redis会把 {} 内的内容认为是一个HashTag,当执行 CRC16算法时,参数为 HashTag 的内容,从而使得计算值相同,也就是会路由到同一台机器上

  • 问题:假设:name数据保存在一号机器上,如果这时发送get请求到三号机器,redis会怎么处理?

  • 答案:三号机器会返回 MOVED 信息(MOVED <1号机器的IP>:<1号机器的redis端口>),然后在根据MOVED信息进行重定向到1号机器获取数据

  • 问题:3号机器怎么知道数据在1号机器?

  • 答案:查看上面机器结构图可以知道,集群之间是相互通信的

  • 问题:怎么做分片迁移?

  • 答案:分片迁移可能是增加了机器,这时需要解决虚拟槽的分配和数据的迁移

    • 虚拟槽的分配

      • redis提供了一个半自动分配虚拟槽的工具,需要自己执行命令
      • 新增节点后,会从各个节点上取一部分虚拟槽到新的节点上
    • 数据的迁移

      • 假设:现在需要将 master 1的数据迁移到 master 2中,迁移过程中,可能会出现不稳定的状态,所以redis定义了一些规则,通过这些规则约束客户端的行为,使得迁移过程中不需要停机(热过载)
      • master 1发送一个状态变更的请求,master 2接送到请求后,状态变为 IMPORTING ,而 master 1的状态变为 MIGRANTING
        • MIGRANTING (表示slot正在迁移,约束1:如果客户端访问的key还没有迁移出去,则正常处理key;约束2:如果key已经迁移或不存在,则回复ASK信息,跳转到 master 2 去执行)
        • IMPORTING (表示正在迁入,约束1:如果客户端的访问不是从ASK跳转过来的时候,不允许修改,因为数据有可能还没有迁移过来,防止之后出现数据冲突,返回MOVED信息)
  • 企业级分片集群选择:condis (优点:多CPU的支持,动态扩容,分片路由) / twemproxy (优点:成熟的解决方案,缺点:无法动态扩容) / redis-cluster

  • 集群配置(3主6从)

  1. redis安装参考前面章节

安装目录:/opt/redis-cluster/redis-5.0.5

  1. redis根目录下创建存放集群配置信息的文件夹和配置文件
# mkdir cluster
# cd cluster
# mkdir 7000 7001 7002 7003 7004 7005 7006 7007 7008
  1. 复制根目录的 redis.conf 配置文件到 7000 文件夹;为方便查看,重命名 redis-7000.conf
# cp redis.conf cluster/7000/redis-7000.conf
  1. 修改 redis-7000.conf 文件
# vim redis-7000.conf
#-- 注释bind 或改为 0.0.0.0
# bind 127.0.0.1

#-- 取消保护模式,允许运行外部访问
protected-mode no

#-- 修改端口号
port 7000

#-- 启用后台进程模式,server启动后,不占用terminal窗口,也就是不会输出日志信息
daemonize yes

#-- 修改pid file的目录,pid是记录reids对应端口进程的id
pidfile /opt/redis-cluster/redis-5.0.5/cluster/7000/redis-7000.pid

#-- 修改持久化数据文件名称
dbfilename dump-7000.rdb

#-- 修改持久化数据文件保存路径
dir /opt/redis-cluster/redis-5.0.5/cluster/7000

#-- 配置内存回收机制(具体查看前面章节)
maxmemory-policy allkeys-lru

#-- 启动集群模式
cluster-enabled yes

#-- 设置集群配置文件
cluster-config-file nodes-7000.conf
  1. 将 7000 文件夹中的 redis-7000.conf,复制到7000~7008文件夹中,并且将文件名和文件内容,关于 7000的数字改为对应端口的数字
# cp redis-7000.conf ../7001/redis-7001.conf
# cd ../7001
# vim redis-7001.conf

vim替换命令::1,$ s/7000/7001/g 将配置文件中 7000 替换成 7001

  1. 分别启动 redis 端口 7000~7008 的服务
# ./src/redis-server cluster/7000/redis-7000.conf
  • 查看启动情况
# ps -ef | grep redis
  1. 使用 redis-cli 构建集群(注:redis 5.0之前使用 ruby 脚本构建集群)
  • –cluster-replicas 2 表示每个主节点下有2个从节点
# ./src/redis-cli --cluster create --cluster-replicas 2 192.168.31.52:7000 192.168.31.52:7001 192.168.31.52:7002 192.168.31.52:7003 192.168.31.52:7004 192.168.31.52:7005 192.168.31.52:7006 192.168.31.52:7007 192.168.31.52:7008

Can I set the above configuration? (type 'yes' to accept): yes
  • 连接redis服务,查看服务信息
# ./src/redis-cli -p 7000
> info replication

[root@ddd redis-5.0.5]# ./src/redis-cli -p 7000
127.0.0.1:7000> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=192.168.31.52,port=7003,state=online,offset=182,lag=0
slave1:ip=192.168.31.52,port=7006,state=online,offset=182,lag=1
master_replid:fdf779a4eafa7581367e40fdb9557918b72260dd
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:182
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:182
  • 测试将 7000 的主节点进程停止,看是否选举成功
#-- 查看redis 7000 端口的pid
# ps -ef | grep redis
#-- 停止pid进程
# kill -9 <pid>
#-- 连接之前 7000 端口的从节点
# ./src/redis-cli -p 7003

[root@ddd redis-5.0.5]# ./src/redis-cli -p 7003
127.0.0.1:7003> info replication
# Replication
role:slave
master_host:192.168.31.52
master_port:7006
master_link_status:up
master_last_io_seconds_ago:2
master_sync_in_progress:0
slave_repl_offset:700
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:8d331c5876425ca511fd39a8421cbeee167c4123
master_replid2:fdf779a4eafa7581367e40fdb9557918b72260dd
master_repl_offset:700
second_repl_offset:505
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:700
  • 将7000端口节点重新启动后,会自动加入到7006端口的主节点中

  • 查看集群的槽分配

> cluster solt

四、Redis实战

问题1:哨兵模式下,客户端应该连接到那个 redis-server ?

  • 哨兵通过监控master来获取所有节点信息
  • 源码:JedisSentinelPool.class
public JedisSentinelPool(String masterName, Set<String> sentinels, GenericObjectPoolConfig poolConfig, int connectionTimeout, int soTimeout, String password, int database, String clientName) {
    ......
        HostAndPort master = this.initSentinels(sentinels, masterName);
        this.initPool(master);
    }
    private HostAndPort initSentinels(Set<String> sentinels, String masterName) {
        .......
            //根据master名称获取对应的 host and port
            List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
        .......

			//监听master变化,如果master重新选举后,需要重新连接
            while(var5.hasNext()) {
                sentinel = (String)var5.next();
                hap = HostAndPort.parseString(sentinel);
                JedisSentinelPool.MasterListener masterListener = new JedisSentinelPool.MasterListener(masterName, hap.getHost(), hap.getPort());
                masterListener.setDaemon(true);
                this.masterListeners.add(masterListener);
                masterListener.start();
            }
        .......
    }

问题2:集群模式下,为什么会有MOVED的error ?

  • 重现错误
[root@ddd redis-5.0.5]# ./src/redis-cli -p 7000
127.0.0.1:7000> set name zcheng
(error) MOVED 5798 192.168.31.52:7001
  • 错误提示,设置的 key 应该保存到 7001 的端口中
  • 解决方法:
[root@ddd redis-5.0.5]# ./src/redis-cli -c -p 7000
127.0.0.1:7000> set name zcheng
-> Redirected to slot [5798] located at 192.168.31.52:7001
OK
  • ok 前会提示,已经将 key 重定向到 7001 端口中,-c 表示如果key不能保存在当前节点,自动重定向

  • Jedis源码解释

  • 入口

JedisCluster jedisCluster = new JedisCluster(hostAndPort);
  • 通过 JedisCluster 找到:
public BinaryJedisCluster(Set<HostAndPort> jedisClusterNode, int timeout, int maxAttempts, GenericObjectPoolConfig poolConfig) {
        this.connectionHandler = new JedisSlotBasedConnectionHandler(jedisClusterNode, poolConfig, timeout);
        ......
}
  • 通过 JedisSlotBasedConnectionHandler() 找到:
public JedisClusterConnectionHandler(Set<HostAndPort> nodes, GenericObjectPoolConfig poolConfig, int connectionTimeout, int soTimeout, String password, String clientName, boolean ssl, SSLSocketFactory sslSocketFactory, SSLParameters sslParameters, HostnameVerifier hostnameVerifier, JedisClusterHostAndPortMap portMap) {
        .......
        this.initializeSlotsCache(nodes, poolConfig, connectionTimeout, soTimeout, password, clientName, ssl, sslSocketFactory, sslParameters, hostnameVerifier);
    }
  • 通过 initializeSlotsCache() 找到:
    private void initializeSlotsCache(Set<HostAndPort> startNodes, GenericObjectPoolConfig poolConfig, int connectionTimeout, int soTimeout, String password, String clientName, boolean ssl, SSLSocketFactory sslSocketFactory, SSLParameters sslParameters, HostnameVerifier hostnameVerifier) {
		.......

                this.cache.discoverClusterNodesAndSlots(jedis);
        .......

    }
  • 通过 this.cache.discoverClusterNodesAndSlots 找到:
public void discoverClusterNodesAndSlots(Jedis jedis) {
    	//上锁
        this.w.lock();

        try {
            this.reset();
            // 获取cluster的slot信息,为后面的key自动路由做准备
            List<Object> slots = jedis.clusterSlots();
            Iterator var3 = slots.iterator();

            ........
        } finally {
            this.w.unlock();
        }
    }
  • 现在开始设置key和value
jedisCluster.set("name","123456");
  • 通过 set() 找到:
public String set(final String key, final String value) {
        return (String)(new JedisClusterCommand<String>(this.connectionHandler, this.maxAttempts) {
            public String execute(Jedis connection) {
                return connection.set(key, value);
            }
        }).run(key);
    }
  • 通过 run() 找到:
public T run(String key) {
    // CRC16 计算 key 的 slot 值
        return this.runWithRetries(JedisClusterCRC16.getSlot(key), this.maxAttempts, false, (JedisRedirectionException)null);
    }
  • runWithRetries 里面进行slot值的自动路由

1.Redis Java客户端介绍

官网:https://redis.io/clients

  • jedis:使用socket进行命令操作

  • redission:分布式的、可扩展的 Java 数据结构,既可以通过命令操作,同时也实现了基于redis的功能,如分布式锁、队列、原子递增、全局ID

  • lettuce:基于netty构建的可伸缩的、线程安全的redis客户端,支持同步、异步和响应式

  • jedis(错误代码)

public class JedisClientDemo {

    public static void main(String[] args) {
        Set<HostAndPort> set = new HashSet<>();
        set.add(new HostAndPort("192.168.31.52", 7006));
        set.add(new HostAndPort("192.168.31.52", 7000));
        set.add(new HostAndPort("192.168.31.52", 7003));
        //HostAndPort hostAndPort = new HostAndPort("192.168.31.52", 7006);
        JedisCluster jedisCluster = new JedisCluster(set);
        jedisCluster.set("name","123456");
        //JedisSentinelPool jedisSentinelPool = new JedisSentinelPool(null,null);
    }

}
  • redisson(错误代码)
public class RedissonClientDemo {
    public static void main(String[] args) {
        Config config = new Config();
        config.useClusterServers().addNodeAddress("");
        RedissonClient client = Redisson.create(config);
        client.getBucket("").set("");
    }
}

2.基于Redis实现分布式锁

  • 分布式(跨进程、跨节点)的环境中,保证共享资源的安全问题。(扣减库存的例子)
  • 利用 redis setnx 实现(key存在,则返回0,设置失败;反之,key存在,则返回1,设置成功)
  • 锁的特性:获得锁、释放锁、超时时间、判断是否重入(避免其它进程释放锁)

//TODO 将编写的代码上传到 github , jedis操作和redisson操作

3.Redis的管道模式

  • 将多个请求合并成一个进行发送

4.Redis的应用架构

1)缓存在应用中的架构模型
  • 用户请求数据时,先判断缓存数据,有则返回缓存数据,无则查询数据库的数据,如果有数据则回写到缓存

  • 命中率

    • 命中缓存数 / 请求缓存总数
    • 如何提高命中率?
      • 预判热点数据(预热),提前将数据加载到缓存中
2)Redis缓存的数据一致性问题
  • 数据库的数据和缓存中的数据的一致性问题
    • 无法保证强一致性,也没有必要,主要原因是需要保证性能不会下降,缓存本质就是为了提高性能
    • 实现最终一致性
    • 在无法保证强一致性的情况下,需要考虑如何保证缓存的数据和数据库的数据的尽可能的一致?
      • 1.更新缓存的数据?还是让缓存的数据失效?
        • 需要看更新缓存的代价,如果更新缓存的数据,需要非常多的前置条件,如需要判断5个接口的数据,这时就可以让缓存失效,下次请求数据时再回写到缓存;如果更新缓存的前置条件非常简单,则可以之间更新缓存的数据
      • 2.先操作数据库?还是先操作缓存?
        • 无论是先操作数据库,还是先操作缓存,都可能会出现数据不一致的情况,因为一方更新了,另一方都有可能更新失败,这时候需要考虑,哪一种对业务的影响更小
        • 最终一致性方案:通过引入消息中间件,当客户端发起数据更新,当某一方(数据库或缓存)更新数据失败时,将失败消息发送到消息中间件的队列中,客户端可以通过消费消息中间件队列的方式,重新发起更新数据的请求
3)关于缓存在使用过程中需要解决(缓存雪崩、缓存穿透)
  • 缓存雪崩:指缓存中大量的key的超时时间相同,导致同一时刻大量的key同时失效,从而可能导致同一时刻大量的请求直接访问到数据库,而如果这时数据库没有应对措施,就会导致数据库压力过大,使得数据库崩溃

    • 解决方法:
    • 1.缓存中取不到值的时候,加锁(排队),再加载数据库数据,并回写到缓存【性能损耗】
    • 2.设计缓存时,合理分配过期时间,可以监控key的过期时间
  • 如果redis宕机,停止运行?

    • 解决方法:
    • 1.使用高可用方案,如集群
    • 2.使用多级缓存,目的:不同的缓存中间件缓存不同的数据类型,保证某一个宕机后,不会全部缓存失效,如:redis+memcache
  • 缓存穿透:指客户端大量的请求都无法命中缓存的key,既有可能是恶意攻击的请求,导致redis性能下降,或者reids服务崩溃,导致数据库压力加重而崩溃

    • 解决方法:
    • 1.对空值做缓存,输入为空,或查询为空【数据更新不频繁的情况】
    • 2.设置key的规则,如果请求的key不符合自定义规则,则视为恶意请求
    • 3.布隆过滤器

5.认识布隆过滤器

  • 缓存 reids服务中key的标志(当前场景的),请求过来后,先经过布隆过滤器判断是否存在key,如果有才会将请求发送到redis服务

  • client --> bloom过滤器 --> redis服务 --> 数据库

  • 布隆过滤器是一种空间效率非常高的概率性算法(压缩算法)

    • bitmap(位图)

      • int类型 4字节,32个比特位,既可存储32个十进制的数据

      • 例如:存储5和3,对应的二进制是 101和11

        • 假设有32个方格,表示32个比特位

        • 5的存储是:从左到右,0开始数到4;既从第5个格子开始,将101分别写入3个方格

        • 3的存储是:从左到右,0开始数到2;既从第3个格子开始,将11分别写入2个方格

        • 既从高位到低位存储,其余空方格填 0

      • 例如:正常存储 40亿的数据,一个数占4个比特位,则需要16G内存

        • bitmap将16G数据压缩到512M
    • 原理:bitmap + hash映射

      • key 通过多个hash函数,每个函数得到的结果,对应bitmap中的一个方格,将 0 改为 1(标记法)(多个函数是为了降低有可能两个不同的key计算出相同值的概率,降低误判的概率)

      • 既如果客户端发送请求到布隆过滤器,布隆过滤器将key进行过个hash函数计算,如果对应的函数结果,在bitmap上的值都是1,则认为key是存在的,否则,key不存在

    • 实现:google的Guava、Redisson

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

趴着喝可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值