众所周知,在NoSQL领域,redis是目前比较流行的一种数据库,也是最近招聘中各大厂面试的必备数据库知识点,可看出其热门程度。在众多纷纭的各种学习教程和文章中,我们如何从初学者快速成为redis的高手,其实,只需要从这三个方面入手,让你看完从入门到高手。
一、redis的概念与原理
1、了解它的概念与基本原理
首先,我们要知道,redis是开源的、内存型的、键值型的数据存储系统,英文Remote DIctionary Server(Redis) ,即远程字典服务。
redis由Salvatore Sanfilippo编写,采用ANSI C。作者来自意大利的西西里岛,现在居住在卡塔尼亚。目前供职于Pivotal公司。他使用的网名是antirez,如果你有兴趣,可以去他的博客逛逛,地址是antirez.com,当然也可以去follow他的github,地址是http://github.com/antirez。作为redis学习者,咱们来一睹作者的芳容,上图
2、数据存储方式
- 存储在内存中:redis是存储于内存中的,所以速度能达到纳秒级别,内存的速度和硬盘的速度不在一个量级,知道为什么redis这么快了吧。
- 持久化到硬盘:内存虽然快,但是内存中的数据在关机后会消失,因此redis采用了持久化方式写入硬盘,不然谁敢使用它,关机就消失那岂不是白存了。redis持久化分为两种方案,RDB持久化、AOF持久化,如图
RDB和AOF方式也是各有千秋,咱们可以根据不同的业务场景综合起来使用
3、开挂的单线程模型
redis为了节省线程开销,开足马力达到最大速度,因此使用的单线程模型,当然单线程也让redis避免了多线程中存在的各种资源恶性竞争,到这里,小伙伴们又开始困惑了,多线程不是比单线程要快吗?答案是,NO,看在什么情况下,如果是单线程操作内存的时候,是最快的。况且在单线程中,redis采用的IO多路复用,一个线程,照样可以同时进行多个IO操作,这就是Epoll模型的开挂之处。
由此可见,redis采用了以下几种方式,实现单线程开挂模式,达到飞一般的速度:
二、数据类型以及应用场景
1、数据存储结构
上面已经说过,Redis是存储于内存中的,因此,设计数据存储结构的时候,作者采用了兼顾性能和内存的一种对象来存储数据,作为Redis最基础的数据结构,叫做:redisObject,我们来看看这个对象的定义:
/* A redisobject, that is a type able to hold a string / list / set */
/* The actualRedis Object */
#define REDIS_LRU_BITS 24
#define REDIS_LRU_CLOCK_MAX ((1<<REDIS_LRU_BITS)-1)/* Max value of obj->lru */
#define REDIS_LRU_CLOCK_RESOLUTION 1000/* LRU clock resolution in ms */
typedef struct redisObject {
unsigned type:4; /* 表示redis的五种类型: STRING 0,LIST 1 ,SET 2,ZSET 3,HASH 4 */
unsigned encoding:4; /* 表示redis的五种类型的存储方式 */
unsigned lru:REDIS_LRU_BITS; /* lru time(relative to server.lruclock) */
int refcount;
void * ptr;
} robj;
懂c语言的小伙伴不难看出,redisObject的5种数据类型,可以搭配6种不同的存储方式,结构如下图所示:
既然是key-value键值对的方式,那我们的key采用的是字符串来存储,哪种方式的存储占用的空间最大呢,我们假设1000万的key的量,key长度和value长度都是相同的情况下,对比一下
由此可见,string类型是最少占用内存的,反之,ZSET类型占用内存最多,因此我们可以要根据业务的需要,合理搭配设计。不过,针对同一种数据类型,Redis也会根据元素的类型、大小、个数,采用不同的编码方式,这个咱们就控制不了,内部自动实现的,我们可以通过配置文件调整参数来实现搭配:
2、数据类型以及应用场景
string
- 介绍
string是key-value中最简单的数据类型,不过value有可能是string,或者int。注意,string类型在redis中是二进制安全(binary Safe)的,所以你可以用来存储很多东西,例如json,图片base64码等,最大可以存放512M的数据。
- 命令
set,get,del,decr,decrBy,incr,incrBy,mget 等
- 业务场景
(1)当做计数器
当redis的string类型的value值为整型或数值类型时,可以使
Incr/IncrBy/IncrByFloat/Decr/DecrBy(原子性)进行自增和自减操作。
我们可以用来做例如访问计数器、点赞数、播放次数、商品秒杀库存、接口防刷、服务降级、网络降速等等类似应用场景。
127.0.0.1:6379> set goods:stock:fsale 100
OK
127.0.0.1:6379> decr goods:stock:fsale
(integer) 99
(2)存储简单信息
例如存储用户的基本信息等,设计的时候可以这样:表名:键名:主键值:字段名 ,比如:
127.0.0.1:6379> set user:id:1:email 78163453@qq.com
OK
127.0.0.1:6379> get user:id:1:email
"78163453@qq.com"
(3)存储某些对象
我们可以把某些对象的json字符串存储起来,当然我们用hash来存储更方便,不过string占用的内存空间要比hash小,具体实施根据业务需要。例如我们用来存储用户对象的JSON信息,如下:
127.0.0.1:6379> set user:id:3 '{"id":3,"username":"loyo","avatar":"76.png","online":"1","token":"4b6e20df-...","user_id":3}'
OK
127.0.0.1:6379> get user:id:3
"{\"id\":3,\"username\":\"loyo\",\"avatar\":\"76.png\",\"online\":\"1\",\"token\":\"4b6e20df-...\",\"user_id\":3}"
(4)分布式锁
setNx在key不存在时才set,这种操作具有原子性,命令:SETNX key value。利用这种特性,我们可以用来实现分布式锁。当然完整的实现分布式锁,也可以用其他类型,而且实际操作还需要用到过期时间,以防止死锁等其他措施,这里只是抛砖引玉。
hash
- 介绍
redis的hash类型,指的是value的类型也是一种hash,就是key-value键值映射关系。hash的存储编码有两种,在少量key数量的情况下,为了节省内存,采用ZIPLIST来存储,大量key数量时,采用HT存储。
- 命令
hset、hget、hdel、hmget、hmset、hgetall、hincr、hincrby、hdecr、hdecrby、hsetNx等
127.0.0.1:6379> hmset user:3 username loyo birthyear 1900 sex 1
OK
127.0.0.1:6379> hget user:3 username
"loyo"
127.0.0.1:6379> hgetall user:3
1) "username"
2) "loyo"
3) "birthyear"
4) "1900"
5) "sex"
6) "1"
- 业务场景
(1)存储对象信息
刚才我们用string存储对象的信息的时候,是不是发现一个问题,如果我要修改或者删除对象里面的属性值,那怎么办,通常的操作就是把字符串转成对象,然后修改,然后在重新存储,这样是不是很麻烦。hash类型允许我们单独修改对象的属性值,是不是比string方便多了。
127.0.0.1:6379> hset user:3 username loyo2
(integer) 0
127.0.0.1:6379> hget user:3 username
"loyo2"
127.0.0.1:6379> hincrby user:3 birthyear 10
(integer) 1910
(2)频繁变更数据对象场景
对象存储在redis中时,如果频繁变更它的一些属性值,可以用hash来存储,例如,购物车、联系人等等。具体业务场景还得根据实际情况,选择更合适的类型。
list
- 介绍
list 是按照插入顺序排序的字符串链表,在头部和尾部插入新的元素(双向链表实现,两端添加元素的时间复杂度为 O(1))。插入元素时,如果 key 不存在,redis 会为该 key 创建一个新的链表,如果链表中所有的元素都被移除,该 key 自动从 redis 中移除。
- 命令
lpush、rpush、lpop、rpop、lrange等
- 业务场景
(1)、消息队列
既然是双向链表,那我们可以用左入右出,或者左出右入来实现队列的功能。可以用来做聊天的消息队列、粉丝关注列表等。
(2)、定时排行榜
list类型的lrange命令可以分页查看队列中的数据。可将每隔一段时间计算一次的排行榜存储在list类型中,如京东每日的手机销量排行、学校每次月考学生的成绩排名、斗鱼年终盛典主播排名等,每日计算一次,存储在list类型中,接口访问时,通过page和size分页获取。
set
- 介绍
set 数据类型是一堆string类型数据的无序的,不重复的集合,对 set 类型的数据进行添加、删除、判断是否存在等操作的时间复杂度O(1) ,因此set类型操作还是非常快的。
- 命令
基本操作:sadd、scard、spop、srem、smove、smembers、sismember、srandmember
交并差集操作:sinter\sinterstore、sunion\sunionstore、sdiff\sdiffstore、等等。
redis 127.0.0.1:6379> SADD myset1 "hello"
(integer) 1
redis 127.0.0.1:6379> SADD myset1 "world"
(integer) 1
redis 127.0.0.1:6379> SADD myset1 "bar"
(integer) 1
redis 127.0.0.1:6379> SADD myset2 "foo"
(integer) 1
redis 127.0.0.1:6379> SMOVE myset1 myset2 "bar"
(integer) 1
redis 127.0.0.1:6379> SMEMBERS myset1
1) "World"
2) "Hello"
redis 127.0.0.1:6379> SMEMBERS myset2
1) "foo"
2) "bar"
- 业务场景
(1)共同好友/关注/粉丝/感兴趣的人集合
set提供了求交集、并集、差集等操作,可以非常方便的实现如共同关注、共同喜好、二度好友、猜你认识等功能,对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集合中。
(2)随机展示,换一换功能
set的操作命令srandmember可以随机获取集合的指定数量的元素,格式是:srandmember mykey[10]。数量为正数时,会随机获取这么多个不重复的元素;如果数量大于集合元素个数,返回全部;如果数量为负,会随机获取这么多个元素,可能有重复。
127.0.0.1:6379> smembers myfriend:3
1) "u4"
2) "u3"
3) "loyo"
4) "u1"
5) "u5"
6) "lili"
7) "karl"
8) "u2"
9) "u6"
127.0.0.1:6379> srandmember myfriend:3 3
1) "u6"
2) "loyo"
3) "u1"
127.0.0.1:6379> srandmember myfriend:3 -3
1) "loyo"
2) "loyo"
3) "u1"
zset
- 介绍
zset的内部使用HashMap和跳跃表(SkipList)来存储,在set的基础上,加入一个集合score,用来对value集合中的数据进行排序,保证它的有序排列,并且在插入的时候就是有序的,即自动排序。
zset的多层SkipList结构图
redis给与zset分配编码方式有两种
- 命令
zadd:向指定集合zset中添加元素member,score用于排序,如果该元素已经存在,则更新其顺序
zrange:查看sourted sets里面的所有元素
zrem:删除名称为key的zset中的元素member(即删除指定zset里面的指定元素)
zincrby:如果在某一个zset中已经存在元素member,则该元素的score增加increment。否则向该集合中添加该元素,其score的值就为指定的increment值
zrank:返回某一个zset中指定元素的索引值(不是插入的时候指定的那个顺序值,是元素的下标)。这个索引值是按照元素的score值从小到大排列的,score值越小,索引值(下标)就越小,score值越大,索引值(下标)就越大
zrevrank:返回某一个zset中指定元素的索引值(不是插入的时候指定的那个顺序值,是元素的下标)。这个索引值是按照元素的score值从大到小排列的,score值越小,索引值(下标)就越大,score值越大,索引值(下标)就越小
zrevrange:返回某一个zset集合中的指定区间的元素及其顺序值,按照score值从大到小降序排列,与zrange相反
zrangebyscore:返回集合中指定顺序值区间的元素
zcount:返回集合中指定顺序值区间的元素总数量
zcard:返回集合中的所有元素个数
zremrangebyrank:删除在集合中排名在给定索引值(下标)区间的元素(注意:是按照索引值删除,这里不是顺序值)
zremrangebyscore:删除在集合中排名在给定顺序值区间的元素(注意:是按照顺序值删除,这里不是索引值)
- 业务场景
(1)有序的排行榜
利用zincrby hotNews:20200511 1 新闻标题,每次浏览新闻的时候,增加一次浏览量,然后我们最后统计出总的点击排行榜。
(2)热点排序的内容列表
用到的例如:zrevrange hotNews:xxxx 0 9 withscores 展示榜单前10
或者 zunionstore hotNews:20200501-20200507 7 等等
(3)好友亲密度排序
按照亲密度,对好友进行排榜。
(4)游戏战力排行榜
游戏中的战力排榜、帮派贡献排榜等等
三、redis分布式集群与运维
1、redis分布式集群
redis作为数据处理重要的一环,随着数据量越来越多,单机部署呈现出明显的性能问题,同时为了保证它的高可用性,我们必须实现redis分布式集群部署。redis目前最常用的分布式集群部署采用的主从模式,包括一主多从,多主多从等方式。
(1)主从复制,读写分离
此方案为最简单的集群方式,主节点master设置为写操作,从节点设置为只能进行读操作,将大大降低读写压力,数据同步是从主节点复制到各从节点。在业务系统发展初期可以使用这种简单的集群。
特点:
- 一个master有多个从节点slave,每个从节点只有个主节点;
- 数据同步方式是单向的,即数据由master流向slave,由master写数据,然后同步到slave;
(2)redis Cluster官方集群方案
redisCluster是官方推荐的集群方案,官方上说是这两个目的:
- 分区可以让Redis管理更大的内存,Redis将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。
- 分区使Redis的计算能力通过简单地增加计算机得到成倍提升,Redis的网络带宽也会随着计算机和网卡的增加而成倍增长。
redis Cluster集群节点网络通信机制
特点:
- 所有节点彼此互联,采用MEET-PING-PONG消息应答机制,内部使用二进制协议(cluster Bus)。
- 每个节点可再次进行主从集群,避免某个槽位的节点挂掉了影响分配,从而达到高可用。
- 业务系统客户端直接连接到各节点,不像Twemproxy那样需要代理节点。
- 把16384槽按照节点数量进行平均分配,由节点进行管理。
- 对每个key按照CRC16规则进行hash运算,把hash结果对16383进行取余,把余数发送给Redis节点。
- 节点接收到数据,验证是否在自己管理的槽编号的范围:
入槽: 如果在自己管理的槽编号范围内,则把数据保存到数据槽中,然后返回执行结果.
不入槽: 如果在自己管理的槽编号范围外,则会把数据发送给正确的节点,由正确的节点来把数据保存在对应的槽中.
2、redis运维与优化
redis在运维上,我们主要关注是的他高性能,高可用,以及常见的redis问题,例如数据一致性、主从复制风暴、网络延时、缓存击穿、缓存穿透、缓存雪崩等等。
(1)数据一致性问题
当redis缓存层的数据和SQL层的数据有差异时,我们通常会采用先去读取数据库的数据,然后在把新数据更新到redis缓存的方式,当然,不管你先删除缓存再写入数据,还是先写入数据库再删redis缓存,在高并发的情况下,都会出现数据不一致的问题。
- 解决办法1:采用延时双删策略
什么是延时双删?即在写入数据库前后都进行redis.del(key)操作,并且设定合理的超时时间。但是时间是多少,得自己根据业务量进行评估。但是时间设置不好,可能也会出现超时情况,导致问题再次出现。所以,此方法有一定的弊端。
- 解决办法2:基于订阅binlog异步更新redis
我们都知道,MySQL数据主从复制采用的是binlog来判定并进行增量更新,因此,我们可以采用中间件来读取binlog文件,从而拿到更新内容,再更新到redis。
中间件目前有阿里系的canal比较出名,当然你自己可以根据业务情况,选择kafka、rabbitMQ等来实现推送更新Redis缓存层,保证数据一致性。
(2)缓存穿透
缓存穿透是指查询一个数据库中不存在的数据,由于缓存中没有,在业务逻辑中,不命中时需要从数据库中查询,查不到数据则不写入缓存,这就将导致这个不存在的数据每次请求都要到数据库中查询,造成缓存穿透。
你可以通俗的理解,直接把redis穿过去了,没有利用到redis缓存。一般情况下,这种情况都是恶意用户查询不存在的值所导致。
- 解决办法1,缓存层保存业务层会遇到的穿透值,例如空值等
此方法遇到黑客使用不可预知的值来穿透时,没有防范措施,而且存放无用的值也是浪费内存。
- 解决办法2,利用布隆过滤器
利用布隆过滤器,先迅速的判定查询的值是否在集合中,不存在,则直接丢弃。bloomFilter原理就是一个对一个key进行k个hash算法获取k个值,在BIT数组中将这k个值散列后设定为1,因此,如果查的时候该key对应的HASH位置有一个为0,则该key不存在,我们便丢弃它。
注意,布隆过滤器如果用来判定是否存在,则可能会误判,如果它说不存在那肯定不存在,如果它说存在,那数据有可能实际不存在,所以,判定的时候一定要判定前者。具体原理,小伙伴们可以线下自行搜索原因,这里就不详细说了。
优点:
占用空间少,而且性能高:Redis的bitmap只支持2^32大小,对应到内存也就是512MB,误判率万分之一,可以放下2亿左右的数据。当然,如果数据大于2亿以上或更高,可采取横向扩展。
(3)缓存击穿
缓存击穿是针对redis中的热点key,很多热点key会承受很大的高并发,但是,如果这个热点key突然失效、过期或者被删除,则大量并发会击穿缓存层,击中物理数据库层,导致物理数据库承受不住压力而出现问题。
解决方案:
- 既然是缓存过期或者失效导致,那就避免这种情况,例如设置过期时间无限大,永不删除等措施;
- 使用互拆锁,当遇到某个key失效后,立即使用互拆锁,把其他操作全部挡在外部等待,等到重新获取数据之后,再删除锁,这样避免大量并发去查询物理数据库导致其瘫痪,但是用户体验会有所下降。可以在前端增加等待来避免用户体验问题。
(4)缓存雪崩
区别于击穿,我们把缓存雪崩定义为大面积的缓存失效,而且失效时间是非常巧合的一致,即是同一时刻,同时全部失效,即为雪崩(类似雪山崩塌)。
解决方案:
- 既然是集体失效,那么,我们可以设置失效的时间为随机,不要设置为固定值,避免集体事件发生;
- 我们在访问物理数据库时,加入限流措施、访问队列等方式,避免失效的时候,把数据库挤爆。
小伙伴们,看完这篇文章,你入门了多少?还有一些运维方面的会在后期总结写给你大家,貌似写太多了了,难以理解,有什么问题欢迎评论!