Redis学习笔记

Redis学习笔记(基于版本:5.0.5 )

基础理论

关系型数据库的特点:
  1. 它以表格的形式,基于行存储数据,是一个二维的模式。
  2. 它存储的是结构化的数据,数据存储有固定的模式(schema),数据需要适应表结构。
  3. 表与表之间存在关联(Relationship)。
  4. 大部分关系型数据库都支持 SQL(结构化查询语言)的操作,支持复杂的关联查询。
  5. 通过支持事务(ACID 酸)来提供严格或者实时的数据一致性。ACID: 原子性、一致性、隔离性、持久性。
关系型数据库限制:
  1. 要实现扩容的话,只能向上(垂直)扩展,比如磁盘限制了数据的存储,就要扩大磁盘容量,通过堆硬件的方式,不支持动态的扩缩容。水平扩容需要复杂的技术来实现,比如分库分表。

  2. 表结构修改困难,因此存储的数据格式也受到限制。

  3. 在高并发和高数据量的情况下, 我们的关系型数据库通常会把数据持久化到磁盘,基于磁盘的读写压力比较大。

非关系型数据库的特点:

NOSQL

叫做“non-relational”或者“Not Only SQL”。

  1. 存储非结构化的数据,比如文本、图片、音频、视频。
  2. 表与表之间没有关联,可扩展性强。
  3. 保证数据的最终一致性。遵循 BASE(碱)理论。 Basically Available(基本
    可用); Soft-state(软状态); Eventually Consistent(最终一致性)。
  4. 支持海量数据的存储和高并发的高效读写。
  5. 支持分布式,能够对数据进行分片存储,扩缩容简单。
Redis 的特性:
  1. 更丰富的数据类型
  2. 进程内与跨进程;单机与分布式
  3. 功能丰富:持久化机制、过期策略
  4. 支持多种编程语言
  5. 高可用,集群
存储形式
  1. KV

  2. 文档存储

  3. 列存储HBase

  4. 图存储 Graph

  5. 对象

  6. XML

基本指令
  1. 切换数据库:select 0 、select 1 …
  2. 清空当前数据库:flushdb
  3. 清空所有数据库: flushall
  4. 存值:set (key) 如:set mytest myValue
  5. 取值:get (key) 如:get mytest
  6. 查看所有键:keys *
  7. 获取键总数: dbsize
  8. 查看键是否存在:exists (key1 key2 …) 如:exists mytest jack
  9. 删除键:del (key1 key2 …)
  10. 重命名键: rename key newkey
  11. 查看类型:type key
Redis 八种数据类型

String、Hash、List、Set、Zset、Hyperloglog、Geo、Streams

最基本也是最常用的数据类型就是 String。其中set 和 get 命令就是 String 的操作命令。

windows版本 命令窗口在redis安装路径下输入 redis-cli

E:\Program Files\Redis>redis-cli
127.0.0.1:6379>auth 123456(密码)
String字符串

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

操作命令:

  • 设置多值:mset key1 value1 key2 value2
mset kobe 24 jordan 23
  • 设置值,如果key存在则不成功: setnx key
setnx kobe 8
  • 基于此可实现分布式锁。 用 del key 释放锁。
    但如果释放锁的操作失败了, 导致其他节点永远获取不到锁, 怎么办?
    加过期时间。 单独用 expire 加过期, 也失败了, 无法保证原子性, 怎么办? 多参数
set key value [expiration EX seconds|PX milliseconds][NX|XX]
  • 使用参数的方式
set lock1 mylockvalue EX 10 NX
  • ( 整数) 值递增 :incr (key )、incrby (key ) 100

    incr kobe 
    
    incrby kobe 100
    
  • ( 整数) 值递减 :decr (key )、decrby (key ) num

    decrby kobe 101
    
  • 浮点数增量 例:set f 2.6 incrbyfloat f 7.3 get f

    set f 2.6
    incrbyfloat f 7.3
    get f   结果为9.9
    
  • 获取多个值:mget key1 key2

    mget f kobe
    
  • 获取值长度:strlen key (长度值包含小数点.)

    strlen f  结果为3
    
  • 字符串追加内容:append key somevalue

    append mytest redis
    
  • 获取指定范围的字符:getrange key beginIdx endIdx 例:getrange test 0 3

    set test redis123
    getrange test 0 4   结果为redis
    
存储(实现)原理

外层kv,key存储在SDS中,vlue存储在redisObject中。实际上五种常用的数据类型的任何一种,都是通过 redisObject 来存储的。

可以使用 type 命令来查看对外的类型。 例:type mytest

type mytest

用object encoding key查询内部编码:

object encoding test

字符串类型的内部编码有三种:

  1. int,存储 8 个字节的长整型(long,2^63-1)。
  2. embstr, 代表 embstr 格式的 SDS(Simple Dynamic String 简单动态字符串),存储小于 44 个字节的字符串。
  3. raw,存储大于 44 个字节的字符串(3.2 版本之前是 39 字节)。
问题:

一、SDS:Redis 中字符串的实现。

二、为什么 Redis 要用 SDS 实现字符串 :

C 语言本身没有字符串类型(只能用字符数组 char[]实现)。
1、使用字符数组必须先给目标变量分配足够的空间,否则可能会溢出。
2、如果要获取字符长度,必须遍历字符数组,时间复杂度是 O(n)。
3、C 字符串长度的变更会对字符数组做内存重分配。
4、通过从字符串开始到结尾碰到的第一个’\0’来标记字符串的结束,因此不能保存图片、音频、视频、压缩文件等 二进制(bytes)保存的内容,二进制不安全。

SDS 的特点:
1、不用担心内存溢出问题,如果需要会对 SDS 进行扩容。
2、获取字符串长度时间复杂度为 O(1),因为定义了 len 属性。
3、通过“空间预分配”( sdsMakeRoomFor)和“惰性空间释放”,防止多次重分配内存。
4、判断是否结束的标志是 len 属性(它同样以’\0’结尾是因为这样就可以使用 C语言中函数库操作字符串的函数 了),可以包含’\0’。

三、embstr 和 raw 的区别?

​ embstr 的使用只分配一次内存空间 (因为 RedisObject 和 SDS 是连续的) , 而 raw需要分配两次内存空间(分别为 RedisObject 和 SDS 分配空间)。
​ 因此与 raw 相比,embstr 的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。
​ 而 embstr 的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个RedisObject 和 SDS 都需要重新分配空间,因此 Redis 中的 embstr 实现为只读。

四、embstr 和 raw 的区别?

​ 当 int 数 据 不 再 是 整 数 , 或 大 小 超 过 了 long 的 范 围(2^63-1=9223372036854775807)时,自动转化为 embstr。

127.0.0.1:6379> set k1 1
OK
127.0.0.1:6379> append k1 a
(integer) 2
127.0.0.1:6379> object encoding k1
"raw"

五、明明没有超过阈值,为什么变成 raw 了?

​ 对于 embstr,由于其实现是只读的,因此在对 embstr 对象进行修改时,都会先转化为 raw 再进行修改。
因此, 只要是修改 embstr 对象, 修改后的对象一定是 raw 的, 无论是否达到了 44个字节。

六、当长度小于阈值时,会还原吗?

​ 关于 Redis 内部编码的转换,都符合以下规律:编码转换在 Redis 写入数据时完成,且转换过程不可逆,只能从小内存编码向大内存编码转换(但是不包括重新 set)。

七、为什么要对底层的数据结构进行一层包装呢?

​ 通过封装,可以根据对象的类型动态地选择存储结构和可以使用的命令,实现节省空间和优化查询速度。

应用场景
  1. 缓存

    String 类型
    例如:热点数据缓存(例如报表,明星劲爆新闻),对象缓存,全页缓存。
    可以提升热点数据的访问速度。

  2. 数据共享分布式

    STRING 类型,因为 Redis 是分布式的独立服务,可以在多个应用之间共享

    例如:分布式Session

<dependency>
	<groupId>org.springframework.session</groupId>
	<artifactId>spring-session-data-redis</artifactId>
</dependency>
  1. 分布式锁

    STRING 类型 setnx 方法,只有不存在时才能添加成功,返回 true。

  2. 全局 ID

    INT 类型,INCRBY,利用原子性

    incrby userid 1000 (分库分表的场景,一次性拿一段)

  3. 计数器

    INT 类型,INCR 方法
    例如:文章的阅读量,微博点赞数,允许一定的延迟,先写入 Redis 再定时同步到数据库。

  4. 限流

    INT 类型,INCR 方法
    以访问者的 IP 和其他信息作为 key, 访问一次增加一次计数, 超过次数则返回 false。

  5. 位统计

    String 类型的 BITCOUNT(1.6.6 的 bitmap 数据结构介绍)。
    字符是以 8 位二进制存储的。

如果一个对象的 value 有多个值的时候,怎么存储?
例如用一个 key 存储一张表的数据

序列化?例如 JSON/Protobuf/XML,会增加序列化和反序列化的开销,并且不能单独获取、修改一个值。
可以通过 key 分层的方式来实现,例如:

mset student:1:sno B00001 student:1:sname 小麻花 student:1:company 腾讯

获取值的时候一次获取多个值:

mget student:1:sno student:1:sname student:1:company

缺点:key 太长,占用的空间太多。有没有更好的方式?

Hash哈希类型

存储类型:包含键值对的无序散列表。value 只能是字符串,不能嵌套其他类型。

同样是存储字符串,Hash 与 String 的主要区别?

  1. 把所有相关的值聚集到一个 key 中,节省内存空间
  2. 只使用一个 key,减少 key 冲突
  3. 当需要批量获取值的时候,只需要使用一个命令,减少内存/IO/CPU 的消耗

Hash 不适合的场景:

  1. Field 不能单独设置过期时间
  2. 没有 bit 操作
  3. 需要考虑数据量分布的问题(value 值非常大的时候,无法分布到多个节点)

操作命令:

hset h1 f 6
hset h1 e 5
hmset h1 a 1 b 2 c 3 d 4
hget h1 a
hmget h1 a b c d
hkeys h1
hvals h1
hgetall h1

key 操作 :

hdel h1
hlen h1
存储(实现)原理

Redis 的 Hash 本身也是一个 KV 的结构,类似于 Java 中的 HashMap。
外层的哈希(Redis KV 的实现)只用到了 hashtable。当存储 hash 数据类型时,我们把它叫做内层的哈希。

内层的哈希底层可以使用两种数据结构实现:
ziplist:OBJ_ENCODING_ZIPLIST(压缩列表) 指向上一个链表的长度及下一个链表的长度。特点:节省内存。
hashtable:OBJ_ENCODING_HT(哈希表)

127.0.0.1:6379> hset h2 f aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
(integer) 1
127.0.0.1:6379> hset h3 f aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
(integer) 1
127.0.0.1:6379> object encoding h2
"ziplist"
127.0.0.1:6379> object encoding h3
"hashtable"
ziplist 压缩列表是什么?

ziplist 是一个经过特殊编码的双向链表,它不存储指向上一个链表节点和指向下一个链表节点的指针,而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能,来换取高效的内存空间利用率,是一种时间换空间的思想。只用在字段个数少,字段值小的场景里面。

什么时候使用 ziplist 存储?

当 hash 对象同时满足以下两个条件的时候,使用 ziplist 编码:
1)所有的键值对的健和值的字符串长度都小于等于 64byte(一个英文字母一个字节);
2)哈希对象保存的键值对数量小于 512 个。

一个哈希对象超过配置的阈值(键和值的长度有>64byte,键值对个数>512 个)时,会转换成哈希表(hashtable)。

hashtable(dict)

在 Redis 中,hashtable 被称为字典(dictionary),它是一个数组+链表的结构。

应用场景
  1. String
    String 可以做的事情,Hash 都可以做。

  2. 存储对象类型的数据

    比如对象或者一张表的数据,比 String 节省了更多 key 的空间,也更加便于集中管理。

    key:用户 id;field:商品 id;value:商品数量。

列表List
操作命令

元素增减:

lpush queue a 从左侧插入a

lpush queue b c

rpush queue d e 从右侧插入 d e

lpop queue 从左侧弹出1个数据

rpop queue 从右侧弹出1个数据

取值:

lindex queue 0

lrange queue 0 -1

存储(实现)原理

在早期的版本中,数据量较小时用 ziplist 存储,达到临界值时转换为 linkedlist 进行存储,分别对应 OBJ_ENCODING_ZIPLIST 和 OBJ_ENCODING_LINKEDLIST 。3.2 版本之后,统一用 quicklist 来存储。

quicklist 存储了一个双向链表,每个节点都是一个 ziplist。

127.0.0.1:6379> object encoding queue
"quicklist"

quicklist
quicklist(快速列表)是 ziplist 和 linkedlist 的结合体。
quicklist.h,head 和 tail 指向双向列表的表头和表尾。

redis.conf 相关参数

参数含义
list-max-ziplist-size( fill)正数表示单个 ziplist 最多所包含的 entry 个数。 负数代表单个 ziplist 的大小, 默认 8k。 -1: 4KB; -2: 8KB; -3: 16KB; -4: 32KB; -5: 64KB
list-compress-depth( compress)压缩深度, 默认是 0。 1: 首尾的 ziplist 不压缩; 2: 首尾第一第二个 ziplist 不压缩, 以此类推
应用场景
  1. 用户消息时间线 timeline

    因为 List 是有序的,可以用来做用户时间线

  2. 消息队列

    List 提供了两个阻塞的弹出操作:BLPOP/BRPOP,可以设置超时时间。
    BLPOP:BLPOP key1 timeout 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
    BRPOP:BRPOP key1 timeout 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
    队列:先进先出:rpush blpop,左头右尾,右边进入队列,左边出队列。
    栈:先进后出:rpush brpop

set 无序集合
存储类型

String 类型的无序集合,最大存储数量 2^32-1(40 亿左右)。

set-操作命令

向集合插入数据

sadd myset a b c d e f g

获取集合中元素

smembers myset    

取集合中元素的个数

scard myset  

从集合中随机获取一个元素

srandmember myset   

随机弹出一个元素

spop myset

弹出指定的元素

srem myset e f  

查看元素是否存在

sismember myset a
存储(实现)原理

Redis 用 intset 或 hashtable 存储 set。

如果元素都是整数类型,就用 inset 存储。如果不是整数类型,就用 hashtable(数组+链表的存来储结构)。

  1. intset
  2. hashtable

如果元素个数超过 512 个,也会用 hashtable 存储。

配置文件 redis.conf

set-max-intset-entries 512
set 应用场景
  1. 抽奖

    随机获取元素
    spop myset

  2. 点赞、 签到、 打卡

    这条微博的 ID 是 t1001,用户 ID 是 u3001。
    用 like:t1001 来维护 t1001 这条微博的所有点赞用户。
    点赞了这条微博:sadd like:t1001 u3001
    取消点赞:srem like:t1001 u3001
    是否点赞:sismember like:t1001 u3001
    点赞的所有用户:smembers like:t1001
    点赞数:scard like:t1001
    比关系型数据库简单许多。

  3. 商品标签

    用 tags:i5001 来维护商品所有的标签。

    sadd tags:i5001 画面清晰细腻
    sadd tags:i5001 真彩清晰显示屏
    sadd tags:i5001 流畅至极

  4. 商品筛选

    获取差集

    sdiff set1 set2
    

    获取交集( intersection )

    sinter set1 set2
    

    获取并集

    sunion set1 set2
    
zset 有序集合
存储类型

sorted set,有序的 set,每个元素有个 score。
score 相同时,按照 key 的 ASCII 码排序。

数据结构对比:

数据结构是否允许重复元素是否有序有序实现方式
列表 list索引下标
集合 set
有序集合 zset分值 score
zset 操作命令

添加元素

zadd myzset 10 java 20 php 30 ruby 40 cpp 50 python

获取全部元素 正序及逆序

zrange myzset 0 -1 withscores
zrevrange myzset 0 -1 withscores

根据分值区间获取元素 (包含)

zrangebyscore myzset 20 30 

移除元素
也可以根据 score rank 删除

zrem myzset php cpp 

统计元素个数

zcard myzset

分值增加 给phthon增加5

zincrby myzset 5 python

根据分值统计个数

zcount myzset 20 60

获取元素 rank 取java的下角标

zrank myzset java     

获取元素 score

zscore myzset java 
存储(实现)原理

同时满足以下条件时使用 ziplist 编码:

  1. 元素数量小于 128 个
  2. 所有 member 的长度都小于 64 字节

在 ziplist 的内部,按照 score 排序递增来存储。插入的时候要移动之后的数据。

对应 redis.conf 参数:

zset-max-ziplist-entries 128
zset-max-ziplist-value 64  

超过阈值之后,使用 skiplist+dict 存储。

zset 应用场景
  1. 排行榜
其他类型数据结构
BitMaps

Bitmaps 是在字符串类型上面定义的位操作。一个字节由 8 个二进制位组成。

应用场景:
用户访问统计
在线用户统计

hyperloglogs

Hyperloglogs:提供了一种不太准确的基数统计方法,比如统计网站的 UV,存在一定的误差。HyperLogLogTest.java

用非常小的内存去统计非常大的数据量 12k 2的64次方。

Streams

5.0 推出的数据类型。支持多播的可持久化的消息队列,用于实现发布订阅功能,借鉴了 kafka 的设计。

数据结构总结
对象对象 type 属性值type 命令输出底层可能的存储结构object encoding
字符串对象OBJ_STRING“string”OBJ_ENCODING_INT OBJ_ENCODING_EMBSTR OBJ_ENCODING_RAWint embstr raw
列表对象OBJ_LIST“list”OBJ_ENCODING_QUICKLISTquicklist
哈希对象OBJ_HASH“hash”OBJ_ENCODING_ZIPLIST OBJ_ENCODING_HTziplist hashtable
集合对象OBJ_SET“set”OBJ_ENCODING_INTSET OBJ_ENCODING_HTintset hashtable
有序集合对象OBJ_ZSET“zset”OBJ_ENCODING_ZIPLIST OBJ_ENCODING_SKIPLISTziplist skiplist( 包含 ht)
编码转换总结
对象原始编码升级编码
字符串对象INTembstrraw
整数并且小于 long 2^63-1超过 44 字节, 被修改
哈希对象ziplisthashtable
键和值的长度小于 64byte, 键值对个数不 超过 512 个, 同时满足
列表对象quicklist
集合对象intsethashtable
元素都是整数类型, 元素个数小于 512 个, 同时满足
有序集合对象ziplistskiplist
元素数量不超过 128 个, 任何一个 member 的长度小于 64 字节, 同时满足。
应用场景总结

缓存——提升热点数据的访问速度
共享数据——数据的存储和共享的问题
全局 ID —— 分布式全局 ID 的生成方案( 分库分表)
分布式锁——进程间共享数据的原子操作保证
在线用户统计和计数
队列、 栈——跨进程的队列/栈
消息队列——异步解耦的消息机制
服务注册与发现 —— RPC 通信机制的服务协调中心( Dubbo 支持 Redis)
购物车
新浪/Twitter 用户消息时间线
抽奖逻辑( 礼物、 转发)
点赞、 签到、 打卡
商品标签
用户( 商品) 关注( 推荐) 模型
电商产品筛选
排行榜

Redis原理

列表的局限

通过队列的 rpush 和 lpop 可以实现消息队列(队尾进队头出),但是消费者需要不停地调用 lpop 查看 List 中是否有等待处理的消息(比如写一个 while 循环)。
为了减少通信的消耗,可以 sleep()一段时间再消费,但是会有两个问题:
1、如果生产者生产消息的速度远大于消费者消费消息的速度,List 会占用大量的内存。
2、消息的实时性降低。

ist 还提供了一个阻塞的命令:blpop,没有任何元素可以弹出的时候,连接会被阻塞。

基于 list 实现的消息队列,不支持一对多的消息分发

发布订阅模式

除了通过 list 实现消息队列之外,Redis 还提供了一组命令实现发布/订阅模式。
这种方式,发送者和接收者没有直接关联(实现了解耦),接收者也不需要持续尝试获取消息。

频道发布与注册

不同客户端

1、订阅者 (Ctrl+c退出客户端)

subscribe c1 c2 c3   

2、发布者(不支持一次向多个频道发布信息)

publish c1 mycontent

3、通配符订阅(?代表一个字符,*代表0个或多个字符)

psubscribe *sport  
psubscribe new *

4、发布同时适应上面的俩个频道

publish newbasketballsport wcba

5、取消订阅(不能在订阅状态下使用)

unsubscribe c1
订阅频道

​ 首先,我们有很多的频道(channel),我们也可以把这个频道理解成 queue。订阅者可以订阅一个或者多个频道。消息的发布者(生产者)可以给指定的频道发布消息。
只要有消息到达了频道,所有订阅了这个频道的订阅者都会收到这条消息。
需要注意的注意是,发出去的消息不会被持久化,因为它已经从队列里面移除了,
所以消费者只能收到它开始订阅这个频道之后发布的消息。

订阅者订阅频道:可以一次订阅多个,比如这个客户端订阅了 3 个频道。

subscribe channel-1 channel-2 channel-3

发布者可以向指定频道发布消息(并不支持一次向多个频道发送消息):

publish channel-1 2673

取消订阅(不能在订阅状态下使用):

unsubscribe channel-1
按规则( Pattern) 订阅频道

支持?和*占位符。?代表一个字符,*代表 0 个或者多个字符。

消费端 1,关注运动信息:

psubscribe *sport

消费端 2,关注所有新闻:

psubscribe news*

消费端 3,关注天气新闻:

psubscribe news-weather

生产者,发布 3 条信息

publish news-sport yaoming
publish news-music jaychou
publish news-weather rain 

Redis 事务

Redis 的单个命令是原子性的(比如 get set mget mset),如果涉及到多个命令的时候,需要把多个命令作为一个不可分割的处理序列,就需要用到事务。
例如之前说的用 setnx 实现分布式锁, 我们先 set, 然后设置对 key 设置 expire,防止 del 发生异常的时候锁不会被释放,业务处理完了以后再 del,这三个动作我们就希望它们作为一组命令执行。

Redis 的事务特点:
  1. 按进入队列的顺序执行
  2. 不对受到其他客户端的请求影响
命令

开启事务 multi

执行事务 exec

放弃事务 discard

监视key watch key

watch命令:

它可以为Redis事务提供CAS乐观锁行为,也就是多个线程更新变量的时候,会跟原值做比较,只有它没有被其他线程修改的情况下,才更新成新的值。

客户端1

set balance 1000
watch balance
multi
incrby balance 100

客户端2

decrby balance 100
此处直接提交事务了将balance值减了100,即为900

回到客户端1,由于值被其他线程修(客户端2)改了,即此次执行事务修改不成功。

exec
(nil)
get balance
900

执行过程中事务发生异常

  • 执行exec之前 之前的命令都没执行成功
  • 执行exec之后 报错的命令没有执行成功
事务可能遇到的问题

把事务执行遇到的问题分成两种,一种是在执行 exec 之前发生错误,一种是在执行 exec 之后发生错误。

在执行 exec 之前发生错误

比如:入队的命令存在语法错误,包括参数数量,参数名等等(编译器错误)。

在这种情况下事务会被拒绝执行,也就是队列中所有的命令都不会得到执行 。

在执行 exec 之后发生错误

比如,类型错误,比如对 String 使用了 Hash 的命令,这是一种运行时错误。

127.0.0.1:6379> flushall
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 1
QUEUED
127.0.0.1:6379> hset k1 a b
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> get k1
"1"

最后我们发现 set k1 1 的命令是成功的,也就是在这种发生了运行时异常的情况下,只有错误的命令没有被执行,但是其他命令没有受到影响。
这个显然不符合原子性的定义,也就是我们没办法用 Redis 的这种事务机制来实现原子性,保证数据的一致。

Lua脚本

概念

Lua是一种轻量级脚本语言,它是用C语言编写的,跟数据的存储过程有点类似。使用Lua脚本来执行Redis命令的好处:

Lua 2.6

  • 一次发送多个命令,减少网络开销
  • Redis会将整个脚本作为一个整体执行,不会被其他请求打断,保持原子性。
  • Lua file 对于复杂的组合命令,我们可以放在文件中,可以实现程序之间的命令集复用。
在Redis中调用Lua脚本

​ 使用eval方法,语法格式

redis> eval lua-script key-num [key1 key2 key3 ....] [value1 value2 value3 ....]
  • eval 代表执行 Lua 语言的命令。
  • lua-script 代表 Lua 语言脚本内容。
  • key-num 表示参数中有多少个 key, 需要注意的是 Redis 中 key 是从 1 开始的, 如果没有 key 的参数, 那么写 0。
  • [key1 key2 key3…]是 key 作为参数传递给 Lua 语言, 也可以不填, 但是需要和 key-num 的个数对应起来。
  • [value1 value2 value3 ….]这些参数传递给 Lua 语言, 它们是可填可不填的。

示例,返回一个字符串,0 个参数:

redis> eval "return 'Hello Redis'" 0
在Lua脚本中调用Redis命令

​ 使用 redis.call(command, key [param1, param2…])进行操作。语法格式:

redis> eval "redis.call('set',KEYS[1],ARGV[1])" 1 lua-key lua-value
  • command 是命令, 包括 set、 get、 del 等。
  • key是被操作的键。
  • param1,param2…代表给key的参数

注意跟java不一样,定义只有形参,调用只有实参。

Lua是在调用时用key表示形参,argv表示参数值(实参)。

设置键值对
redis> eval "return redis.call('set',KEYS[1],ARGV[1])" 1 gupao 2673
redis> get gupao

以上命令等价于 set gupao 2673。

在 redis-cli 中直接写 Lua 脚本不够方便,也不能实现编辑和复用,通常我们会把脚本放在文件里面,然后执行这个文件。

在 Redis 中调用 Lua 脚本
cd /usr/local/soft/redis5.0.5/src
vim gupao.lua

Lua 脚本内容,先设置,再取值:

redis.call('set','gupao','lua666')
return redis.call('get','gupao')

在 Redis 客户端中调用 Lua 脚本

cd /usr/local/soft/redis5.0.5/src
redis-cli --eval gupao.lua 0

得到返回值:
[root@localhost src]# redis-cli --eval gupao.lua 0
"lua666

应用场景:对IP进行限流
缓存 Lua 脚本

在脚本比较长的情况下,如果每次调用脚本都需要把整个脚本传给 Redis 服务端,会产生比较大的网络开销。为了解决这个问题,Redis 提供了 EVALSHA 命令,允许开发者通过脚本内容的 SHA1 摘要来执行脚本。

如何缓存:

Redis 在执行 script load 命令时会计算脚本的 SHA1 摘要并记录在脚本缓存中,执行 EVALSHA 命令时 Redis 会根据提供的摘要从脚本缓存中查找对应的脚本内容,如果找到了则执行脚本,否则会返回错误:“NOSCRIPT No matching script. Please use EVAL.”
127.0.0.1:6379> script load “return ‘Hello World’”
“470877a599ac74fbfda41caa908de682c5fc7d4b”
127.0.0.1:6379> evalsha “470877a599ac74fbfda41caa908de682c5fc7d4b” 0
“Hello World”

Redis为什么这么快

根据官方的数据,Redis 的 QPS 可以达到 10 万左右(每秒请求数)。

即redis每秒钟可接收请求10万次作用。

原因:

  • 纯内存结构 KV
  • 单线程 无创建线程和销毁线程的消耗;避免上下文切换;资源竞争问题。
  • 异步非阻塞I/O,多路复用
  • 物理寻址 虚拟存储器(虚拟内存)
内存

Redis基于内存操作,KV 结构的内存数据库,时间复杂度 O(1)。

单线程

Redis是单线程的,单线程好处:

  • 没有创建线程,销毁线程带来的消耗
  • 避免了上下文切换导致的CPU消耗
  • 避免了线程之间带来的竞争问题,例如加锁释放锁死锁等等。
异步非阻塞

异步非阻塞IO,多路复用处理并发连接。

Redis 为什么是单线程的?

因为单线程已经够用了,CPU 不是 redis 的瓶颈。Redis 的瓶颈最有可能是机器内存或者网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案了。

单线程为什么这么快?

因为 Redis 是基于内存的操作,我们先从内存开始说起。

虚拟存储器( 虚拟内存 Vitual Memory)
IO多路复用( I/O Multiplexing)

I/O 指的是网络 I/O。
多路指的是多个 TCP 连接(Socket 或 Channel)。
复用指的是复用一个或多个线程。
它的基本原理就是不再由应用程序自己监视连接,而是由内核替应用程序监视文件描述符。

基于文件的操作系统 FD File Descriptor

FD索引

非负整数

0 标准输入 键盘

1 标准输出 显示器

2 标准错误输出 显示器

redis调用图

在这里插入图片描述

多路复用

在这里插入图片描述

内存回收

定义

Reids 所有的数据都是存储在内存中的, 在某些情况下需要对占用的内存空间进行回收。 内存回收主要分为两类, 一类是 key 过期, 一类是内存使用达到上限 (max_memory)触发内存淘汰。

过期策略
  • 定时过期

主动淘汰 expire key seconds 对key进行过期时间设置。

每个设置过期时间的 key 都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的 CPU 资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。

  • 惰性过期

被动淘汰 下次被访问时,判断是否过期。

只有当访问一个 key 时,才会判断该 key 是否已过期,过期则清除。该策略可以最大化地节省 CPU 资源,却对内存非常不友好。极端情况可能出现大量的过期 key 没有再次被访问,从而不会被清除,占用大量内存。

​ getCommand expireIfNeed()

​ set memory 上线

​ activeExpireCycle()

  • 定期过期

每隔一定的时间,会扫描一定数量的数据库的 expires 字典中一定数量的 key,并清除其中已过期的 key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得 CPU 和内存资源达到最优的平衡效果。

Redis 中同时使用了惰性过期和定期过期两种过期策略。

淘汰策略

Redis 的内存淘汰策略,是指当内存使用达到最大内存极限时,需要使用淘汰算法来决定清理掉哪些数据,以保证新数据的存入。

最大内存设置 动态修改

redis> config set maxmemory 3GB

到达最大内存以后怎么办?
redis.conf
# maxmemory-policy noeviction

# volatile-lru -> Evict using approximated LRU among the keys with an expire set.
# allkeys-lru -> Evict any key using approximated LRU.
# volatile-lfu -> Evict using approximated LFU among the keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.
# volatile-random -> Remove a random key among the ones with an expire set.
# allkeys-random -> Remove a random key, any key.
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
# noeviction -> Don't evict anything, just return an error on write operations.

算法上:

LRU,Least Recently Used:最近最少使用。判断最近被使用的时间,目前最远的数据优先被淘汰。

LFU,Least Frequently Used,最不常用,4.0 版本新增。

策略含义
volatile-lru根据 LRU 算法删除设置了超时属性( expire) 的键, 直到腾出足够内存为止。 如果没有 可删除的键对象, 回退到 noeviction 策略。
allkeys-lru根据 LRU 算法删除键, 不管数据有没有设置超时属性, 直到腾出足够内存为止。
volatile-lfu在带有过期时间的键中选择最不常用的。
allkeys-lfu在所有的键中选择最不常用的, 不管数据有没有设置超时属性。
volatile-random在带有过期时间的键中随机选择。
allkeys-random随机删除所有键, 直到腾出足够内存为止。
volatile-ttl根据键值对象的 ttl 属性, 删除最近将要过期数据。 如果没有, 回退到 noeviction 策略。
noeviction默认策略, 不会删除任何数据, 拒绝所有写入操作并返回客户端错误信息( error) OOM command not allowed when used memory, 此时 Redis 只响应读操作。

如果没有符合前提条件的 key 被淘汰,那么 volatile-lru、volatile-random 、volatile-ttl 相当于 noeviction(不做内存回收)。

动态修改淘汰策略

redis> config set maxmemory-policy volatile-lru

持久化机制

​ Redis 速度快,很大一部分原因是因为它所有的数据都存储在内存中。如果断电或者宕机,都会导致内存中的数据丢失。为了实现重启后数据不丢失,Redis 提供了两种持久化的方案,一种是 RDB 快照 (Redis DataBase),一种是 AOF(Append Only File)。

  • RDB快照 Redis DataBase

    默认持久化方案 dump.rdb

  • AOF Append Only File

RDB

​ RDB 是 Redis 默认的持久化方案。当满足一定条件的时候,会把当前内存中的数据写入磁盘,生成一个快照文件 dump.rdb。Redis 重启会通过加载 dump.rdb 文件恢复数据。

自动触发
  • 配置规则触发

    redis.conf, SNAPSHOTTING,其中定义了触发把数据保存到磁盘的触发频率。如果不需要 RDB 方案,注释 save 或者配置成空字符串""。

    save 900 1 # 900 秒内至少有一个 key 被修改( 包括添加)
    save 300 10 # 400 秒内至少有 10 个 key 被修改
    save 60 10000 # 60 秒内至少有 10000 个 key 被修改
    

注意上面的配置是不冲突的,只要满足任意一个都会触发。
RDB 文件位置和目录:

# 文件路径,
dir ./
# 文件名称
dbfilename dump.rdb
# 是否是 LZF 压缩 rdb 文件
rdbcompression yes
# 开启数据校验
rdbchecksum yes
参数说明
dirrdb 文件默认在启动目录下( 相对路径) config get dir 获取
dbfilename文件名称
rdbcompression开启压缩可以节省存储空间, 但是会消耗一些 CPU 的计算时间, 默认开启
rdbchecksum使用 CRC64 算法来进行数据校验, 但是这样做会增加大约 10%的性能消耗, 如果希望获取到最 大的性能提升, 可以关闭此功能
  • shutdown 触发,保证服务器正常关闭。
  • flushall,RDB 文件是空的,没什么意义。
手动触发

如果我们需要重启服务或者迁移数据,这个时候就需要手动触 RDB 快照保存。Redis提供了两条命令:

  • save
  • bgsave

​ save 在生成快照的时候会阻塞当前 Redis 服务器, Redis 不能处理其他命令。如果内存中的数据比较多,会造成 Redis 长时间的阻塞。生产环境不建议使用这个命令。为了解决这个问题,Redis 提供了第二种方式。

​ 执行 bgsave 时,Redis 会在后台异步进行快照操作,快照同时还可以响应客户端请求。具体操作是 Redis 进程执行 fork 操作创建子进程(copy-on-write),RDB 持久化过程由子进程负责,完成后自动结束。它不会记录 fork 之后后续的命令。阻塞只发生在fork 阶段,一般时间很短。用 lastsave 命令可以查看最近一次成功生成快照的时间。

RDB 文件的优势和劣势

一、优势
1.RDB 是一个非常紧凑(compact)的文件,它保存了 redis 在某个时间点上的数据集。这种文件非常适合用于进行备份和灾难恢复。
2.生成 RDB 文件的时候,redis 主进程会 fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘 IO 操作。
3.RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
二、劣势
1、RDB 方式数据没办法做到实时持久化/秒级持久化。因为 bgsave 每次运行都要执行 fork 操作创建子进程,频繁执行成本过高。
2、在一定间隔时间做一次备份,所以如果 redis 意外 down 掉的话,就会丢失最后一次快照之后的所有修改(数据有丢失)。如果数据相对来说比较重要,希望将损失降到最小,则可以使用 AOF 方式进行持久化。

AOF

AOF:Redis 默认不开启。AOF 采用日志的形式来记录每个写操作,并追加到文件中。开启后,执行更改 Redis 数据的命令时,就会把命令写入到 AOF 文件中。
Redis 重启时会根据日志文件的内容把写指令从前到后执行一次以完成数据的恢复工作。

配置文件 redis.conf

# 开关
appendonly no
# 文件名
appendfilename "appendonly.aof"
参数说明
appendonlyRedis 默认只开启 RDB 持久化, 开启 AOF 需要修改为 yes
appendfilename “appendonly.aof”路径也是通过 dir 参数配置 config get dir
AOF 数据恢复

重启 Redis 之后就会进行 AOF 文件的恢复。

AOF 优势与劣势

优点:

1、AOF 持久化的方法提供了多种的同步频率,即使使用默认的同步频率每秒同步一次,Redis 最多也就丢失 1 秒的数据而已。
缺点:
1、对于具有相同数据的的 Redis,AOF 文件通常会比 RDF 文件体积更大(RDB存的是数据快照)。
2、虽然 AOF 提供了多种同步的频率,默认情况下,每秒同步一次的频率也具有较高的性能。在高并发的情况下,RDB 比 AOF 具好更好的性能保证。

两种方案比较

如果可以忍受一小段时间内数据的丢失,毫无疑问使用 RDB 是最好的,定时生成RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快。否则就使用 AOF 重写。但是一般情况下建议不要单独使用某一种持久化机制,而是应该两种一起用,在这种情况下,当 redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。

Redis分布式

为什么需要集群?

  • 性能
  • 扩展
  • 可用性、安全

性能
Redis 本身的 QPS 已经很高了,但是如果在一些并发量非常高的情况下,性能还是会受到影响。这个时候我们希望有更多的 Redis 服务来完成工作。

扩展
第二个是出于存储的考虑。因为 Redis 所有的数据都放在内存中,如果数据量大,很容易受到硬件的限制。升级硬件收效和成本比太低,所以我们需要有一种横向扩展的方法。

可用性
第三个是可用性和安全的问题。如果只有一个 Redis 服务,一旦服务宕机,那么所有的客户端都无法访问,会对业务造成很大的影响。另一个,如果硬件发生故障,而单机的数据无法恢复的话,带来的影响也是灾难性的。

可用性、数据安全、性能都可以通过搭建多个 Reids 服务实现。其中有一个是主节点(master),可以有多个从节点(slave)。主从之间通过数据同步,存储完全相同的数据。如果主节点发生故障,则把某个从节点改成主节点,访问新的主节点。

Redis主从复制(replication)

主从复制配置

主从节点 master -replica/slave

例如一主多从,203 是主节点,在每个 slave 节点的 redis.conf 配置文件增加一行

即将当前客户端设置为203节点的从节点

slaveof 192.168.8.203 6379

主从切换时,这个节点配置会被重写成

replicaof 192.168.8.203 6379

或者重新启动时通过参数指定master节点

./redis-server --slaveof 192.168.8.203 6379

或在客户端直接执行 slaveof xx xx,使该 Redis 实例成为从节点。

启动后,查看集群状态:

redis> info replication

从节点不能写入数据(只读),只能从 master 节点同步数据。get 成功,set 失败。

主节点写入后,slave 会自动从 master 同步数据。

断开复制: 此时从节点会变成自己的主节点,不再复制数据。

redis> slaveof no one
主从复制原理
连接阶段

1、slave node 启动时(执行 slaveof 命令),会在自己本地保存 master node 的信息,包括 master node 的 host 和 ip。
2、slave node 内部有个定时任务 replicationCron(源码 replication.c),每隔 1秒钟检查是否有新的 master node 要连接和复制,如果发现,就跟 master node 建立socket 网络连接,如果连接成功,从节点为该 socket 建立一个专门处理复制工作的文件事件处理器,负责后续的复制工作,如接收 RDB 文件、接收命令传播等。当从节点变成了主节点的一个客户端之后,会给主节点发送 ping 请求。

数据同步阶段

3、master node 第一次执行全量复制,通过 bgsave 命令在本地生成一份 RDB 快照,将 RDB 快照文件发给 slave node(如果超时会重连,可以调大 repl-timeout 的值) 。slave node 首先清除自己的旧数据,然后用 RDB 文件加载数据。

命令传播阶段

4、master node 持续将写命令,异步复制给 slave node。

延迟是不可避免的,只能通过优化网络。

repl-disable-tcp-nodelay no

通过 master_repl_offset 记录的偏移量

redis> info replication
主从复制的不足

主从模式解决了数据备份和性能(通过读写分离)的问题,但是还是存在一些不足:

1、RDB 文件过大的情况下,同步非常耗时。
2、在一主一从或者一主多从的情况下,如果主服务器挂了,对外提供的服务就不可用了,单点问题没有得到解决。如果每次都是手动把之前的从服务器切换成主服务器,这个比较费时费力,还会造成一定时间的服务不可用。

可用性保证之Sentinel

Sentinel原理

从 Redis2.8 版本起,提供了一个稳定版本的 Sentinel(哨兵),用来解决高可用的问题。它是一个特殊状态的 redis 实例。

我们会启动一个或者多个 Sentinel 的服务(通过 src/redis-sentinel),它本质上只是一个运行在特殊模式之下的 Redis,Sentinel 通过 info 命令得到被监听 Redis 机器的master,slave 等信息。

为了保证监控服务器的可用性,我们会对 Sentinel 做集群的部署。Sentinel 既监控所有的 Redis 服务,Sentinel 之间也相互监控。

注意:Sentinel 本身没有主从之分,只有 Redis 服务节点有主从之分。

服务下线

Sentinel 默认以每秒钟 1 次的频率向 Redis 服务节点发送 PING 命令。如果在down-after-milliseconds 内都没有收到有效回复,Sentinel 会将该服务器标记为下线(主观下线)。

# sentinel.conf
sentinel down-after-milliseconds <master-name> <milliseconds>

这个时候 Sentinel 节点会继续询问其他的 Sentinel 节点,确认这个节点是否下线, 如果多数 Sentinel 节点都认为 master 下线,master 才真正确认被下线(客观下线),这个时候就需要重新选举 master。

故障转移

如果 master 被标记为下线,就会开始故障转移流程 。

故障转移流程的第一步就是在 Sentinel 集群选择一个 Leader,由 Leader 完成故障转移流程。Sentinle 通过 Raft 算法,实现 Sentinel 选举。

Ratf 算法

在分布式存储系统中,通常通过维护多个副本来提高系统的可用性,那么多个节点之间必须要面对数据一致性的问题。Raft 的目的就是通过复制的方式,使所有节点达成一致,但是这么多节点,以哪个节点的数据为准呢?所以必须选出一个 Leader。

大体上有两个步骤:领导选举,数据复制。

Raft 的核心思想:先到先得,少数服从多数。

问题总结
  1. 怎么让一个原来的 slave 节点成为主节点?

选出 Sentinel Leader 之后, 由 Sentinel Leader 向某个节点发送 slaveof no one命令,让它成为独立节点。

然后向其他节点发送 slaveof x.x.x.x xxxx(本机服务),让它们成为这个节点的子节点,故障转移完成。

  1. 这么多从节点,选谁成为主节点?

关于从节点选举,一共有四个因素影响选举的结果,分别是断开连接时长、优先级排序、复制数量、进程 id。

如果与哨兵连接断开的比较久,超过了某个阈值,就直接失去了选举权。如果拥有选举权,那就看谁的优先级高,这个在配置文件里可以设置(replica-priority 100),数值越小优先级越高。

如果优先级相同,就看谁从 master 中复制的数据最多(复制偏移量最大),选最多的那个,如果复制数量也相同,就选择进程 id 最小的那个。

Sentinel的功能总结
  1. 监控:Sentinel 会不断检查主服务器和从服务器是否正常运行。
  2. 通知:如果某一个被监控的实例出现问题,Sentinel 可以通过 API 发出通知。
  3. 自动故障转移(failover):如果主服务器发生故障,Sentinel 可以启动故障转移过程。把某台服务器升级为主服务器,并发出通知。
  4. 配置管理:客户端连接到 Sentinel,获取当前的 Redis 主服务器的地址。
Sentinel实战
Sentinel配置

为了保证 Sentinel 的高可用,Sentinel 也需要做集群部署,集群中至少需要三个Sentinel 实例(推荐奇数个,防止脑裂)。

hostnameIP 地址节点角色&端口
master192.168.8.203Master: 6379 / Sentinel : 26379
slave1192.168.8.204Slave : 6379 / Sentinel : 26379
Slave2192.168.8.205Slave : 6379 / Sentinel : 26379

以 Redis 安装路径/usr/local/soft/redis-5.0.5/为例。
在 204 和 205 的 src/redis.conf 配置文件中添加

slaveof 192.168.8.203 6379

在 203、204、205 创建 sentinel 配置文件(安装后根目录下默认有 sentinel.conf) :

命令如下:

cd /usr/local/soft/redis-5.0.5
mkdir logs
mkdir rdbs
mkdir sentinel-tmp
vim sentinel.conf

在Redis根目录下建立sentinel.conf 文件,各服务器内容相同

daemonize yes
port 26379
protected-mode no
dir "/usr/local/soft/redis-5.0.5/sentinel-tmp"
sentinel monitor redis-master 192.168.8.203 6379 2
sentinel down-after-milliseconds redis-master 30000
sentinel failover-timeout redis-master 180000
sentinel parallel-syncs redis-master 1

上面出现了 4 个’redis-master’,这个名称要统一,并且使用客户端(比如 Jedis)连接的时候名称要正确。

hostnameIP 地址
protected-mode是否允许外部网络访问
dirsentinel 的工作目录
sentinel monitorsentinel 监控的 redis 主节点
down-after-milliseconds( 毫秒)master 宕机多久, 才会被 Sentinel 主观认为下线
sentinel failover-timeout( 毫秒)1 同一个 sentinel 对同一个 master 两次 failover 之间的间隔时间。 2. 当一个 slave 从一个错误的 master 那里同步数据开始计算时间。 直到 slave 被纠正为向正确的 master 那里同步数据时。 3.当想要取消一个正在进行的 failover 所需要的时间。 4.当进行 failover 时, 配置所有 slaves 指向新的 master 所需的最大时间。
parallel-syncs这个配置项指定了在发生 failover 主备切换时最多可以有多少个 slave 同时对新的 master 进行 同步, 这个数字越小, 完成 failover 所需的时间就 越长, 但是如果这个数字越大, 就意味着越 多的 slave 因为 replication 而 不可用。 可以通过将这个值设为 1 来保证每次只有一个 slave 处于不能处 理命令请求的状态。
Sentinel 验证

启动 Redis 服务和 Sentinel

cd /usr/local/soft/redis-5.0.5/src
# 启动 Redis 节点
./redis-server ../redis.conf
# 启动 Sentinel 节点
./redis-sentinel ../sentinel.conf
# 或者
./redis-server ../sentinel.conf --sentinel

查看集群状态:

redis> info replication
Sentinel 连接使用

Jedis 连接 Sentinel

master name 来自于 sentinel.conf 的配置。

private static JedisSentinelPool createJedisPool() {
    // master的名字是sentinel.conf配置文件里面的名称
    String masterName = "redis-master";
    Set<String> sentinels = new HashSet<String>();
    sentinels.add("192.168.8.203:26379");
    sentinels.add("192.168.8.204:26379");
    sentinels.add("192.168.8.205:26379");
    pool = new JedisSentinelPool(masterName, sentinels);
    return pool;
}

Spring Boot 连接 Sentinel

spring.redis.sentinel.master=redis-master
spring.redis.sentinel.nodes=192.168.8.203:26379,192.168.8.204:26379,192.168.8.205:26379

无论是 Jedis 还是 Spring Boot(2.x 版本默认是 Lettuce),都只需要配置全部哨兵的地址,由哨兵返回当前的 master 节点地址。

哨兵机制的不足
  • 主从切换的过程中会丢失数据,因为只有一个 master。
  • 只能单点写,没有解决水平扩容的问题。
  • 如果数据量非常大,这个时候我们需要多个 master-slave 的 group,把数据分布到不同的 group 中。

数据怎么分片?分片之后,怎么实现路由?

Redis分布式方案

如果要实现 Redis 数据的分片,我们有三种方案。

第一种是在客户端实现相关的逻辑,例如用取模或者一致性哈希对 key 进行分片,查询和修改都先判断 key 的路由。
第二种是把做分片处理的逻辑抽取出来,运行一个独立的代理服务,客户端连接到这个代理服务,代理服务做请求的转发。
第三种就是基于服务端实现。

客户端Sharding

Jedis 客户端提供了 Redis Sharding 的方案,并且支持连接池。

使用 ShardedJedis 之类的客户端分片代码的优势是配置简单,不依赖于其他中间件,分区的逻辑可以自定义,比较灵活。但是基于客户端的方案,不能实现动态的服务增减,每个客户端需要自行维护分片策略,存在重复代码。
第二种思路就是把分片的代码抽取出来,做成一个公共服务,所有的客户端都连接到这个代理层。由代理层来实现请求和转发。

代理 Proxy

典型的代理分区方案有 Twitter 开源的 Twemproxy 和国内的豌豆荚开源的 Codis。

Redis Cluster

Redis Cluster 是在 Redis 3.0 的版本正式推出的,用来解决分布式的需求,同时也可以实现高可用。跟 Codis 不一样,它是去中心化的,客户端可以连接到任意一个可用节点。

数据分片有几个关键的问题需要解决:
1、数据怎么相对均匀地分片
2、客户端怎么访问到相应的节点和数据
3、重新分片的过程,怎么保证正常服务

Redis实战

RedisTemplate 看RedisConnectionFactory 是由哪个客户端去实现的 如 JedisConnectionFactory、LettuceConnectionFactory

Jedis

Jedis 是我们最熟悉和最常用的客户端。轻量,简洁,便于集成和改造。

public static void main(String[] args) {
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    jedis.auth("123456");
    jedis.set("mytest", "redisTest");
    System.out.println(jedis.get("mytest"));
    jedis.close();
}

线程不安全

多个线程多个服务调用一个连接示例,出现线程不安全,可创建线程池,一个请求创建一个连接示例。

Jedis 多个线程使用一个连接的时候线程不安全。可以使用连接池,为每个请求创建不同的连接,基于 Apache common pool 实现。跟数据库一样,可以设置最大连接数等参数。Jedis 中有多种连接池的子类。

如:ShardedJedisPool, JedisSentinePool

public static void main(String[] args) {
    JedisPool pool = new JedisPool(ip, port);
    Jedis jedis = jedisPool.getResource();
    // ...
}

Jedis 有 4 种工作模式:单节点、分片、哨兵、集群。

请求模式:Client、Pipeline、事务

Lettuce

特点:线程安全的客户端、支持同步、异步及响应式编程。

SpringBoot2.X默认的Redis客户端,基于Netty构建的。

Redisson

非传统的Redis的客户端,在redis基础上提供的Java的数据网格,数据结构,服务。

缓存使用场景
方案选择

1、先操作Redis的数据再操作数据库的数据

2、先操作数据库的数据再操作Redis的数据

Q:操作Redis,更新,还是删除

推荐删除,一定程度避免缓存和数据不一致

先更新数据库,再删除缓存

1、正常情况

2、异常情况 数据库成功,缓存失败,redis旧值,解决:消息队列式的再次删缓存。

异步方案 更新数据 binlog 服务监听binlog

先删除Redis缓存,再更新数据库

1、正常情况

2、异常情况 删除缓存成功,更新数据库失败,当并发情况 出现问题。解决:延时双删

热点数据发现

1、客户端

2、代理层 Proxy

3、服务端

4、机器 packetbeat

缓存雪崩

防止缓存雪崩(高并发,没使用到缓存,直接访问到数据库)

1、过期时间 random

2、永不过期

3、预更新

缓存穿透

数据不存在:缓存穿透 Redis被打穿,失去作为数据库屏障的作用。

如何在海量元素中(例如10亿无序、不定长、不重复)快速判断一个元素是否存在?

BitMap(位图) BitSet

布隆过滤器 Guava

<dependency>
	<groupId>com.google.guava</groupId>
	<version>21.0</version>
</dependency>

如果布隆过滤器判断元素在集合中存在,不一定存在

如何元素实际存在,布隆过滤器一定判断存在

如果元素实际不存在,布隆过滤器可能判断存在

Jedis

Jedis 是我们最熟悉和最常用的客户端。轻量,简洁,便于集成和改造。

public static void main(String[] args) {
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    jedis.auth("123456");
    jedis.set("mytest", "redisTest");
    System.out.println(jedis.get("mytest"));
    jedis.close();
}

线程不安全

多个线程多个服务调用一个连接示例,出现线程不安全,可创建线程池,一个请求创建一个连接示例。

Jedis 多个线程使用一个连接的时候线程不安全。可以使用连接池,为每个请求创建不同的连接,基于 Apache common pool 实现。跟数据库一样,可以设置最大连接数等参数。Jedis 中有多种连接池的子类。

如:ShardedJedisPool, JedisSentinePool

public static void main(String[] args) {
    JedisPool pool = new JedisPool(ip, port);
    Jedis jedis = jedisPool.getResource();
    // ...
}

Jedis 有 4 种工作模式:单节点、分片、哨兵、集群。

请求模式:Client、Pipeline、事务

Lettuce

特点:线程安全的客户端、支持同步、异步及响应式编程。

SpringBoot2.X默认的Redis客户端,基于Netty构建的。

Redisson

非传统的Redis的客户端,在redis基础上提供的Java的数据网格,数据结构,服务。

缓存使用场景
方案选择

1、先操作Redis的数据再操作数据库的数据

2、先操作数据库的数据再操作Redis的数据

Q:操作Redis,更新,还是删除

推荐删除,一定程度避免缓存和数据不一致

先更新数据库,再删除缓存

1、正常情况

2、异常情况 数据库成功,缓存失败,redis旧值,解决:消息队列式的再次删缓存。

异步方案 更新数据 binlog 服务监听binlog

先删除Redis缓存,再更新数据库

1、正常情况

2、异常情况 删除缓存成功,更新数据库失败,当并发情况 出现问题。解决:延时双删

热点数据发现

1、客户端

2、代理层 Proxy

3、服务端

4、机器 packetbeat

缓存雪崩

防止缓存雪崩(高并发,没使用到缓存,直接访问到数据库)

1、过期时间 random

2、永不过期

3、预更新

缓存穿透

数据不存在:缓存穿透 Redis被打穿,失去作为数据库屏障的作用。

如何在海量元素中(例如10亿无序、不定长、不重复)快速判断一个元素是否存在?

BitMap(位图) BitSet

布隆过滤器 Guava

<dependency>
	<groupId>com.google.guava</groupId>
	<version>21.0</version>
</dependency>

如果布隆过滤器判断元素在集合中存在,不一定存在

如何元素实际存在,布隆过滤器一定判断存在

如果元素实际不存在,布隆过滤器可能判断存在

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值