共计 10212 个字符,预计需要花费 26 分钟才能阅读完成。
提到 Redis,大家肯定会想到的几个点是什么呢?
高并发,KV 存储,内存数据库,丰盛的数据结构,单线程(6 版本之前)
那么,接下来,下面提到的这些,都会一一给大家解答,带大家领略一下 Redis 的魅力,文章会比拟长,局部废话,请大家跳过,谢谢!~
欢送进群 973961276 一起聊聊技术吹吹牛,每周都会有几次抽奖送专业书籍的流动,奖品不甚值钱,但也算个搏个彩头
===========================
为什么会呈现缓存?
个别状况下,数据都是在数据库中,利用零碎间接操作数据库。当访问量上万,数据库压力增大,这个时候,怎么办呢?
有小伙伴会说了,分库分表,读写拆散。确实,这些的确是解决比拟高的访问量的解决办法,然而,如果访问量更大,10 万,100 万呢?怎么分仿佛都不解决问题吧,所以咱们须要用到其余方法,来解决高并发带来的数据库压力。
这个时候,缓存呈现了,缓存,顾名思义,就是先把数据缓存在内存中一份,当拜访的时候,咱们会先拜访内存的数据,如果内存中的数据不存在,这个时候,咱们再去读取数据库,之后把数据库中的数据再备份一份到内存中,这样下次读申请过去的时候,还是会间接先从内存中拜访,拜访到内存的数据了之后就间接返回了。这样做就完满的升高了数据库的压力,可能十万个申请进来,全副都拜访了内存中备份的数据,而没有去拜访数据库,或者说只有大量的申请拜访到了数据库,这样真的是大大降低了数据库的压力,而且这样做也进步了零碎响应,大家想一下,内存的读写速度是远远大于硬盘的读写速度的,一个申请进来读取的内存能够比读取硬盘快很多很多,用户的体验也会很高。
什么是缓存呢?
缓存原指 CPU 上的一种高速存储器,它先于内存与 CPU 替换数据,速度很快
当初泛指存储在计算机上的原始数据的复制集,便于快速访问。
在互联网技术中,缓存是零碎疾速响应的关键技术之一。
不足我的项目实战经验和想跳槽涨薪或是自我晋升的敌人看这里 >>c/c++ 我的项目实战 / 后盾服务器开发高级架构师
感觉文字不好了解的敌人能够配合这个视频一起看 >>redis、nginx 及 skynet 源码剖析探索(上)
===========================
缓存的读写模式
缓存有三种读写模式
Cache Aside Pattern(罕用)
Cache Aside Pattern(旁路缓存),是最经典的缓存 + 数据库读写模式
读的时候,先读缓存,缓存没有的话,就读数据库,而后取出数据后放入缓存,同时返回响应。
更新的时候,先更新数据库,而后再删除缓存
为什么是删除缓存,而不是更新缓存呢?
1. 缓存的值是一个构造,hash,list,更新数据须要遍历
2. 懒加载,应用的时候才更新缓存,也能够采纳异步的形式填充缓存
高并发脏读的三种状况
1. 先更新数据库,在更新缓存
update 与 commit 之间,更新缓存,commit 失败,则 DB 与缓存数据不统一
2. 先删除缓存,再更新数据库
update 与 commit 之间,有新的读,缓存空,读 DB 数据到缓存,数据是旧的数据
commit 后 DB 为新的数据
则 DB 与缓存数据不统一
3. 先更新数据库,再删除缓存(举荐)
update 与 commit 之间,有新的读,缓存空,读 DB 数据到缓存,数据是旧的数据
commit 后 DB 为新的数据
则 DB 与缓存数据不统一
采纳延时双删策略
Read/Write Through Pattern
应用程序只操作缓存,缓存操作数据库
Read-Through(穿透读模式 / 直读模式):应用程序读缓存,缓存没有,由缓存回源到数据库,并写入缓存
Write-Through(穿透写模式 / 直写模式):应用程序写缓存,缓存写数据库。该种模式须要提供数据库的 handler,开发较为简单
Write Behind Caching Pattern
应用程序只更新缓存
缓存通过异步的形式将数据批量或合并后更新到 DB 中
不能时时同步,甚至会丢数据
而 Redis 又是什么呢?
Redis 是一个高性能的开源的,C 语言写的 NoSQL(非关系型数据库)也叫做缓存数据库,数据保留在内存中。Redis 是以 key-value 模式存储,和传统的关系型数据库不一样。不肯定遵循传统数据库的那些根本要求。比方,不遵循 SQL 规范,事务,表构造等。Redis 有十分丰盛的数据类型,比方 String,list,set,zset,hash 等
Redis 能够做一些什么呢?
- 下面说的能够加重数据库压力,进步并发量,进步零碎响应工夫
- 做 Session 拆散
传统的 Session 是由本人的 tomcat 进行保护和治理的,在集群和分布式状况下,不同的 tomcat 要治理不同的 session,只能在各个 tomcat 之间,通过网络和 IO 进行 session 复制,极大的影响了零碎的性能
Redis 解决了这一个问题,将登陆胜利后的 session 信息,寄存在 Redis 中,这样多个 tomcat 就能够共享 Session 信息了
- 做分布式锁
个别 Java 中的锁都是多线程锁,是在一个过程中的,多个过程在并发的时候也会产生问题,也要管制时序性,这个时候 Redis 能够用来做分布式锁,应用 Redis 的 setnx 命令来实现
用 Redis 做缓存,有这么有多长处,那么,毛病是不是也会对应的有很多呢?
- 额定的硬件收入
缓存是一种软件系统中以空间换工夫的技术,须要额定的磁盘空间和内存空间来存储数据
- 高并发缓存生效
在高并发的状况下,会呈现缓存生效(缓存穿透,缓存雪崩,缓存击穿等问题)造成霎时数据库访问量增大,甚至解体,所以这些问题是肯定要去解决的
- 缓存与数据库数据同步
缓存与数据库无奈做到数据的时时同步
- 缓存并发竞争
多个 Redis 客户端同时对一个 key 进行 set 值的时候因为执行程序引起的并发的问题
Redis 的装置这里就不说了,mac,windows,linux 网上各种装置教程,很多,大家去网上搜搜跟着做就 ok 了,比较简单,接下来,带着大家来剖析一下,Redis 中的一些常见的数据类型吧。
Redis 的数据结构
Redis 是一个 key-value 的存储系统,key 的类型是字符串
Redis 中常见的 value 的数据类型,有五种,string,list,hash,set,zset
string 字符串类型
string 适宜做单值缓存,对象缓存,分布式锁等
命令名称
命令格局
命令形容
set
set key value
赋值
get
get key
取值
getset
getset key value
取值并赋值
setnx
setnx key value
当 value 不存在时采纳赋值
set key value NX PX 3000 原子操作,px 设置毫秒数
append
append key value
向尾部追加值
strlen
strlen key
获取字符串长度
incr
incr key
递增数字
incrby
incrby key increment
减少指定的整数
decr
decr key
递加数字
decrby
decrby key decrement
缩小指定的整数
mset
mset key value key value
批量赋值
mget
mget key key
批量取值
接下来,咱们执行以下 Redis 的这些命令
set 命令:
127.0.0.1:6379> set name liuxixi
OK
复制代码
get 命令:
127.0.0.1:6379> set name liuxixi
OK
127.0.0.1:6379> get name
"liuxixi"
复制代码
getset 命令:
127.0.0.1:6379> getset name lixixi
"liuxixi"
127.0.0.1:6379> get name
"lixixi"
复制代码
setnx 命令:
127.0.0.1:6379> setnx age 12
(integer) 1 // 第一次返回 1 代表设置胜利
127.0.0.1:6379> setnx age 13
(integer) 0 // 第二次返回 0 代表没有设置胜利
复制代码
append 命令:
127.0.0.1:6379> append name xi
(integer) 8 // 返回的 8 是 value 的长度
127.0.0.1:6379> get name
"lixixixi"
复制代码
strlen 命令:
127.0.0.1:6379> strlen name
(integer) 8
复制代码
incr 命令:
127.0.0.1:6379> incr age // 能够用来做点赞性能
(integer) 14
127.0.0.1:6379> get age
"14"
复制代码
incrby 命令:
127.0.0.1:6379> incrby age 3
(integer) 17
127.0.0.1:6379> get age
"17"
复制代码
decr 命令:
127.0.0.1:6379> decr age
(integer) 16
127.0.0.1:6379> get age
"16"
复制代码
decrby 命令:
127.0.0.1:6379> decrby age 3
(integer) 13
127.0.0.1:6379> get age
"13"
复制代码
hash 散列类型
命令名称
命令格局
命令形容
hset
hset key field value
赋值,不区别新增或批改
hmset
hmset field1 value1 field2 value2
批量赋值
hsetnx
hsetnx key field value
赋值,如果 filed 存在则不操作
hexists
hexists key filed
查看某个 field 是否存在
hget
hget key field
获取一个字段值
hmget
hmget key field1 field2 …
获取多个字段值
hgetall
hgetall key
hdel
hdel key field1 field2..
删除指定字段
hincrby
hincrby key field increment
指定字段自增 increment
hlen
hlen key
取得字段数量
利用场景:能够做电商购物车
电商购物车:
- 以用户 id 为 key
- 商品 id 为 field
- 商品数量为 value
购物车操作:
- 增加商品:hset cart:1001 10088 1
- 减少数量:hincrby cart:1001 10088 1
- 商品总数:hlen cart:1001
- 删除商品:hdel cart:1001 10088
- 获取购物车所有商品:hgetall cart:1001
hash 构造的优缺点
长处
- 同类数据归类整合存储,不便数据管理
- 相比 string 操作耗费内存与 cpu 更小
- 相比 string 存储更节俭空间
毛病
- 过期性能不能应用在 field 上,只能用在 key 上
- Redis 集群架构下不适宜大规模应用
list 列表类型
list 列表类型能够存储有序,可反复的元素
获取头部或尾部左近的记录是极快的
list 的元素个数最多为 2^31- 1 个(40 亿)
常见操作命令如下:
命令名称
命令格局
命令形容
lpush
lpush key v1 v2 v3 …
从左侧插入列表
lpop
lpop key
从列表左侧取出
rpush
rpush key v1 v2 v3 …
从右侧插入列表
rpop
rpop key
从列表右侧取出
lpushx
lpushx key value
将值插入到列表头部
rpushx
rpushx key value
将值插入到列表尾部
blpop
blpop key timeout
从列表左侧取出,当列表为空时阻塞,能够设置最大阻塞时 间,单位为秒
brpop
blpop key timeout
从列表右侧取出,当列表为空时阻塞,能够设置最大阻塞时 间,单位为秒
llen
llen key
取得列表中元素个数
lindex
lindex key index
取得列表中下标为 index 的元素 index 从 0 开始
lrange
lrange key start end
返回列表中指定区间的元素,区间通过 start 和 end 指定
lrem
lrem key count value
删除列表中与 value 相等的元素
当 count>0 时,lrem 会从列表右边开始删除; 当 count<0 时,lrem 会从列表后边开始删除; 当 count= 0 时,lrem 删除所有值 为 value 的元素
lset
lset key index value
将列表 index 地位的元素设置成 value 的值
ltrim
ltrim key start end
对列表进行修剪,只保留 start 到 end 区间
rpoplpush
rpoplpush key1 key2
从 key1 列表右侧弹出并插入到 key2 列表左侧
brpoplpush
brpoplpush key1 key2
从 key1 列表右侧弹出并插入到 key2 列表左侧,会阻塞
linsert
linsert key BEFORE/AFTER pivot value
将 value 插入到列表,且位于值 pivot 之前或之后
罕用数据结构
Stack(栈)=LPUSH+LPOP
Queue(队列)=LPUSH+RPOP
BlockingMQ(阻塞队列)=LPUSH+BRPOP
list 利用场景:
微博和微信公众号音讯流
微博和公众号都是新发的音讯是在最下面的
- MacTalk 发微博,音讯 ID 为 10018
LPUSH msg:{ID} 10018
- 备胎说车发微博,音讯 ID 为 10086
LPUSH msg:{ID} 10086
- 查看最新微博音讯
LRANGE msg:{ID} 0 4
如果微博大 V,或者微信大 V,关注比拟高的,几千个,上万个,能够分零售,比方先给在线的发,这样就很快了
set 汇合类型
set:无序,惟一元素
汇合中最大的成员数为 2^32-1
常见操作命令如下表:
命令名称
命令格局
命令形容
sadd
sadd key mem1 mem2 ….
为汇合增加新成员
srem
srem key mem1 mem2 ….
删除汇合中指定成员
smembers
smembers key
取得汇合中所有元素
spop
spop key
返回汇合中一个随机元素,并将该元素删除
srandmember
srandmember key
返回汇合中一个随机元素,不会删除该元素
scard
scard key
取得汇合中元素的数量
sismember
sismember key member
判断元素是否在汇合内
sinter
sinter key1 key2 key3
求多汇合的交加
sdiff
sdiff key1 key2 key3
求多汇合的差集
sunion
sunion key1 key2 key3
求多汇合的并集
利用场景:
实用于不能反复的且不须要程序的数据结构
比方:关注的用户,还能够通过 spop 进行随即抽奖
微信抽奖小程序:
- 点击参加抽奖退出汇合
SADD key {userId}
- 查看参加抽奖所有用户
SMEMBERS key
- 抽取 count 名中奖者
SRANDMEMBER key [count] / SPOP key [count]
微信微博点赞,珍藏,标签
- 点赞
SADD like:{音讯 ID} {用户 ID}
- 勾销点赞
SREM like:{音讯 ID} {用户 ID}
- 检查用户是否点过赞
SISMEMBMR like:{音讯 ID} {用户 ID}
- 获取点赞的用户列表
SMEMBERS like:{音讯 ID}
- 获取点赞用户数
SCARD like:{音讯 ID}
汇合操作实现微博微信关注模型
- 你关注的人
xx -> {x,xxx}
- 我关注的人
Ll -> {xx , xxx}
- 我和你关注的人
SINTER xx LI -> {xxx}
- 我关注的人也关注他:
SISMEMBER xx LI
- 我可能意识的人:
SDIFF xx LI -> {xx}
zset 有序汇合类型
元素自身是无序不反复的
每一个元素关联一个分数(score)
可按分数排序,分数可反复
常见操作命令如下表:
命令名称
命令格局
命令形容
zadd
zadd key score1 member1 score2 member2 …
为有序汇合增加新成员
zrem
zrem key mem1 mem2 ….
删除有序汇合中指定成员
zcard
zcard key
取得有序汇合中的元素数量
zcount
zcount key min max
返回汇合中 score 值在 [min,max] 区间 的元素数量
zincrby
zincrby key increment member
在汇合的 member 分值上加 increment
zscore
zscore key member
取得汇合中 member 的分值
zrank
zrank key member
取得汇合中 member 的排名(按分值从 小到大)
zrevrank
zrevrank key member
取得汇合中 member 的排名(按分值从大到小)
zrange
zrange key start end
取得汇合中指定区间成员,按分数递增 排序
zrevrange
zrevrange key start end
取得汇合中指定区间成员,按分数递加 排序
利用场景:
因为能够依照分值排序,所以实用于各种排行榜。比方:点赞排行榜,销量排行榜,关注排行榜等。
举例:
127.0.0.1:6379> zadd hit:1 100 item1 20 item2 45 item3
(integer) 3
127.0.0.1:6379> zcard hit:1
(integer) 3
127.0.0.1:6379> zscore hit:1 item3
"45"
127.0.0.1:6379> zrevrange hit:1 0 -1
1) "item1"
2) "item3"
3) "item2"
127.0.0.1:6379>
复制代码
zset 汇合操作实现排行榜
- 点击新闻
ZINCRBY hotNews:20190819 1 守护香港
- 展现当日排行前十
ZREVRANGE hotNews:20190819 0 9 WITHSCORES
- 七日搜寻榜单计算
ZUNIONSTORE hotNews:20190813-20190819 7
hotNews:20190813 hotNews:20190814… hotNews:20190819
- 展现七日排行前十
ZREVRANGE hotNews:20190813-201908109 0 9 WITHSCORES
Redis 的单线程和高性能
Redis 是单线程的么?
Redis 的单线程次要是指 Redis 的网络 IO 和键值对读写是由一个线程来实现的,这也是 Redis 对外提供键值存储服务的次要流程。但 Redis 的其余性能,比方长久化,异步删除,集群数据同步等,都是由额定的线程执行的。
Redis 单线程为什么还能这么快?
这里咱们在本地测试一下 Redis 反对的并发
执行这条命令:./redis-benchmark get
后果:====== get ======
100000 requests completed in 1.02 seconds
50 parallel clients
3 bytes payload
keep alive: 1
host configuration "save": 900 1 300 10 60 10000
host configuration "appendonly": no
multi-thread: no
0.00% <= 0.1 milliseconds
13.00% <= 0.2 milliseconds
55.85% <= 0.3 milliseconds
80.60% <= 0.4 milliseconds
92.57% <= 0.5 milliseconds
97.12% <= 0.6 milliseconds
99.06% <= 0.7 milliseconds
99.68% <= 0.8 milliseconds
99.86% <= 0.9 milliseconds
99.90% <= 1.0 milliseconds
99.90% <= 1.1 milliseconds
99.90% <= 1.2 milliseconds
99.91% <= 1.3 milliseconds
99.93% <= 1.4 milliseconds
99.95% <= 1.5 milliseconds
99.97% <= 1.6 milliseconds
99.98% <= 1.7 milliseconds
99.99% <= 1.8 milliseconds
99.99% <= 1.9 milliseconds
100.00% <= 2 milliseconds
100.00% <= 2 milliseconds
98328.42 requests per second
复制代码
这里咱们能够看到,没秒的话,差不多能够反对小 10 万的并发,这曾经是一个很恐怖的数据了
因为它的所有数据都在内存中,所有的运算都是内存级别的运算,而且单线程防止了多线程的切换性能耗费问题。正因为 Redis 是单线程的,所以要小心应用 Redis 命令,对于那些耗时的指令(比方 keys),肯定要审慎应用,一不小心就可能导致 Redis 卡顿。
Redis 单线程如何解决那么多并发客户端连贯?
Redis 的 IO 多路复用:Redis 利用 epoll 来实现 IO 多路复用,将连贯信息和事件放到队列中,一次放到文件事件分派器,事件分派器将事件分发给事件处理器。
Redis 的一些其它高级命令
keys
全量遍历键,用来列出所有满足特定正则字符串规定的 key,当 Redis 数据量比拟大时,性能比拟差,要防止应用
scan:渐进式遍历键
SCAN cursor [MATCH pattern] [COUNT count]
scan 参数提供了三个参数,第一个是 cursor 整数值(hash 桶的索引值),第二个是 key 的正则模式,第三个是一次遍历 key 的数量(参考值,底层遍历的数量不肯定),并不是符合条件的后果数量。第一次遍历时,cursor 值为 0,而后将返回后果中第一个整数值作为下一次遍历的 cursor。始终遍历到返回的 cursor 值为 0 时完结。
127.0.0.1:6379> scan 0 match key* count 3
1) "12" // 这个 12 代表返回下一次扫描的游标数,下一次 scan 就须要从这个数开始扫描
2) 1) "key4"
127.0.0.1:6379> scan 12 match key* count 3
1) "26"
2) 1) "key1"
2) "key3"
复制代码
留神:然而 scan 并非完满得空,如果在 scan 的过程中如果有键的变动(减少,删除,批改),那么遍历成果可能会碰到如下问题:新增的键可能没有遍历到,遍历出了反复的键等状况,也就是说 scan 并不能保障残缺的键遍历进去所有的键,这些是咱们在开发时须要思考的。
Redis 外围设计原理
Redis 作为 key-value 存储系统,数据结构如下:
一个 Redis 实例对应多个 DB,一个 DB 对应多个 key,key 个别都是 string 的,前面的 value 叫做 RedisObject,不是说 value 就是 string,list,map 这些,而是说这些所有的类型,都被 Redis 封装成了一个叫 RedisObjcet,具体是哪个类型呢?这里是用指针的形式来指向具体是哪个类型
为什么要这么做,次要是为了进步 Redis 的性能
PS:这里插一句,为什么应用指针的形式要比应用对象自身的形式性能更好呢?
这里有两点:
第一点是动态分配,还有指针的一大特点在于你只须要在后面申明一下指针指向的类型(而如果要应用理论的对象,你还须要定义一下)。这样你就能升高你的编译单元之间的耦合性从而缩小编译工夫
RedisDB 构造
Redis 没有表的概念,Redis 实例所对应的 DB 以编号辨别,DB 自身就是 key 的命名空间
比方:user:1000 作为 key 的值,示意在 user 这个命名空间下 id 为 1000 的元素,相似于 user 表的 id=1000 的行
SDS 字符串
家喻户晓,Redis 是用 C 语言来实现的,在 C 语言中,String 这个类型,其实就是一个 char 数组,比方 char data[]=”xxx0″,然而,客户端往 Redis 发送 set 命令,是能够发任意的字符串的,是没有校验的,所以如果咱们发了一个字符串 xx0xx,那么 0 前面的 xx 是不会读的,只会读后面的 xx(C 语言中用 ”0″ 示意字符串完结,如果字符串自身就有 ”0″ 字符,字符串就会被截断)
所以 Redis 自实现了一个 string 叫 sds,sds 中记录了一个 len 和一个 char buf[],len 用来记录 buf 的长度,比方 char buf[] = “xx0xx”,那么 len 就是 5,sds 中还有一个比拟重要的属性就是 free,示意还残余多少
free 是通过扭转 len 来计算,比方 ”xxx1234″ 改成 “xxx123456″,那么会依照(len+addlen)*2=18 来扩容,这个时候 len 变成了 9,free 就是 18- 9 也变成了 9
比方:
char buf[] = "xxx1234" 改成 "xxx123456" // 这里的 buf 是柔性数组
free:12 变成 free:10
len:8 变成 len:10
复制代码
Redis 这样设计 SDS 有什么益处:
- 二进制平安的数据结构
- 提供了内存预分配机制,防止了频繁的内存调配
- 兼容 C 语言的函数库
- 有独自的统计变量 len 和 free,能够不便的失去字符串长度,这样就防止了读取不残缺的危险。
- 内容寄存在柔性数组 buf 中,SDS 对下层裸露的指针不是指向构造体 SDS 的指针,而是间接指向柔性数组 buf 的指针。下层可像读取 C 字符串一样读取 SDS 的内容,兼容 C 语言解决字符串的各种函数。
这里解释一下什么叫柔型数组:
柔型数组即数组大小待定的数组,C 语言中构造体的最初一个元素能够是大小未知的数组,也就是所谓的 0 长度,所以咱们能够用构造体来创立柔性数组。柔性数组主要用途是为了满足需要变长度的构造体,为了解决应用数组时内存的冗余和数组的越界问题
这也是 Redis3.2 之前所实现的。
作者:Five 在致力
链接:https://juejin.im/post/689387…
起源:掘金
著作权归作者所有。商业转载请分割作者取得受权,非商业转载请注明出处。