Redis
Linux安装
【狂神说Java】Redis最新超详细版教程通俗易懂_哔哩哔哩_bilibili
Keys
序号 | 命令及描述 |
---|---|
1 | DEL key 该命令用于在 key 存在时删除 key。 |
2 | DUMP key 序列化给定 key ,并返回被序列化的值。 |
3 | EXISTS key 检查给定 key 是否存在。 |
4 | EXPIRE key seconds 为给定 key 设置过期时间,以秒计。 |
5 | EXPIREAT key timestamp EXPIREAT 的作用和 EXPIRE 类似,都用于为 key 设置过期时间。 不同在于 EXPIREAT 命令接受的时间参数是 UNIX 时间戳(unix timestamp)。 |
6 | PEXPIRE key milliseconds 设置 key 的过期时间以毫秒计。 |
7 | PEXPIREAT key milliseconds-timestamp 设置 key 过期时间的时间戳(unix timestamp) 以毫秒计 |
8 | KEYS pattern 查找所有符合给定模式( pattern)的 key 。 |
9 | MOVE key db 将当前数据库的 key 移动到给定的数据库 db 当中。 |
10 | PERSIST key 移除 key 的过期时间,key 将持久保持。 |
11 | PTTL key 以毫秒为单位返回 key 的剩余的过期时间。 |
12 | TTL key 以秒为单位,返回给定 key 的剩余生存时间(TTL, time to live)。 |
13 | RANDOMKEY 从当前数据库中随机返回一个 key 。 |
14 | RENAME key newkey 修改 key 的名称 |
15 | RENAMENX key newkey 仅当 newkey 不存在时,将 key 改名为 newkey 。 |
16 | SCAN cursor [MATCH pattern …] [COUNT count] 迭代数据库中的数据库键。 |
17 | TYPE key 返回 key 所储存的值的类型。 |
String
序号 | 命令及描述 |
---|---|
1 | SET key value 设置指定 key 的值 |
2 | GET key 获取指定 key 的值。 |
3 | GETRANGE key start end 返回 key 中字符串值的子字符 |
4 | GETSET key value 将给定 key 的值设为 value ,并返回 key 的旧值(old value)。 |
6 | MGET key1 [key2..] 获取所有(一个或多个)给定 key 的值。 |
7 | SETBIT key offset value 对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。 |
8 | SETEX key seconds value 将值 value 关联到 key ,并将 key 的过期时间设为 seconds (以秒为单位)。 |
9 | SETNX key value 只有在 key 不存在时设置 key 的值。 |
10 | SETRANGE key offset value 用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始。 |
11 | STRLEN key 返回 key 所储存的字符串值的长度。 |
12 | MSET key value [key value …] 同时设置一个或多个 key-value 对。 |
13 | MSETNX key value [key value …] 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。 |
14 | PSETEX key milliseconds value 这个命令和 SETEX 命令相似,但它以毫秒为单位设置 key 的生存时间,而不是像 SETEX 命令那样,以秒为单位。 |
15 | INCR key 将 key 中储存的数字值增一。 |
16 | INCRBY key increment 将 key 所储存的值加上给定的增量值(increment) 。 |
17 | INCRBYFLOAT key increment 将 key 所储存的值加上给定的浮点增量值(increment) 。 |
18 | DECR key 将 key 中储存的数字值减一。 |
19 | DECRBY key decrement key 所储存的值减去给定的减量值(decrement) 。 |
20 | APPEND key value 如果 key 已经存在并且是一个字符串, APPEND 命令将指定的 value 追加到该 key 原来值(value)的末尾。 |
GETRANGE key start end
获取key[start,end],end/start
为负数时代表倒数第|end|/|start|
个字符。当start值大于end时返回空字符串!!
List
Redis的List与常规说的列表有所不同,一般来说列表是单口的,即.append()只能在列表尾添加元素。但是Redis的List是双口的,能在列表首尾添加元素。而且元素的取存是和堆栈一致的。
序号 | 命令及描述 |
---|---|
1 | BLPOP key1 [key2 …] timeout 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 |
2 | BRPOP key1 [key2 …] timeout 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 |
3 | BRPOPLPUSH source destination timeout 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 |
4 | LINDEX key index 通过索引获取列表中的元素 |
5 | LINSERT key BEFORE|AFTER pivot value 在列表的元素前或者后插入元素 |
6 | LLEN key 获取列表长度 |
7 | LPOP key 移出并获取列表的第一个元素 |
8 | LPUSH key value1 [value2 …] 将一个或多个值插入到列表头部 |
9 | LPUSHX key value 将一个值插入到已存在的列表头部 |
10 | LRANGE key start stop 获取列表指定范围内的元素 |
11 | LREM key count value 移除列表count个相同元素 |
12 | LSET key index value 通过索引设置列表元素的值 |
13 | LTRIM key start stop 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。 |
14 | RPOP key 移除列表的最后一个元素,返回值为移除的元素。 |
15 | RPOPLPUSH source destination 移除列表的最后一个元素,并将该元素添加到另一个列表并返回 |
16 | RPUSH key value1 [value2 …] 在列表中添加一个或多个值 |
17 | RPUSHX key value 为已存在的列表添加值 |
LPUSH相当于堆栈的FILO原则,RPUSH相当于队列的FIFO原则
AFTER是队列右端插入元素,BEFORE是往队列左端插入元素
Set
和Java的Set一样,Set是一个无序不重复集合,因而Set不能通过下标指定获取元素,但可以用作随机数集合。
序号 | 命令及描述 |
---|---|
1 | SADD key member1 [member2 …] 向集合添加一个或多个成员 |
2 | SCARD key 获取集合的成员数 |
3 | SDIFF key1 [key2 …] 返回第一个集合与其他集合之间的差异。 |
4 | SDIFFSTORE destination key1 [key2 …] 返回给定所有集合的差集并存储在 destination 中 |
5 | SINTER key1 [key2 …] 返回给定所有集合的交集 |
6 | SINTERSTORE destination key1 [key2 …] 返回给定所有集合的交集并存储在 destination 中 |
7 | SISMEMBER key member 判断 member 元素是否是集合 key 的成员 |
8 | SMEMBERS key 返回集合中的所有成员 |
9 | SMOVE source destination member 将 member 元素从 source 集合移动到 destination 集合 |
10 | SPOP key 移除并返回集合中的一个随机元素 |
11 | SRANDMEMBER key [count …] 返回集合中一个或多个随机数 |
12 | SREM key member1 [member2 …] 移除集合中一个或多个成员 |
13 | SUNION key1 [key2 …] 返回所有给定集合的并集 |
14 | SUNIONSTORE destination key1 [key2 …] 所有给定集合的并集存储在 destination 集合中 |
15 | SSCAN key cursor [MATCH pattern …] [COUNT count] 迭代集合中的元素 |
Hash
序号 | 命令及描述 |
---|---|
1 | HDEL key field1 [field2 …] 删除一个或多个哈希表字段 |
2 | HEXISTS key field 查看哈希表 key 中,指定的字段是否存在。 |
4 | HGETALL key 获取在哈希表中指定 key 的所有字段和值 |
5 | HINCRBY key field increment 为哈希表 key 中的指定字段的整数值加上增量 increment 。 |
6 | HINCRBYFLOAT key field increment 为哈希表 key 中的指定字段的浮点数值加上增量 increment 。 |
7 | HKEYS key 获取所有哈希表中的字段 |
8 | HLEN key 获取哈希表中字段的数量 |
9 | HGET key field1 [field2 …] 获取所有给定字段的值 |
10 | HSET key field1 value1 [field2 value2 …] 同时将多个 field-value (域-值)对设置到哈希表 key 中。 |
12 | HSETNX key field value 只有在字段 field 不存在时,设置哈希表字段的值。 |
13 | HVALS key 获取哈希表中所有值。 |
14 | HSCAN key cursor [MATCH pattern …] [COUNT count] 迭代哈希表中的键值对。 |
HMGET和HMSET在Redis 4.0.0后被官方废弃,其功能和HSET,HGET一致。
HGETALL可以获取key-value,HKEYS只能获取key,HVALS只能获取value
Zset
Zset相比于Set,是个有序的无重复集合,Zset的主要方法用来排列集合数据
Zset存储的数据结构也与Set不一样,类似于value-key的结构,这样就可以通过key来定向获取分数了。
序号 | 命令及描述 |
---|---|
1 | ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员,或者更新已存在成员的分数 |
2 | ZCARD key 获取有序集合的成员数 |
3 | ZCOUNT key min max 计算在有序集合中指定区间分数的成员数 |
4 | ZINCRBY key increment member 有序集合中对指定成员的分数加上增量 increment |
5 | ZINTERSTORE destination numkeys key [key …] 计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 destination 中 |
6 | ZLEXCOUNT key min max 在有序集合中计算指定字典区间内成员数量 |
7 | ZRANGE key min max [BYSCORE|BYLEX] [REV] [LIMIT offset count] [WITHSCORES]通过索引区间返回有序集合指定区间内的成员 |
10 | ZRANK key member 返回有序集合中指定成员的索引 |
11 | ZREM key member [member …] 移除有序集合中的一个或多个成员 |
17 | ZREVRANK key member 返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序 |
18 | ZSCORE key member 返回有序集中,成员的分数值 |
19 | ZUNIONSTORE destination numkeys key [key …] 计算给定的一个或多个有序集的并集,并存储在新的 key 中 |
20 | ZSCAN key cursor [MATCH pattern] [COUNT count] 迭代有序集合中的元素(包括元素成员和元素分值) |
21 | ZRANDMEMBER key [count [WITHSCORES]] 随机获取集合中count个member |
Zset会自动对内部的数据进行递增的排序
ZRANGE是个功能齐全的方法,能够替代之前的ZREVRANGE, ZRANGEBYSCORE, ZREVRANGEBYSCORE, ZRANGEBYLEX and ZREVRANGEBYLEX.
ZRANGE key min max
是以下标为截取范围如ZRANGE key 0 -1
截取第一个到倒数第一个。ZRANGE key min max BYSCORE
是以分数为截取范围,如ZRANGE key -inf +inf BYSCORE
截取负无穷到正无穷ZRANGE key min max BYLEX
是以字典顺序为截取范围,且只以member在字典的顺序为排序(此模式下要求最好所有member的分数都要一致,否则返回的结果不可预测)因为BYLEX只看member在字典的顺序,而a排在最后面违反了Zset递增排序的规则,这样BYLEX查询就会出错。
对于
ZADD myzset 0 a 0 b 0 c 0 d 0 e 0 f 0 g
而言,ZRANGE myzset - + BYLEX
截取所有member。ZRANGE myset (a [d BYLEX
返回b、c、dZRANGE key max min REV
会以降序返回,但注意范围是max min。ZRANGE key min max WITHSCORES
会将member和score一起返回,默认只返回member。ZCOUNT key min max
是基于分数为截取范围,ZLEXCOUNT key min max
才是基于字典顺序为截取范围。
Geospatial
序号 | 命令及描述 |
---|---|
1 | GEOADD key longitude latitude member [longitude latitude member …] 添加地理位置 |
2 | GEOPOS key member [member …] 获取地点的经纬 |
3 | ZREM key member [member …] Geo没有专门的删除方法,但Geo也是一个Zset。 |
4 | GEODIST key member1 member2 [m|km|ft|mi] 测量两地点之间的直线距离 |
5 | GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key] [STOREDIST key] 获取离特定点radius距离内的所有地点,并按从近到远排列输出 |
6 | [GEOSEARCH key [FROMMEMBER member] [FROMLONLAT longitude latitude] [BYRADIUS radius m|km|ft|mi] [BYBOX width height m|km|ft|mi] [ASC|DESC] COUNT count [ANY] ] [WITHCOORD] [WITHDIST] 寻找count个以member或指定坐标为中心的圆形/方形范围内的地点。 |
GEOADD china:city 121.47 31.23 shanghai 116.40 39.90 beijing 106.50 29.53 chongqing 114.05 22.52 shenzhen 120.16 30.24 hangzhou
有效经度为 -180 到 180 度。有效纬度是从 -85.05112878 到 85.05112878 度。
GEORADIUS在Redis 6.2.0已经被弃用,代替的是 GEOSEARCH 和GEOSEARCHSTORE
FROMMEMBER:使用给定的<成员>在已排序集合中的位置。
FROMLONLAT:使用给定的<经度>和<纬度>位置。
BYRADIUS:类似于GEORADIUS,根据给定的<radius>
搜索圆形区域内。
BYBOX:在轴对齐的矩形内搜索,由<height>
和<width>
决定。
COUNT:在返回结果中截取前COUNT个元素,如果开启ANY选项,直到找到足够的匹配项就直接返回。否则会全部找完再进行排序。
WITHCOORD:将地点的经纬同时返回
WITHDIST:将地点与中心点的距离也返回,距离单位与搜寻范围的距离单位一致
Hyperloglog
序号 | 命令及描述 |
---|---|
1 | PFADD key element [element …] 添加一组元素 |
2 | PFCOUNT key [key …] 返回所有key集合交集的基数 |
3 | PFMERGE destkey sourcekey [sourcekey …] 合并sourcekey并存到destkey |
- PFADD如果添加了已有的元素,重复的元素将被阻止添加。
- PFCOUNT计算集合的基数(集合中不重复的元素个数),当PFCOUNT用单个键调用时,因为PFCOUNT使用缓存来记住之前计算的基数,而基数很少改变,故而计算时间很短。当使用多个键调用PFCOUNT时,会执行HyperLogLogs的动态合并,而且无法缓存合并的基数,故而计算时间较长。
Bitmap
序号 | 命令及描述 |
---|---|
1 | SETBIT key offset value 在offset偏移处设置0/1 |
2 | GETBIT key offset 获取offset的bit位 |
3 | BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL] |
4 | BITCOUNT key [start end] 统计bit位为1的位数 |
5 | BITOP AND|OR|XOR destkey key [key …] 将所有key进行位运算并存储结果到destkey BITOP NOT destkey srckey |
- Bitmap并不是一个特殊的数据结构,可以看作是字符串类型。因而bitmap也能使用GET、SET方法。
- SETBIT设置了一个高bit位的值后,Bitmap会自动增长以确保能在offset处保持位,未被设置的位则默认为0。offset参数要求大于或等于0,并且小于2^32^(这将Bitmap限制为512MB)
Redis事务
Redis事物本质是一组命令的集合,在事务的执行过程中会按照顺序执行。Redis事务是一次性、顺序性、排他性的。
但注意,Redis事务没有隔离级别的概念,而Redis命令是保持原子性的,但事务本身不保证原子性!
事务执行
MULTI(开启事务)
SET、GET … (命令入队)
EXEC (执行事务)
事务出现编译异常
Redis命令如果出现了语法错误,当其尝试入队时Redis会提示错误。最后运行时整个事务都会被阻止。
运行时异常
Redis命令如果操作了不得当数据,如自增字符串,用Hash命令操作List、String等,执行事务时正常命令可以照样执行,错误命令则抛出异常。
Jedis
通过Jedis连接远程服务器的Redis:
建立maven项目,导入依赖
1 |
|
创立代码文件:
1 |
|
改变Redis配置(备份在/usr/local/bin/leviconfig/redis.conf),允许远程连接:
注释掉bind,protected-mode为no
防火墙开启6379端口:
1 |
|
阿里云开放安全组6379端口
以上完成后执行Java代码即可看到PONG的输出。
Redis持久化
Redis数据库是存在于内存中的,具有断电即失的特点,因而Redis持久化数据很重要
RDB(Redis DataBase)
通过拍摄快照的方式实现持久化,将某个时间的内存数据存储在一个rdb文件中,在redis服务重新启动的时候加载文件中的数据
Redis会单独创建一个子进程来进行持久化,会先将数据写入到一个临时文件中,带持久化过程结束了,再用该临时文件替换上次持久化好的文件。整个持久化的过程中,主进程不进行任何IO操作,创建子进程后主进程仍然可以响应Client请求。
如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是很敏感的话,RDB比AOF方式更为有效。
RDB的缺点就是最后一次持久化后的数据可能会丢失。
Redis.conf
以下是Redis配置文件关于RDB的默认配置值。
RDB触发条件
1、 满足save 900 1 的条件
2、 执行了bgasve/save命令,bgsave不会阻塞主进程,而save会
3、 shutdown退出Redis/flush清除数据
注:FLUSHALL生成的rdb/aof文件是能储存flush之前的数据的:Redis的flushall/flushdb误操作 - 小家电维修 - 博客园 (cnblogs.com)
RDB恢复快照
1 |
|
只要dump.rdb保留在/usr/local/bin(默认在redis-server同级目录),重新启动redis就能重载备份数据。
FLUSHALL后恢复RDB数据
Redis使用rdb文件恢复数据 - 天宇轩-王 - 博客园 (cnblogs.com)
AOF(Append Only File)
AOF文件能够将我们的操作记录下来,在redis启动的时候会顺序执行记录的写命令。这种方式明显就不适合大量数据的存储了,但能很好地防止Flush的误操作恢复备份。
Redis.conf
AOF触发条件
appendfsync默认是每秒触发,即每秒记录一次写操作。
AOF修复
AOF可能因为电脑宕机导致文件破损,使用redis-check-aof --fix
就可以修复AOF文件,修复方式就是删去出错的写操作。如果AOF没有被修复,那么Redis是无法开启的。
FLUSH后恢复AOF数据
Redis AOF之执行flushdb或flushall之后的后悔药_小楼一夜听春雨,深巷明朝卖杏花-CSDN博客
Redis集群
环境搭建
在同一台服务器上搭建一主二从的拟集群环境,建立三分Redis配置文件,分别为redis79.conf,redis80.conf,redis81.conf
搭建好后进入redis,会发现每一台redis都是主节点。因为默认情况下每个节点都是主节点
1 |
|
选择一个作为从机通过SLAVEOF 127.0.0.1 6379
就能指定主机。
1 |
|
实际情况下要通过配置文件设置主从关系,更改从机配置文件的
relicaof <masterip> <masterport>
与masterauth <master-password>
主从复制
注意
整个集群中只有主机有读写功能,而从机没有写功能。从机共享主机资源,可以备份数据。
主机宕机且不配置哨兵模式时,如果主机宕机退出,从机依然依附原主机。但此时整个集群都不能做写操作!!
从机宕机后重连。除非配置文件中设置了主从关系,否则该机器脱离集群!!当然也可通过SLAVEOF进入集群。
复制原理
从机连接到集群后会像主机发送sync同步请求,主机接收后启动后台存盘进程,同时从机所有接收到的用于修改数据集的命令,在后台进程执行完毕后,主机同步所有数据文件到从机。
==全量复制==:从机接收到数据文件后,将其存盘并加载至内存
==增量复制==:后续主机会将新的数据依次传递给从机
只要从机连接到集群,就会自动完成一次全量复制
哨兵
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
这里的哨兵有两个作用
- 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
- 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。
用文字描述一下多哨兵故障切换(failover)的过程。假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。
单哨兵
创建哨兵配置文件sentinel.conf
:
1 |
|
启动哨兵,哨兵启动成功后会更新sentinel.conf
。
1 |
|
宕机主机,哨兵确认宕机后会投票选取一个从机为新的主机,相关日志如下:
1 |
|
即使原主机回来,也只能成为新主机的从机。
哨兵配置
1 |
|
Redis击穿和雪崩
缓存穿透
对于系统A,假设一秒 5000 个请求,结果其中 4000 个请求是黑客发出的恶意攻击。
黑客发出的那 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。
==解决方案:==
- 缓存空对象
如果数据库中找不到,就会返回一个空值储存到缓存中,并设置过期时间。在有效期内,所有相同key的请求都能在缓存中找到值。
- 布隆过滤器
布隆过滤器对可能的参数以hash形式储存,其他不合理的请求会被丢弃,从而避免直接对数据库的施压。
==隐患:==
但如果大量不同的key都找不到数据,就会导致缓存中存有大量无效的空值,从而降低性能。
缓存击穿
当某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
==解决方案:==
- 设置key永不过期
这种方式可以说是最可靠的,最安全的但是占空间,内存消耗大,并且不能保持数据最新 这个需要根据具体的业务逻辑来做。但也可以隔段时间便去数据库更新数据,此时要求不会给数据库很大的压力。
- 使用互斥锁
就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用某些一定能成功返回值的操作(比如Redis的SETNX)去set一个mutex key(一个特定的key,当成竞争资源),当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法
站在用户层面,但key失效时用户获取的就是空页面。因此只需要多刷新几次,等待缓存恢复正常就能获取到正常页面了。
1 |
|
redis.set(key, value, expire_secs)
是有讲究的,如果在SETNX和EXPIRE分开操作,但两者之间程序又发生了错误,当前锁又无法释放,key成为了永不过期。所以根本原因还是需要一个原子的操作,在获得锁的同时能够同时设置锁的过期时间。
缓存雪崩
对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机。缓存挂了,此时 1 秒 5000 个请求全部落数据库,数据库必然扛不住,它会报一下警,然后就挂了。此时,如果没有采用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。
==解决方案:==
- Redis高可用
有预知地在流量集中爆发的时间段前,扩大缓存集群(如暂停其他服务)来应付集中的请求,以此避免全盘崩溃
- ehcache 缓存 + hystrix 限流&降级
在缓存失效后,通过加锁或是限制队列长度的方式控制流量,避免数据库被直接打死。
- 数据预热
有预知地让缓存去数据库访问一遍可能会被集中访问的key,在大并发访问之前手动触发加载缓存的预热key。
Redis应用
Redis应用场景
- 对于具有时效性的业务功能如验证码,订单有效期
- redis分布式集群系统可以用作Session共享
- redis的zset类型可用作实时排行榜
- redis的setnx用作分布式锁
- 分布式缓存
Redis过期策略及内存淘汰
Redis对于过期数据或溢出内存后进行淘汰数据的策略可概括为定期删除+惰性删除
为什么不用定时删除策略?
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.
定期删除和惰性删除
定期删除,redis默认每个100ms随机抽取key进行检查,如果key过期则删除。但只采用定期删除,可能会导致很多过期key无法被检查到。
惰性删除,当获取某一key时,如果该key设置了过期时间,再检查是否过期,过期了才会删除。
但如果有一过期key没有被检查到,也没有被使用上,即绕开了定期删除与惰性删除的机制。这种key就会一直堆积在内存中,就需要引入内存淘汰机制。
内存淘汰机制
redis.conf中可以配置内存淘汰机制,当内存不足以纳入新的数据时就会触发机制:
1 |
|
allkeys-lru
:在键空间中,优先淘汰最近最少使用的keynoeviction
:写入新数据时会报错。allkeys-random
:在键空间中,随机移除某一keyvolatile-lru
:在设置了过期时间的键空间中,优先淘汰最近最少使用的keyvolatile-random
:在设置了过期时间的键空间中,随机移除某一keyvolatile-ttl
:在设置了过期时间的键空间中,优先淘汰TTL设置得更早的key
Redis与数据库一致性问题
当数据库和缓存双写,必定会存在不一致的问题。为了解决这一问题,我们可以采用双删+TTL失效来实现,但这也只能保证数据的最终一致性。如果对数据有强一致性的要求,就不能设置缓存!!!
数据库和缓存更新,就容易出现缓存和数据库间的数据一致性问题。不管是先写数据库,再删除缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。举个例子:
- 如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中依然为脏数据。
- 如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。
为了解决一致性问题,普遍有以下四种方式(为简述方便,对于每一种方法的两步分别以1,2标识,如写进程A的两步分别为A1,A2):
- 先更新数据库,再更新缓存
- 先更新缓存,再更新数据库
- 先删除缓存,再更新数据库
- 先更新数据库,再删除缓存
==先更新数据库,再更新缓存==
对于写进程A,B,执行顺序为A1-B1-B2-A1,这样缓存中的数据还是A的数据,数据出现不一致。
==先更新缓存,再更新数据库==
对于写进程A,A2在A1后执行失败,数据依旧不一致。
其实,通过更新缓存来同步数据库数据的两种方式都有以下两个问题:
- 对于写操作频繁的场景,缓存的数据时刻都在更新,浪费性能
- 如果真正需要写入缓存的数据是要进入数据库后再演算出来的,那么先更新缓存,所更新的数据也是错误的。
那么,通过懒加载的方式(删除缓存)同步数据库数据更佳
懒加载
当数据库中的数据更新/删除时选择删除缓存中的数据,删除后不更新新数据到缓存中,直到查询的时候没命中缓存,访问数据库后再添加至缓存
在懒加载的概念下,我们讨论后两种方式:先删除缓存,再更新数据库/先更新数据库,再删除缓存
==先删除缓存,再更新数据库==
- 线程 A 要更新 X = 2(数据库中 X = 1)
- 线程 A 先删除缓存
- 线程 B 读缓存,发现不存在,从数据库中读取到旧值(X = 1)
- 线程 A 将新值写入数据库(X = 2)
- 线程 B 将旧值写入缓存(X = 1)
缓存中依然留的是旧值。这还是一个数据库出现的问题,如果在主从库的环境下:
- 线程 A 更新主库 X = 2(原值 X = 1)
- 线程 A 删除缓存
- 线程 B 查询缓存,没有命中,查询「从库」得到旧值(从库 X = 1)
- 从库「同步」完成(主从库 X = 2)
- 线程 B 将「旧值」写入缓存(X = 1)
为了解决以上两个问题,业界给出了延迟双删的答案:
延迟双删
1)先删除缓存;
2)再写数据库;
3)触发异步写入串行化MQ(也可以采取一种key+version的分布式锁);
4)MQ接受再次删除缓存。
两次删除的目的是防止在1)到 2)的过程中读操作访问缓存,从而把还未更新的数据由存到了缓存中去。延迟删除就保证了此后缓存中的数据是最新的。
但是!!双删依旧存在问题!!
1、A删除缓存
2、B查询数据库获取旧值
3、B更新了缓存
4、A更新数据库
5、A延时删缓存
这种情况下,1-3步会让删除的旧缓存重新回来了,那么先删除缓存就没有任何意义。
此外!!如果步骤3要晚于步骤5,以后的读操作读的还是旧缓存!!!(虽然可以加长延时长度来避免)
==先更新数据库,再删除缓存==
综上,该方式能应付大多并发请求。为了防止删除缓存程序宕机,可以设置缓存有效时间。
但这样更新数据库到删除缓存的这段时间内,缓存和数据库无法保证一致性。
串行化
对于==先删除缓存,再更新数据库==,其问题就是没能保证读应该在写之后发生,为此我们可以引用串行MQ来保证读写的顺序执行。
- 先删缓存,将更新数据库的操作放进有序队列中
- 如果缓存查不到,访问数据库和更新缓存的操作都进入有序队列
我们再考虑以下场景:
- 线程 A 先删除缓存
- 线程 A 要更新 X = 2(数据库中 X = 1),更新数据库请求进入MQ
- 线程 B 读缓存,发现不存在,读取数据库请求进入MQ
- MQ消费到更新数据库请求,线程 A 将新值写入数据库(X = 2)
- MQ消费到读取数据库请求,线程 B 读取新值(X = 2)
- MQ消费到更新缓存请求,线程 B更新缓存
可以优化的一点是,当有多个连续更新缓存请求在MQ中,只需要保留最新的更新请求即可。
但该方案依旧存在问题!!!
- 如果数据库更新频繁,缓存一直没有值,这就会导致大量读取数据库请求积压在MQ末端中,如果放行这些请求,数据库的压力会很大。
- 串行化虽然能保证不会出现数据不一致的问题,但是高并发更新操作场景下,读操作被推迟,严重降低系统吞吐量,影响客户体验。
重发
==消息队列==
无论是先还是后操作数据库,两步没有全走完都会出现不一致的后果,为了保证请求能确保被完全执行,我们可以引入消息队列,原因有三:
- 重发操作不能由业务层发起,这会严重干扰业务进行。同时如果执行失败的线程中一直重试,还没等执行成功,此时如果项目「重启」了,那这次重试请求也就「丢失」了,那这条数据就一直不一致了。因此我们就需要在另一个服务中完成重发操作
- 消息队列能保证队列里的消息在成功被消费之前不会丢失
- 消息队列可以保证消息被成功传递,否则还会继续投递
==订阅Binlog==
当然业务层也可以不发起重发操作,因为更新数据库的时候会将信息写入binlog日志中,通过订阅该日志就能获取到key,之后发布删除操作到MQ中。常用的如阿里的Canal
总结
在保证最终一致性的情况下,可以采用「先更新数据库,再删除缓存」方案,并配合「消息队列」或「订阅变更日志」的方式来做。
串行化能达到强一致性的,但会降低缓存的作用。
Redis并发竞争key
如果多个子系统同时设置一个key,可以:
(1)如果对这个key操作,不要求顺序
这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可,比较简单。
(2)如果对这个key操作,要求顺序
假设有一个key1,系统A需要将key1设置为valueA,系统B需要将key1设置为valueB,系统C需要将key1设置为valueC.
期望按照key1的value值按照 valueA–>valueB–>valueC的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳。假设时间戳如下
1 |
|
那么,假设这会系统B先抢到锁,将key1设置为{valueB 3:05}。接下来系统A抢到锁,发现自己的valueA的时间戳早于缓存中的时间戳,那就不做set操作了。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!