• 八股文之redis


    文章目录

    非关系型数据库之redis

    谈谈你对redis理解

    Redis(Remote Dictionary Server) 是开源的高性能非关系型键值对数据库,可以存储键和五种不同类型的值之间的映射,键类型只能是字符串,值支持五种数据类型:字符串,列表,集合,散列表,有序集合;和传统的管系统数据库不同redis基于内存处理的,读写速度非常快,故而redis常常用来作为缓存,分布式锁,事务,持久化机制,多种集群方案。

    Redis五种数据类型及应用场景(重点)

    STRING字符串、整数或者浮点数对整个字符串或者字符串的其中一部分执行操作 对整数和浮点数执行自增或者自减操作 :一个键最大能存储512MB1、分布式锁:SETNX(Key, Value),释放锁:DEL(Key),2、复杂计数功能缓存(用户量,视频播放量)
    LIST列表从两端压入或者弹出元素 对单个或者多个元素进行修剪, 只保留一个范围内的元素存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的数据,简单的消息队列的功能
    SET无序集合添加、获取、移除单个元素 检查一个元素是否存在于集合中 计算交集、并集、差集 从集合里面随机获取元素交集、并集、差集的操作,比如交集,可以把两个人的粉丝列表整一个交集,做全局去重的功能,点赞,转发,收藏;
    HASH包含键值对的无序散列表添加、获取、移除单个键值对 获取所有键值对 检查某个键是否存在结构化的数据,比如一个对象,单点登录
    ZSET有序集合添加、获取、删除元素 根据分值范围或者成员来获取元素 计算一个键的排名去重但可以排序,如获取排名前几名的用户,做排行榜应用,取TOPN操作;延时任务;做范围查找。周榜,月榜,年榜

    基本操作

    # 查询所有的键值对信息
    keys *
    
    • 1
    • 2

    字符串 String

    SET			--存入一个字符串键
    SETNX		--存入一个字符串键,若Key存在则操作失败
    GET			--获取指定Key的字符串
    MSET		--批量存入字符串键
    MGET		--批量获取指定Key的字符串
    DEL			--删除指定Key(所有类型都可以使用此命令)
    
    127.0.0.1:6379> set k2 v2
    OK
    127.0.0.1:6379> get k2
    "v2"
    127.0.0.1:6379> set k3 12
    OK
    127.0.0.1:6379> get k3
    "12"
    127.0.0.1:6379> set k4 12.24
    OK
    127.0.0.1:6379> get k4
    "12.24"
    127.0.0.1:6379> set k5 23.12f
    OK
    127.0.0.1:6379> get k5
    "23.12f"
    127.0.0.1:6379> set k6 true
    OK
    127.0.0.1:6379> get k6
    "true"
    127.0.0.1:6379> get k11
    "v1"
    127.0.0.1:6379> get k22
    "v3"
    127.0.0.1:6379> del key11
    (integer) 1
    127.0.0.1:6379> mget k4 k6
    1) "12.24"
    2) "true"
    127.0.0.1:6379> set k1 k2
    OK
    127.0.0.1:6379> setnx k1 v11
    (integer) 0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    列表 List

    LPUSH Key value [value...]			--往key的列表键中左边放入一个元素,key不存在则新建
    RPUSH Key value [value...]			--往key的列表键中右边放入一个元素,key不存在则新建
    LPOP Key							--从key的列表键最左端弹出一个元素
    RPOP Key							--从key的列表键最右端弹出一个元素
    LRANGE Key start stop				--获取列表键从start下标到stop下标的元素
    
    eg:案例说明
    127.0.0.1:6379> lpush list1 99
    (integer) 10
    127.0.0.1:6379> lpop list1
    "99"
    127.0.0.1:6379> rpop list1
    "l44"
    127.0.0.1:6379> lpush list1 87
    (integer) 9
    127.0.0.1:6379> lrange list1 0 10
    1) "87"
    2) "23"
    3) "l4"
    4) "l3"
    5) "l2"
    6) "l1"
    7) "l11"
    8) "l22"
    9) "l33"
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    无序集合 SET

    SADD Key member [member...]			--往集合键key中存放元素,若key不存在则新建
    SREM Key member [member...]			--从集合键key中删除元素
    SMEMBERS Key						--获取集合键key中的所有元素
    SCARD Key							--获取集合键key中的元素个数
    SISMEMBER Key member				--判断{member}元素是否存在于集合键key中
    SRANDMEMBER Key [count]				--从集合键key中选出{count}个元素,不从集合键key中删除
    SPOP Key [count]					--从集合键key中选出{count}个元素,并且从集合键key中删除
    eg:案例说明
    127.0.0.1:6379> sadd set001 1
    (integer) 1
    127.0.0.1:6379> sadd set001 2 3 4
    (integer) 3
    127.0.0.1:6379> srem set001 1
    (integer) 1
    127.0.0.1:6379> smembers set001
    1) "2"
    2) "3"
    3) "4"
    127.0.0.1:6379> scard set001
    (integer) 3
    127.0.0.1:6379> sismember set001 1
    (integer) 0
    127.0.0.1:6379> sismember set001 2
    (integer) 1
    127.0.0.1:6379> srandmember set001 2
    1) "4"
    2) "2"
    127.0.0.1:6379> srandmember set001 2
    1) "3"
    2) "2"
    127.0.0.1:6379> spop set001 1
    1) "3"
    127.0.0.1:6379> srandmember set001 1
    1) "2"
    127.0.0.1:6379> smembers set001
    1) "2"
    2) "4"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    散列表 HASH

    hash
    HSET			--存入一个key filed 散列结构
    HSETNX			--存入一个key field,若key中filed已经存在则操作失败
    HGET			--获取指定key field
    HMSET			--批量存入key filed
    HMGET			--批量获取key filed
    HDEL			--删除指定Key filed
    HINCRBY			--对key field的数值进行加减操作
    eg:案例练习
    127.0.0.1:6379> hset hset002 name '24dsf'
    (integer) 1
    127.0.0.1:6379> hget hset002 name
    "24dsf"
    127.0.0.1:6379> hmset hset002 age 98.2f sex 'nan'
    OK
    127.0.0.1:6379> hmget hset002 age sex
    1) "98.2f"
    2) "nan"
    127.0.0.1:6379> hdel hset002 s1
    (integer) 1
    127.0.0.1:6379> hset hset002 age1 98
    (integer) 1
    127.0.0.1:6379> hincrby hset002 age1 2
    (integer) 100
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    有序集合 ZSET

    ZADD Key score element [...]			--往有序集合键key中存放元素,若key不存在则新建
    ZREM Key element [element...]			--从有序集合键key中删除元素
    ZSCORE Key element						--获取有序集合键key中{element}元素的score值
    ZINCRBY Key increment element			--给有序集合键key中{element}元素进行score值操作,若key不存在则新建,{element}元素不存在则新增后进行score值操作
    ZCARD Key								--获取有序集合键key中元素个数
    ZRANGE Key start stop [WITHSCORES]		--正序获取有序集合键key中从start下标到end下标的元素
    ZREVRANGE Key start stop [WITHSCORES]	--倒序获取有序集合键key中从start下标到end下标的元素
    # 添加
    127.0.0.1:6379> zadd  zset001 1 redis
    (integer) 1
    127.0.0.1:6379> zadd zset001 2 mysql
    (integer) 1
    127.0.0.1:6379> zadd zset001 3 oracle
    (integer) 1
    # 删除
    127.0.0.1:6379> zrem zset001 mysql
    # 查找指定元素的score
    (integer) 1
    127.0.0.1:6379> zscore zset001 redis
    "1"
    127.0.0.1:6379> zcard zset001
    (integer) 2
    127.0.0.1:6379> zrange zset001 0 1
    1) "redis"
    2) "mongodb"
    127.0.0.1:6379> zrange zset001 0 0
    1) "redis"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    为什么使用redis,或者说为什么用缓存

    主要从“高性能”和“高并发”这两点来看待这个问题。

    假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!

    高并发:

    直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。

    为什么要用 Redis 而不用 map 做缓存?

    ​ 缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。

    ​ 使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。

    Redis为什么这么快

    完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中

    采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU

    能说一下RDB和AOF的实现原理吗?(必备)

    1、持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。

    2、rdb:rdb:快照方式,按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。

    aop:aof:将数据每隔一秒追加到文件中。

    3、两者区别联系

    • AOF文件比RDB更新频率高,优先使用AOF还原数据。
    • AOF比RDB更安全也更大
    • RDB性能比AOF好
    • 如果两个都配了优先加载AOF

    参考:

    RDB:

    优点:

    1. 非常紧凑的文件

    2. 恢复速度快

    3. 存储效率高

    4. 适用于灾难恢复

    缺点:

    1. 容易丢失数据

    2. 消耗内存

    3. 宕机带来的数据丢失风险

    4. 存储数据量大时,效率很低

    AOF:

    优点:

    1. 数据安全

    2. 每一秒记录一次

    缺点:

    1. 在宕机时,会丢失1秒的数据

    2. 恢复速度慢

    Redis的过期键的删除策略

    • 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。
    • 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
    • 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。

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

    redis淘汰策略以及应用场景

    Redis官方给的警告,当内存不足时,Redis会根据配置的缓存策略淘汰部分keys,以保证写入成功。当无淘汰策略时或没有找到适合淘汰的key时,Redis直接返回out of memory错误。

    1、volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰

    2、volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰

    3、volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰

    4、allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰

    5、allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰

    6、no-enviction(驱逐):禁止驱逐数据

    应用场景:
    1).在Redis中,数据有一部分访问频率较高,其余部分访问频率较低,或者无法预测数据的使用频率时,设置allkeys-lru是比较合适的。
    2). 如果所有数据访问概率大致相等时,可以选择allkeys-random。
    3). 如果研发者需要通过设置不同的ttl来判断数据过期的先后顺序,此时可以选择volatile-ttl策略。
    4). 如果希望一些数据能长期被保存,而一些数据可以被淘汰掉时,选择volatile-lru或volatile-random都是比较不错的。
    5). 由于设置expire会消耗额外的内存,如果计划避免Redis内存在此项上的浪费,可以选用allkeys-lru 策略,这样就可以不再设置过期时间,高效利用内存了。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    Redis key的过期时间和永久有效分别怎么设置?

    EXPIRE和PERSIST命令。

    我们知道通过expire来设置key 的过期时间,那么对过期的数据怎么处理呢?

    1. 定时去清理过期的缓存;
    2. 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。

    Redis如何做内存优化?

    ​ 可以好好利用Hash,list,sorted set,set等集合类型数据,因为通常情况下很多小的Key-Value可以用更紧凑的方式存放到一起。尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面。

    缓存雪崩(必备)

    缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

    解决方案

    1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。

    2. 一般并发量是特别多的时候,使用最多的解决方案是加锁排队。

    3. redis高可用

      1. redis有可能挂掉,多增加几台redis实例,(一主多从或者多主多从),这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
    4. 限流降级

      1. 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量,对某个key只允许一个线程查询数据和写缓存,其他线程等待。
    5. 数据预热

      1. 数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key。

    举例说明:

    ​ 比如我们基本上都经历过购物狂欢节,假设商家举办 23:00-24:00 商品打骨折促销活动。程序小哥哥在设计的时候,在 23:00 把商家打骨折的商品放到缓存中,并通过redis的expire设置了过期时间为1小时。这个时间段许多用户访问这些商品信息、购买等等。但是刚好到了24:00点的时候,恰好还有许多用户在访问这些商品,这时候对这些商品的访问都会落到数据库上,导致数据库要抗住巨大的压力,稍有不慎会导致,数据库直接宕机(over)

    缓存穿透(必备)

    缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

    解决方案

    1. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
    2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
    3. 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力

    缓存击穿(必备)

    缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

    解决方案

    1. 设置热点数据永远不过期。
    2. 加互斥锁,互斥锁

    归纳起来:造成缓存击穿的原因有两个。

    (1)一个“冷门”key,突然被大量用户请求访问。

    (2)一个“热门”key,在缓存中时间恰好过期,这时有大量用户来进行访问。

    Redis实现分布式锁(重点)

    想要实现分布式锁,必须要求 Redis 有「互斥」的能力,我们可以使用 SETNX 命令,这个命令表示SET if Not eXists,即如果 key 不存在,才会设置它的值,否则什么也不做。

    两个客户端进程可以执行这个命令,达到互斥,就可以实现一个分布式锁。

    客户端 1 申请加锁,加锁成功:

    127.0.0.1:6379> SETNX lock 1
    (integer) 1     // 客户端1,加锁成功
    
    • 1
    • 2

    客户端 2 申请加锁,因为它后到达,加锁失败:

    127.0.0.1:6379> SETNX lock 1
    (integer) 0     // 客户端2,加锁失败
    
    • 1
    • 2

    此时,加锁成功的客户端,就可以去操作「共享资源」,例如,修改 MySQL 的某一行数据,或者调用一个 API 请求。

    操作完成后,还要及时释放锁,给后来者让出操作共享资源的机会。如何释放锁呢?

    也很简单,直接使用 DEL 命令删除这个 key 即可:

    127.0.0.1:6379> DEL lock // 释放锁
    (integer) 1
    
    • 1
    • 2

    这个逻辑非常简单,整体的路程就是这样:

    图片

    但是,它存在一个很大的问题,当客户端 1 拿到锁后,如果发生下面的场景,就会造成「死锁」:

    1. 程序处理业务逻辑异常,没及时释放锁
    2. 进程挂了,没机会释放锁

    这时,这个客户端就会一直占用这个锁,而其它客户端就「永远」拿不到这把锁了。

    怎么解决这个问题呢?

    如何避免死锁?

    我们很容易想到的方案是,在申请锁时,给这把锁设置一个「租期」。

    在 Redis 中实现时,就是给这个 key 设置一个「过期时间」。这里我们假设,操作共享资源的时间不会超过 10s,那么在加锁时,给这个 key 设置 10s 过期即可:

    127.0.0.1:6379> SETNX lock 1    // 加锁
    (integer) 1
    127.0.0.1:6379> EXPIRE lock 10  // 10s后自动过期
    (integer) 1
    
    • 1
    • 2
    • 3
    • 4

    这样一来,无论客户端是否异常,这个锁都可以在 10s 后被「自动释放」,其它客户端依旧可以拿到锁。

    但这样真的没问题吗?

    还是有问题。

    现在的操作,加锁、设置过期是 2 条命令,有没有可能只执行了第一条,第二条却「来不及」执行的情况发生呢?例如:

    1. SETNX 执行成功,执行 EXPIRE 时由于网络问题,执行失败
    2. SETNX 执行成功,Redis 异常宕机,EXPIRE 没有机会执行
    3. SETNX 执行成功,客户端异常崩溃,EXPIRE 也没有机会执行

    总之,这两条命令不能保证是原子操作(一起成功),就有潜在的风险导致过期时间设置失败,依旧发生「死锁」问题。

    怎么办?

    在 Redis 2.6.12 版本之前,我们需要想尽办法,保证 SETNX 和 EXPIRE 原子性执行,还要考虑各种异常情况如何处理。

    但在 Redis 2.6.12 之后,Redis 扩展了 SET 命令的参数,用这一条命令就可以了:

    // 一条命令保证原子性执行
    127.0.0.1:6379> SET lock 1 EX 10 NX
    OK
    
    • 1
    • 2
    • 3

    这样就解决了死锁问题,也比较简单。

    我们再来看分析下,它还有什么问题?

    试想这样一种场景:

    1. 客户端 1 加锁成功,开始操作共享资源
    2. 客户端 1 操作共享资源的时间,「超过」了锁的过期时间,锁被「自动释放」
    3. 客户端 2 加锁成功,开始操作共享资源
    4. 客户端 1 操作共享资源完成,释放锁(但释放的是客户端 2 的锁)

    看到了么,这里存在两个严重的问题:

    1. 锁过期:客户端 1 操作共享资源耗时太久,导致锁被自动释放,之后被客户端 2 持有
    2. 释放别人的锁:客户端 1 操作共享资源完成后,却又释放了客户端 2 的锁

    导致这两个问题的原因是什么?我们一个个来看。

    第一个问题,可能是我们评估操作共享资源的时间不准确导致的。

    例如,操作共享资源的时间「最慢」可能需要 15s,而我们却只设置了 10s 过期,那这就存在锁提前过期的风险。

    过期时间太短,那增大冗余时间,例如设置过期时间为 20s,这样总可以了吧?

    这样确实可以「缓解」这个问题,降低出问题的概率,但依旧无法「彻底解决」问题。

    为什么?

    原因在于,客户端在拿到锁之后,在操作共享资源时,遇到的场景有可能是很复杂的,例如,程序内部发生异常、网络请求超时等等。

    既然是「预估」时间,也只能是大致计算,除非你能预料并覆盖到所有导致耗时变长的场景,但这其实很难。

    有什么更好的解决方案吗?

    别急,关于这个问题,我会在后面详细来讲对应的解决方案。

    我们继续来看第二个问题。

    第二个问题在于,一个客户端释放了其它客户端持有的锁。

    想一下,导致这个问题的关键点在哪?

    重点在于,每个客户端在释放锁时,都是「无脑」操作,并没有检查这把锁是否还「归自己持有」,所以就会发生释放别人锁的风险,这样的解锁流程,很不「严谨」!

    如何解决这个问题呢?

    锁被别人释放怎么办?

    解决办法是:客户端在加锁时,设置一个只有自己知道的「唯一标识」进去。

    例如,可以是自己的线程 ID,也可以是一个 UUID(随机且唯一),这里我们以 UUID 举例:

    // 锁的VALUE设置为UUID
    127.0.0.1:6379> SET lock $uuid EX 20 NX
    OK
    
    • 1
    • 2
    • 3

    这里假设 20s 操作共享时间完全足够,先不考虑锁自动过期的问题。

    之后,在释放锁时,要先判断这把锁是否还归自己持有,伪代码可以这么写:

    // 锁是自己的,才释放
    if redis.get("lock") == $uuid:
        redis.del("lock")
    
    • 1
    • 2
    • 3

    这里释放锁使用的是 GET + DEL 两条命令,这时,又会遇到我们前面讲的原子性问题了。

    1. 客户端 1 执行 GET,判断锁是自己的
    2. 客户端 2 执行了 SET 命令,强制获取到锁(虽然发生概率比较低,但我们需要严谨地考虑锁的安全性模型)
    3. 客户端 1 执行 DEL,却释放了客户端 2 的锁

    由此可见,这两个命令还是必须要原子执行才行。

    怎样原子执行呢?Lua 脚本。

    我们可以把这个逻辑,写成 Lua 脚本,让 Redis 来执行。

    因为 Redis 处理每一个请求是「单线程」执行的,在执行一个 Lua 脚本时,其它请求必须等待,直到这个 Lua 脚本处理完成,这样一来,GET + DEL 之间就不会插入其它命令了。

    图片

    安全释放锁的 Lua 脚本如下:

    // 判断锁是自己的,才释放
    if redis.call("GET",KEYS[1]) == ARGV[1]
    then
        return redis.call("DEL",KEYS[1])
    else
        return 0
    end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    好了,这样一路优化,整个的加锁、解锁的流程就更「严谨」了。

    这里我们先小结一下,基于 Redis 实现的分布式锁,一个严谨的的流程如下:

    1. 加锁:SET lock_key $unique_id EX $expire_time NX
    2. 操作共享资源
    3. 释放锁:Lua 脚本,先 GET 判断锁是否归属自己,再 DEL 释放锁

    图片

    好,有了这个完整的锁模型,让我们重新回到前面提到的第一个问题。

    锁过期时间不好评估怎么办?

    锁过期时间不好评估怎么办?

    前面我们提到,锁的过期时间如果评估不好,这个锁就会有「提前」过期的风险。

    当时给的妥协方案是,尽量「冗余」过期时间,降低锁提前过期的概率。

    这个方案其实也不能完美解决问题,那怎么办呢?

    是否可以设计这样的方案:加锁时,先设置一个过期时间,然后我们开启一个「守护线程」,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行「续期」,重新设置过期时间。

    这确实一种比较好的方案。

    如果你是 Java 技术栈,幸运的是,已经有一个库把这些工作都封装好了:Redisson

    Redisson 是一个 Java 语言实现的 Redis SDK 客户端,在使用分布式锁时,它就采用了「自动续期」的方案来避免锁过期,这个守护线程我们一般也把它叫做「看门狗」线程。

    图片

    除此之外,这个 SDK 还封装了很多易用的功能:

    • 可重入锁
    • 乐观锁
    • 公平锁
    • 读写锁
    • Redlock(红锁,下面会详细讲)

    这个 SDK 提供的 API 非常友好,它可以像操作本地锁的方式,操作分布式锁。如果你是 Java 技术栈,可以直接把它用起来。

    这里不重点介绍 Redisson 的使用,大家可以看官方 Github 学习如何使用,比较简单。

    到这里我们再小结一下,基于 Redis 的实现分布式锁,前面遇到的问题,以及对应的解决方案:

    • 死锁:设置过期时间
    • 过期时间评估不好,锁提前过期:守护线程,自动续期
    • 锁被别人释放:锁写入唯一标识,释放锁先检查标识,再释放

    如何保证Redis与数据库的数据一致性(重点)

    问题解决思路
    先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致 先删除缓存,再更新数据库。如果数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,所以去读了数据库中的旧数据,然后更新到缓存中
    数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。完了,数据库和缓存中的数据不一样了 方案一:写请求先删除缓存,再去更新数据库,(异步等待段时间)再删除缓存(成功表示有脏数据出现);这种方案读取快速,但会出现短时间的脏数据。 方案二:写请求先修改缓存为指定值,再去更新数据库,再更新缓存。读请求过来后,先读缓存,判断是指定值后进入循环状态,等待写请求更新缓存。如果循环超时就去数据库读取数据,更新缓存。这种方案保证了读写的一致性,但是读请求会等待写操作的完成,降低了吞吐量。

    redis中单点模式,主从模式,哨兵模式,集群模式

    主从模式

    1.一个Master可以有多个Slaves,可以是1主N从。

    2.默认配置下,master节点可以进行读和写,slave节点只能进行读操作,写操作被禁止(readonly)。

    3.不要修改配置让slave节点支持写操作,没有意义,原因一,写入的数据不会被同步到其他节点;原因二,当master节点修改同一条数据后,slave节点的数据会被覆盖掉。

    4.slave节点挂了不影响其他slave节点的读和master节点的读和写,重新启动后会将数据从master节点同步过来。

    5.master节点挂了以后,不影响slave节点的读,Redis将不再提供写服务,master节点启动后Redis将重新对外提供写服务。

    6.特别说明:该种模式下,master节点挂了以后,slave不会竞选成为master。

    哨兵模式

    ​ 宕机的主节点下线了,

    ​ 找到一个slave作为master,

    ​ 通知所有slave连接新的master,

    ​ 启动新的master和slave,

    ​ 全量复制N + 部分复制N

    问题?

    ​ 谁来确认master宕机了

    ​ 找一个主节点,怎么找?

    ​ 修改配置后,原来主节点恢复了怎么办?

    ​ 哨兵sentinel是一个分布式系统,用于对主从结构中的每台服务器进行监控,当出现故障时候通过投票选择新的master并将所有slave连接新的master;

    ​ 作用

    ​ 监控:

    ​ 不断检查master和slave是否正常运行

    ​ master存活检测,master和slave运行情况检测

    ​ 通知

    ​ 当被监控的服务器出现问题时,向其他哨兵或者客户端进行通知

    自动故障转移

    ​ 断开master和slave连接,选取一个slave作为master,将其他slave连接到新的master上,并且告知客户端新的服务器地址。

    注意:哨兵也是一台服务器,只是不提供数据服务,通常哨兵配置单数个

    集群模式

    cluster的出现是为了解决单机Redis容量有限的问题,将Redis的数据根据一定的规则分配到多台机器。对cluster的一些理解:

    一个 Redis 集群包含 16384 个哈希槽(hash slot),数据库中的每个键都属于这 16384 个哈希槽的其中一个,集群中的每个节点负责处理一部分哈希槽。

    例如一个集群有三个主节点,其中:

    节点 A 负责处理 0 号至 5500 号哈希槽。

    节点 B 负责处理 5501 号至 11000 号哈希槽。

    节点 C 负责处理 11001 号至 16384 号哈希槽。

    这种将哈希槽分布到不同节点的做法使得用户可以很容易地向集群中添加或者删除节点。例如:如果用户将新节点 D 添加到集群中, 那么集群只需要将节点 A 、B 、 C 中的某些槽移动到节点 D 就可以了。

    如果用户要从集群中移除节点 A , 那么集群只需要将节点 A 中的所有哈希槽移动到节点 B 和节点 C , 然后再移除空白(不包含任何哈希槽)的节点 A 就可以了。

    这里需要注意的是,集群如果是5主5从,主节点也是16384个hash slot,而不会因为主节点的增多slot也增多。我们在分槽的时候,尽量把槽平均分给主节点。因为一个key落在哪个槽里面,是根据key的CRC16值模上16384得出的值来计算的。

    2.Redis 集群对节点使用了主从复制功能: 集群中的每个节点都有 1 个至 N 个复制品(replica), 其中一个复制品为主节点(master), 而其余的 N-1 个复制品为从节点(slave)。

    我们知道集群模式下,1主N从时,当主节点挂掉时,从节点通过心跳监听机制,会竞选成为主节点(这时设置的readonly会失效),所以在部署的时候,主从节点应该部署在不同的机器上,这个时候如果主节点的服务器宕机,从节点竞选成功后会继续承担读写的任务。

    redis主从复制,读写分离

    当启动一个 slave node 的时候,它会发送一个 PSYNC 命令给 master node。

    如果这是 slave node 初次连接到 master node,那么会触发一次 full resynchronization 全量复制。此时 master 会启动一个后台线程,开始生成一份 RDB 快照文件,

    同时还会将从客户端 client 新收到的所有写命令缓存在内存中。RDB 文件生成完毕后, master 会将这个 RDB 发送给 slave,slave 会先写入本地磁盘,然后再从本地磁盘加载到内存中,

    接着 master 会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据。

    slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据

    主从复制几种方式

    1. 一主二仆 A(B、C) 一个Master两个Slave
    2. 薪火相传(去中心化)A - B - C ,B既是主节点(C的主节点),又是从节点(A的从节点)
    3. 反客为主(主节点down掉后,手动操作升级从节点为主节点) & 哨兵模式(主节点down掉后,自动升级从节点为主节点)

    如何解决redis并发竞争的key问题(重点)

    Redis为单进程单线程模式,采用队列模式将并发访问变为串行访问。Redis本身没有锁的概念,Redis对于多个客户端连接并不存在竞争,但是在Jedis客户端对Redis进行并发访问时会发生连接超时、数据转换错误、阻塞、客户端关闭连接等问题,这些问题均是由于客户端连接混乱造成。对此有2种解决方法:

    第一种方案:分布式锁+时间戳

    这种情况,主要是准备一个分布式锁,大家去抢锁,抢到锁就做set操作。

    加锁的目的实际上就是把并行读写改成串行读写的方式,从而来避免资源竞争。

    分布式锁

    1. 加锁:SET lock_key $unique_id EX $expire_time NX
    2. 操作共享资源
    3. 释放锁:Lua 脚本,先 GET 判断锁是否归属自己,再 DEL 释放锁

    问题

    1、如何避免死锁

    2、锁被别人释放怎么办

    3、锁过期时间不好评估

    时间戳

    要求key的操作需要顺序执行,所以需要保存一个时间戳判断set顺序

    第二种方案:利用消息队列

    在并发量过大的情况下,可以通过消息中间件进行处理,把并行读写进行串行化。

    把Redis.set操作放在队列中使其串行化,必须的一个一个执行。

    这种方式在一些高并发的场景中算是一种通用的解决方案。

    redis实现异步队列?

    ​ 使用list类型保存数据信息,rpush生产消息,lpop消费消息,当lpop没有消息时,可以sleep一段时间,然后再检查有没有信息,如果不想sleep的话,可以使用blpop, 在没有信息的时候,会一直阻塞,直到信息的到来。

    ​ redis可以通过pub/sub主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。

    如果对方追问pub/sub有什么缺点?在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq等。

    使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。

    redis实现延时队列

    之前用rabbitMQ实现延时队列,Redis由于其自身的Zset数据结构,也同样可以实现延时的操作

    Redis回收使用的是什么算法?

    最近使用算法LRU

    redis使用特点,理解

    • 缓存和数据库双写一致性问题
    • 缓存雪崩,击穿,穿透问题
    • 缓存的并发竞争问题

    注意:

    redis查看日志:使用 redis-cli 连上后执行 INFO 命令就行

    redis在默认情况下,是不会生成日志文件的,所以需要配置

    理解:当我们的一些数据比较固定的时候,发送1次请求和发送1000次请求获得的结果都是一样的。所以后面的999次请求都是做一些重复耗时、没有意义的工作。

    举例说明:用户发送请求查询所需要的数据,首先从缓存中来获取,若是缓存中有,直接在缓存直接获取,请求结束。

    若是缓存没有需要的数据,查询数据库,获取想要的数据,然后把想要的数据set到缓存中,下次访问相同的数据直接从缓存获取。

    关系型数据库和非关系型数据库

    关系型数据库
    易于维护,使用方便,复杂操作,但是对海量数据读写不友好,灵活度不高
    非关系型数据库
    格式灵活,速度快,成本低,但是不支持事务处理。

    假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?(必备)

    使用keys指令可以扫出指定模式的key列表:keys pre*,这个时候面试官会追问该命令对线上业务有什么影响,直接看下一个问题。

    如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?(必备)

    redis 的单线程的。keys 指令会导致线 程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时 候可以使用 scan 指令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间 会比直接用 keys 指令长。

    如果有大量的key需要设置同一时间过期,一般需要注意什么?

    如果大量的key过期时间设置的过于集中,到过期的那个时间点,redis可能会出现短暂的卡顿现象。一般需要在时间上加一个随机值,使得过期时间分散一些。

    Redis如何做持久化的?

    bgsave做镜像全量持久化,aof做增量持久化。因为bgsave会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要aof来配合使用。在redis实例重启时,会使用bgsave持久化文件重新构建内存,再使用aof重放近期的操作指令来实现完整恢复重启之前的状态。

    对方追问那如果突然机器掉电会怎样?取决于aof日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据。

    对方追问bgsave的原理是什么?你给出两个词汇就可以了,fork和cow。fork是指redis通过创建子进程来进行bgsave操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。

    是否使用过Redis集群,集群的原理是什么?

    Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务

    Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储

    Redis常用的客户端有哪些

    Jedis:是Redis的Java实现客户端,提供了比较全面的Redis命令的支持
    Redisson:实现了分布式和可扩展的Java数据结构
    Lettuce:高级Redis客户端,用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器
    1)优点:
      Jedis:比较全面的提供了Redis的操作特性
      Redisson:促使使用者对Redis的关注分离,提供很多分布式相关操作服务,例如,分布式锁,分布式集合,可通过Redis支持延迟队列
      Lettuce:主要在一些分布式缓存框架上使用比较多
    2)可伸缩:
    Jedis:使用阻塞的I/O,且其方法调用都是同步的,程序流需要等到sockets处理完I/O才能执行,不支持异步。Jedis客户端实例不是线程安全的,所以需要通过连接池来使用Jedis。
    Redisson:基于Netty框架的事件驱动的通信层,其方法调用是异步的。Redisson的API是线程安全的,所以可以操作单个Redisson连接来完成各种操作
    Lettuce:基于Netty框架的事件驱动的通信层,其方法调用是异步的。Lettuce的API是线程安全的,所以可以操作单个Lettuce连接来完成各种操作

    WATCH命令和基于CAS的乐观锁

    在Redis的事务中,WATCH命令可用于提供CAS(check-and-set)功能。假设咱们经过WATCH命令在事务执行以前监控了多个Keys,假若在WATCH以后有任何Key的值发生了变化,EXEC命令执行的事务都将被放弃,同时返回Null multi-bulk应答以通知调用者事务执行失败。例如,咱们再次假设Redis中并未提供incr命令来完成键值的原子性递增,若是要实现该功能,咱们只能自行编写相应的代码。其伪码以下:

    val = GET mykey
    val = val + 1
    SET mykey $val
    
    • 1
    • 2
    • 3

    以上代码只有在单链接的状况下才能够保证执行结果是正确的,由于若是在同一时刻有多个客户端在同时执行该段代码,那么就会出现多线程程序中常常出现的一种 错误场景–竞态争用(race condition)。好比,客户端A和B都在同一时刻读取了mykey的原有值,假设该值为10,此后两个客户端又均将该值加一后set回Redis服 务器,这样就会致使mykey的结果为11,而不是咱们认为的12。为了解决相似的问题,咱们须要借助WATCH命令的帮助,见以下代码:

    WATCH mykey
    val = GET mykey
    val = val + 1
    MULTI
    SET mykey $val
    EXEC
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    和此前代码不一样的是,新代码在获取mykey的值以前先经过WATCH命令监控了该键,此后又将set命令包围在事务中,这样就能够有效的保证每一个链接在 执行EXEC以前,若是当前链接获取的mykey的值被其它链接的客户端修改,那么当前链接的EXEC命令将执行失败。这样调用者在判断返回值后就能够获 悉val是否被从新设置成功

    redis事务的了解CAS

    和众多其它数据库一样,Redis作为NoSQL数据库也同样提供了事务机制。在Redis中,MULTI/EXEC/DISCARD/WATCH这四个命令是我们实现事务的基石。相信对有关系型数据库开发经验的开发者而言这一概念并不陌生,即便如此,我们还是会简要的列出

    Redis中

    事务的实现特征:

    在事务中的所有命令都将会被串行化的顺序执行,事务执行期间,Redis不会再为其它客户端的请求提供任何服务,从而保证了事务中的所有命令被原子的执行

    和关系型数据库中的事务相比,在Redis事务中如果有某一条命令执行失败,其后的命令仍然会被继续执行

    我们可以通过MULTI命令开启一个事务,有关系型数据库开发经验的人可以将其理解为"BEGIN TRANSACTION"语句。在该语句之后执行的命令都将被视为事务之内的操作,最后我们可以通过执行EXEC/DISCARD命令来提交/回滚该事务内的所有操作。这两个Redis命令可被视为等同于关系型数据库中的COMMIT/ROLLBACK语句

    在事务开启之前,如果客户端与服务器之间出现通讯故障并导致网络断开,其后所有待执行的语句都将不会被服务器执行。然而如果网络中断事件是发生在客户端执行EXEC命令之后,那么该事务中的所有命令都会被服务器执行

    当使用Append-Only模式时,Redis会通过调用系统函数write将该事务内的所有写操作在本次调用中全部写入磁盘。然而如果在写入的过程中出现系统崩溃,如电源故障导致的宕机,那么此时也许只有部分数据被写入到磁盘,而另外一部分数据却已经丢失。Redis服务器会在重新启动时执行一系列必要的一致性检测,一旦发现类似问题,就会立即退出并给出相应的错误提示。此时,我们就要充分利用Redis工具包中提供的redis-check-aof工具,该工具可以帮助我们定位到数据不一致的错误,并将已经写入的部分数据进行回滚。修复之后我们就可以再次重新启动Redis服务器了

    redis常见性能问题和解决方案(重点)

    Master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以Master最好不要写内存快照

    Master AOF持久化,如果不重写AOF文件,这个持久化方式对性能的影响是最小的,但是AOF文件会不断增大,AOF文件过大会影响Master重启的恢复速度。Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化,如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次

    Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象

    Redis主从复制的性能问题,为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内

  • 相关阅读:
    力扣接雨水(解析)
    WebGL实现简易的局部“马赛克”
    企业管理的基本知识有哪些?如何梳理企业流程管理?
    Hbase 笔记
    类和对象(上)--关于面向对象,类的定义,访问限定符,this指针
    DolphinDB Python API 离线安装教程
    LLVM系列(1)- LLVM简介
    【老生谈算法】matlab绘制温度场原理——温度场原理
    孕妇胃烧心是胎儿长头发?其实是因为这2点
    css为盒子设置滚动条&隐藏滚动条
  • 原文地址:https://blog.csdn.net/greek7777/article/details/126256043