其他
万字好文!带你入门 redis
Redis 是什么?
Redis 特性
速度快 单节点读 110000次/s,写81000次/s 数据存放内存中 用C语言实现,离操作系统更近 单线程架构,6.0开始支持多线程(CPU、IO读写负荷) 持久化 数据的更新将异步地保存到硬盘(RDB和 AOF) 多种数据结构 不仅仅支持简单的 key-value 类型数据,还支持:字符串、hash、列表、集合、有序集合, 支持多种编程语言 功能丰富 HyperLogLog、GEO、发布订阅、Lua脚本、事务、Pipeline、Bitmaps,key 过期 简单稳定 源码少、单线程模型 主从复制 Redis 支持数据的备份(master-slave)与集群(分片存储),以及拥有哨兵监控机制。 Redis 的所有操作都是原子性的,同时 Redis 还支持对几个操作合并后的原子性执行。
Redis 典型使用场景
Redis 高并发原理
Redis是纯内存数据库,一般都是简单的存取操作,线程占用的时间很多,时间的花费主要集中在IO上,所以读取速度快 Redis使用的是非阻塞 IO,IO 多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切换和竞争。 Redis采用了单线程的模型,保证了每个操作的原子性,也减少了线程的上下文切换和竞争。 Redis存储结构多样化,不同的数据结构对数据存储进行了优化,如压缩表,对短数据进行压缩存储,再如,跳表,使用有序的数据结构加快读取的速度。 Redis采用自己实现的事件分离器,效率比较高,内部采用非阻塞的执行方式,吞吐能力比较大。
Redis 安装
下载 Redis
解压并安装 Redis
ls
列出所有目录会发现与下载 redis 之前多了一个 redis-6.2.4.tar.gz 文件和 redis-6.2.4 的目录。移动 Redis 目录(可选)
cd
到 '/usr/local' 目录下输入 ls
命令可以查询到当前目录已经多了一个 redis 子目录,同时 '/root' 目录下已经没有 'redis-6.2.4' 文件:编译
cd
到 '/usr/local/redis' 目录,输入命令 make
执行编译命令,接下来控制台会输出各种编译过程中输出的内容:安装
rm -rf /usr/local/redis
即可删除 Redis。Redis 启动
最简默认启动 安装后在 bin 目录下直接执行 redis-server 验证(ps –aux | grep redis) 动态参数启动(可配置一下参数,例如指定端口) ./bin/redis-server –port 6380 配置文件启动 ./bin/redis-server& ./redis.conf 生产环境一般选择配置启动 单机多实例配置文件可以用端口区分开
redis.conf 配置文件
cat、vim、less
等 linux 内置的读取命令读取该文件。:wq
命令保存并退出),如下图:object encoding
命令查询内部编码。通用全局命令
常用全局命令
keys:查看所有键 dbsize:键总数 exists key:检查键是否存在 del key [key ...]:删除键 expire key seconds:键过期 ttl key: 通过 ttl 命令观察键键的剩余过期时间 type key:键的数据结构类型
简单使用截图
字符串使用
常用字符串命令
set key value [ex seconds] [px milliseconds] [nx|xx]: 设置值,返回 ok 表示成功 ex seconds:为键设置秒级过期时间。 px milliseconds:为键设置毫秒级过期时间。 nx:键必须不存在,才可以设置成功,用于添加。可单独用 setnx 命令替代 xx:与nx相反,键必须存在,才可以设置成功,用于更新。可单独用 setxx 命令替代 get key:获取值 mset key value [key value ...]:批量设置值,批量操作命令可以有效提高业务处理效率 mget key [key ...]:批量获取值,批量操作命令可以有效提高业务处理效率 incr key:计数,返回结果分 3 种情况: 值不是整数,返回错误。 值是整数,返回自增后的结果。 键不存在,按照值为0自增,返回结果为1。 decr(自减)、incrby(自增指定数字)、 decrby(自减指定数字)
字符串简单使用截图
字符串使用场景
缓存数据,提高查询性能。比如存储登录用户信息、电商中存储商品信息 可以做计数器(想知道什么时候封锁一个IP地址(访问超过几次)),短信限流 共享 Session,例如:一个分布式Web服务将用户的Session信息(例如用户登录信息)保存在各自服务器中,这样会造成一个问题,出于负载均衡的考虑,分布式服务会将用户的访问均衡到不同服务器上,用户刷新一次访问可 能会发现需要重新登录,为了解决这个问题,可以使用Redis将用户的Session进行集中管理,在这种模式下只要保证Redis是高可用和扩展性的,每次用户 更新或者查询登录信息都直接从Redis中集中获取,如图:
哈希 hash
常用哈希命令
hset key field value:设置值 hget key field:获取值 hdel key field [field ...]:删除field hlen key:计算field个数 hmset key field value [field value ...]:批量设置field-value hmget key field [field ...]:批量获取field-value hexists key field:判断field是否存在 hkeys key:获取所有field hvals key:获取所有value hgetall key:获取所有的field-value incrbyfloat和hincrbyfloat:就像incrby和incrbyfloat命令一样,但是它们的作 用域是 filed
哈希简单使用截图
哈希使用场景
Hash也可以同于对象存储,比如存储用户信息,与字符串不一样的是,字符串是需要将对象进行序列化(比如 json 序列化)之后才能保存,而 Hash 则可以讲用户对象的每个字段单独存储,这样就能节省序列化和反序列的时间。如下:
此外还可以保存用户的购买记录,比如 key 为用户 id,field 为商品i d,value 为商品数量。同样还可以用于购物车数据的存储,比如 key 为用户 id,field 为商品 id,value 为购买数量等等:
列表(lists)
常用列表命令
rpush key value [value ...]:从右边插入元素 lpush key value [value ...]:从左边插入元素 linsert key before|after pivot value:向某个元素前或者后插入元素 lrange key start end:获取指定范围内的元素列表, lrange key 0 -1
可以从左到右获取列表的所有元素lindex key index:获取列表指定索引下标的元素 llen key:获取列表长度 lpop key:从列表左侧弹出元素 rpop key:从列表右侧弹出 lrem key count value:删除指定元素,lrem命令会从列表中找到等于value的元素进行删除,根据count的不同 分为三种情况: ·count>0,从左到右,删除最多count个元素。 count<0,从右到左,删除最多count绝对值个元素。 count=0,删除所有。 ltrim key start end:按照索引范围修剪列表 lset key index newValue:修改指定索引下标的元素 blpop key [key ...] timeout 和 brpop key [key ...] timeout:阻塞式弹出
列表简单使用截图
列表使用场景
热销榜,文章列表 实现工作队列(利用lists的push操作,将任务存在lists中,然后工作线程再用pop操作将任务取出进行执行 ),例如消息队列 最新列表,比如最新评论
lpush+lpop=Stack(栈) lpush+rpop=Queue(队列) lpsh+ltrim=Capped Collection(有限集合) lpush+brpop=Message Queue(消息队列)
set 集合和 zset 有序集合
常用集合命令
sadd key element [element ...]:添加元素,返回结果为添加成功的元素个数 srem key element [element ...]:删除元素,返回结果为成功删除元素个数 smembers key:获取所有元素 sismember key element:判断元素是否在集合中,如果给定元素element在集合内返回1,反之返回0 scard key:计算元素个数,scard的时间复杂度为O(1),它不会遍历集合所有元素 spop key:从集合随机弹出元素,从3.2版本开始,spop也支持[count]参数。 srandmember key [count]:随机从集合返回指定个数元素,[count]是可选参数,如果不写默认为1 sinter key [key ...]:求多个集合的交集 suinon key [key ...]:求多个集合的并集 sdiff key [key ...]:求多个集合的差集
集合简单使用截图
常用有序集合命令
zadd key score member [score member ...]:添加成员,返回结果代表成功添加成员的个数。Redis3.2为zadd命令添加了nx、xx、ch、incr四个选项: nx:member必须不存在,才可以设置成功,用于添加 xx:member必须存在,才可以设置成功,用于更新 ch:返回此次操作后,有序集合元素和分数发生变化的个数 incr:对score做增加,相当于后面介绍的zincrby zcard key:计算成员个数 zscore key member:计算某个成员的分数 zrank key member 和 zrevrank key member:计算成员的排名,zrank是从分数从低到高返回排名,zrevrank反之 zrem key member [member ...]:删除成员 zincrby key increment member:增加成员的分数 zrange key start end [withscores] 和 zrevrange key start end [withscores]:返回指定排名范围的成员,zrange是从低到高返回,zrevrange反之。 zrangebyscore key min max [withscores] [limit offset count] 和 zrevrangebyscore key max min [withscores] [limit offset count] 返回指定分数范围的成员,其中zrangebyscore按照分数从低到高返回,zrevrangebyscore反之 zcount key min max:返回指定分数范围成员个数 zremrangebyrank key start end:删除指定排名内的升序元素 zremrangebyscore key min max:删除指定分数范围的成员 zinterstore 和 zunionstore 命令求集合的交集和并集,可用参数比较多,可用到再查文档
有序集合简单使用截图
集合和有序集合使用场景
给用户添加标签 给标签添加用户 根据某个权重进行排序的队列的场景,比如游戏积分排行榜,设置优先级的任务列表,学生成绩表等
关于跳跃列表
列表、集合和有序集合异同
三、小功能大用处
慢查询分析
OK
> config set slowlog-max-len 1200
OK
> config rewrite
OK
slowlog get
命令获取慢查询日志,在 slowlog get
后面还可以加一个数字,用于指定获取慢查询日志的条数,比如,获取2条慢查询日志:1) 1) (integer) 6107
2) (integer) 1616398930
3) (integer) 3109
4) 1) "config"
2) "rewrite"
2) 1) (integer) 6106
2) (integer) 1613701788
3) (integer) 36004
4) 1) "flushall"
唯一标识ID 命令执行的时间戳 命令执行时长 执行的命名和参数
slowlog len
命令获取慢查询日志的长度;通过 slowlog reset
命令清理慢查询日志。Pipeline(流水线)机制
* 测试Redis流水线
* @author liu
*/
publicclass TestPipelined {
/**
* 使用Java API测试流水线的性能
*/
@SuppressWarnings({ "unused", "resource" })
@Test
public void testPipelinedByJavaAPI() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(20);
jedisPoolConfig.setMaxTotal(10);
jedisPoolConfig.setMaxWaitMillis(20000);
JedisPool jedisPool = new JedisPool(jedisPoolConfig,"localhost",6379);
Jedis jedis = jedisPool.getResource();
long start = System.currentTimeMillis();
// 开启流水线
Pipeline pipeline = jedis.pipelined();
// 测试10w条数据读写
for(int i = 0; i < 100000; i++) {
int j = i + 1;
pipeline.set("key" + j, "value" + j);
pipeline.get("key" + j);
}
// 只执行同步但不返回结果
//pipeline.sync();
// 以list的形式返回执行过的命令的结果
List<Object> result = pipeline.syncAndReturnAll();
long end = System.currentTimeMillis();
// 计算耗时
System.out.println("耗时" + (end - start) + "毫秒");
}
/**
* 使用RedisTemplate测试流水线
*/
@SuppressWarnings({ "resource", "rawtypes", "unchecked", "unused" })
@Test
public void testPipelineBySpring() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
RedisTemplate rt = (RedisTemplate)applicationContext.getBean("redisTemplate");
SessionCallback callback = (SessionCallback)(RedisOperations ops)->{
for(int i = 0; i < 100000; i++) {
int j = i + 1;
ops.boundValueOps("key" + j).set("value" + j);
ops.boundValueOps("key" + j).get();
}
returnnull;
};
long start = System.currentTimeMillis();
// 执行Redis的流水线命令
List result = rt.executePipelined(callback);
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
事务与 Lua
multi 和 exec 命令
OK
127.0.0.1:6379> SET msg "hello chrootliu"
QUEUED
127.0.0.1:6379> GET msg
QUEUED
127.0.0.1:6379> EXEC
1) OK
1) hello chrootliu
不够满足原子性。一个事务执行过程中,其他事务或 client 是可以对相应的 key 进行修改的(并发情况下,例如电商常见的超卖问题),想要避免这样的并发性问题就需要使用 WATCH 命令,但是通常来说,必须经过仔细考虑才能决定究竟需要对哪些 key 进行 WATCH 加锁。然而,额外的 WATCH 会增加事务失败的可能,而缺少必要的 WATCH 又会让我们的程序产生竞争条件。 后执行的命令无法依赖先执行命令的结果。由于事务中的所有命令都是互相独立的,在遇到 exec 命令之前并没有真正的执行,所以我们无法在事务中的命令中使用前面命令的查询结果。我们唯一可以做的就是通过 watch 保证在我们进行修改时,如果其它事务刚好进行了修改,则我们的修改停止,然后应用层做相应的处理。 事务中的每条命令都会与 Redis 服务器进行网络交互。Redis 事务开启之后,每执行一个操作返回的都是 queued,这里就涉及到客户端与服务器端的多次交互,明明是需要一次批量执行的 n 条命令,还需要通过多次网络交互,显然非常浪费(这个就是为什么会有 pipeline 的原因,减少 RTT 的时间)。
Redis 事务缺陷的解决 – Lua
script 一段 Lua 脚本或 Lua 脚本文件所在路径及文件名 numkeys Lua 脚本对应参数数量 key [key …] Lua 中通过全局变量 KEYS 数组存储的传入参数 arg [arg …] Lua 中通过全局变量 ARGV 数组存储的传入附加参数
1) "key1"
2) "key2"
3) "first"
4) "second"
"232fd51614574cf0867b83d384a5e898cfd24e5a"
EVALSHA "232fd51614574cf0867b83d384a5e898cfd24e5a" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
redis.call()
或 redis.pcall()
传入 Redis 命令就可以直接执行:-- KEYS[1] 要限制的ip
-- ARGV[1] 限制的访问次数
-- ARGV[2] 限制的时间
--
local key = "rate.limit:" .. KEYS[1]
local limit = tonumber(ARGV[1])
local expire_time = ARGV[2]
local is_exists = redis.call("EXISTS", key)
if is_exists == 1then
if redis.call("INCR", key) > limit then
return0
else
return1
end
else
redis.call("SET", key, 1)
redis.call("EXPIRE", key, expire_time)
return1
end
Bitmaps
Bitmaps本身不是一种数据结构,实际上它就是字符串,但是它可以对字符串的位进行操作。 Bitmaps单独提供了一套命令,所以在 Redis 中使用 Bitmaps 和使用字符串的方法不太相同。可以把Bitmaps 想象成一个以位为单位的数组,数组的每个单元只能存储 0 和 1,数组的下标在 Bitmaps 中叫做偏移量。
getbit key offset # 返回 key 对应的 string 在 offset 处的 bit 值
bitcount key [start end] # start end 范围内被设置为1的数量,不传递 start end 默认全范围
(integer) 0
127.0.0.1:6379> setbit userLogin:2021-04-10 999999 1 #userId=999999的用户登录,这是今天第二个登录、的用户。
(integer) 0
127.0.0.1:6379> setbit userLogin:2021-04-10 3333 1
(integer) 0
127.0.0.1:6379> setbit userLogin:2021-04-10 8888 1
(integer) 0
127.0.0.1:6379> setbit userLogin:2021-04-10 100000 1
(integer) 0
127.0.0.1:6379> getbit active:2021-04-10 66666
(integer) 1
127.0.0.1:6379> getbit active:2021-04-10 55555
(integer)
127.0.0.1:6379> bitcount active:2021-04-10
(integer) 5
HyperLogLog
# 如果 HyperLogLog 估计的近似基数在 PFADD 命令执行之后出现了变化, 那么命令返回 1 , 否则返回 0
# 如果命令执行时给定的键不存在, 那么程序将先创建一个空的 HyperLogLog 结构, 然后再执行命令
pfadd key value1 [value2 value3]
# PFCOUNT 命令会给出 HyperLogLog 包含的近似基数
# 在计算出基数后, PFCOUNT 会将值存储在 HyperLogLog 中进行缓存,知道下次 PFADD 执行成功前,就都不需要再次进行基数的计算。
pfcount key
# PFMERGE 将多个 HyperLogLog 合并为一个 HyperLogLog , 合并后的 HyperLogLog 的基数接近于所有输入 HyperLogLog 的并集基数。
pfmerge destkey key1 key2 [...keyn]
(integer) 1
127.0.0.1:6379> pfcount totaluv
(integer) 1
127.0.0.1:6379> pfadd totaluv user2
(integer) 1
127.0.0.1:6379> pfcount totaluv
(integer) 2
127.0.0.1:6379> pfadd totaluv user3
(integer) 1
127.0.0.1:6379> pfcount totaluv
(integer) 3
127.0.0.1:6379> pfadd totaluv user4
(integer) 1
127.0.0.1:6379> pfcount totaluv
(integer) 4
127.0.0.1:6379> pfadd totaluv user5
(integer) 1
127.0.0.1:6379> pfcount totaluv
(integer) 5
127.0.0.1:6379> pfadd totaluv user6 user7 user8 user9 user10
(integer) 1
127.0.0.1:6379> pfcount totaluv
(integer) 10
只为了计算独立总数,不需要获取单条数据。 可以容忍一定误差率,毕竟HyperLogLog在内存的占用量上有很大的优势。
HashMap:算法简单,统计精度高,对于少量数据建议使用,但是对于大量的数据会占用很大内存空间; BitMap:位图算法,具体内容可以参考我的这篇文章,统计精度高,虽然内存占用要比 HashMap 少,但是对于大量数据还是会占用较大内存; HyperLogLog:存在一定误差,占用内存少,稳定占用 12k 左右内存,可以统计 2^64 个元素,对于上面举例的应用场景,建议使用。
发布订阅
unsubscribe channel # 退订指定频道
publish channel message # 发送消息
psubscribe pattern # 订阅指定模式
punsubscribe pattern # 退订指定模式
(integer) 1 # 返回的是接收这条消息的订阅者数量
Reading messages... (press Ctrl-C to quit)
1) "subscribe" // 消息类型
2) "TestChanne" // 频道
3) "hello" // 消息内容
GEO
# 这里的成员就是指代具体的业务数据,比如说用户的ID等
# 需要注意的是Redis的纬度有效范围不是[-90,90]而是[-85,85]
# 如果在添加一个空间元素时,这个元素中的menber已经存在key中,那么GEOADD命令会返回0,相当于更新了这个menber的位置信息
GEOADD key longitude latitude member [longitude latitude member]
# 用于添加城市的坐标信息
geoadd cities:locations 117.12 39.08 tianjin 114.29 38.02 shijiazhuang 118.01 39.38 tangshan 115.29 38.51 baoding
# 获取地理位置信息
geopos key member [member ...]
# 获取天津的坐标
geopos cities:locations tianjin
# 获取两个坐标之间的距离
# unit代表单位,有4个单位值
- m (meter) 代表米
- km (kilometer)代表千米
- mi (miles)代表英里
- ft (ft)代表尺
geodist key member1 member2 [unit]
# 获取天津和保定之间的距离
GEODIST cities:locations tianjin baoding km
# 获取指定位置范围内的地理信息位置集合,此命令可以用于实现附近的人的功能
# georadius和georadiusbymember两个命令的作用是一样的,都是以一个地理位置为中心算出指定半径内的其他地理信息位置,不同的是georadius命令的中心位置给出了具体的经纬度,georadiusbymember只需给出成员即可。其中radiusm|km|ft|mi是必需参数,指定了半径(带单位),这两个命令有很多可选参数,参数含义如下:
# - withcoord:返回结果中包含经纬度。
# - withdist:返回结果中包含离中心节点位置的距离。
# - withhash:返回结果中包含geohash,有关geohash后面介绍。
# - COUNT count:指定返回结果的数量。
# - asc|desc:返回结果按照离中心节点的距离做升序或者降序。
# - store key:将返回结果的地理位置信息保存到指定键。
# - storedist key:将返回结果离中心节点的距离保存到指定键。
georadius key longitude latitude radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]
georadiusbymember key member radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]
# 获取geo hash
# Redis使用geohash将二维经纬度转换为一维字符串,geohash有如下特点:
# - GEO的数据类型为zset,Redis将所有地理位置信息的geohash存放在zset中。
# - 字符串越长,表示的位置更精确,表3-8给出了字符串长度对应的精度,例如geohash长度为9时,精度在2米左右。长度和精度的对应关系,请参考:https://easyreadfs.nosdn.127.net/9F42_CKRFsfc8SUALbHKog==/8796093023252281390
# - 两个字符串越相似,它们之间的距离越近,Redis利用字符串前缀匹配算法实现相关的命令。
# - geohash编码和经纬度是可以相互转换的。
# - Redis正是使用有序集合并结合geohash的特性实现了GEO的若干命令。
geohash key member [member ...]
# 删除操作,GEO没有提供删除成员的命令,但是因为GEO的底层实现是zset,所以可以借用zrem命令实现对地理位置信息的删除。
zrem key member
java -> Jedis python -> redis-py node -> ioredis
五、持久化、主从同步与缓存设计持久化
RDB 是一次全量备份,AOF 日志是连续的增量备份, RDB 是内存数据的二进制序列化形式,在存储上非常紧凑,而 AOF 日志记录的是内存数据修改的指令记录文本。 AOF 以独立日志的方式记录每次写命令, 重启时再重新执行 AOF 文件中的命令达到恢复数据的目的。AOF 的主要作用 是解决了数据持久化的实时性,目前已经是 Redis 持久化的主流方式。
RDB
执行 bgsave 命令,Redis 父进程判断当前是否存在正在执行的子进 程,如 RDB/AOF 子进程,如果存在bgsave命令直接返回。 父进程执行 fork 操作创建子进程,fork 操作过程中父进程会阻塞,通 过 info stats 命令查看 latest_fork_usec 选项,可以获取最近一个 fork 操作的耗 时,单位为微秒。 父进程fork完成后,bgsave 命令返回 “Background saving started” 信息 并不再阻塞父进程,可以继续响应其他命令。 子进程创建 RDB 文件,根据父进程内存生成临时快照文件,完成后 对原有文件进行原子替换。执行lastsave命令可以获取最后一次生成 RDB 的 时间,对应 info 统计的 rdb_last_save_time 选项。 进程发送信号给父进程表示完成,父进程更新统计信息,具体见 info Persistence下的 rdb_* 相关选项。
AOF
所有的写入命令会追加到aof_buf(缓冲区)中。 AOF缓冲区根据对应的策略向硬盘做同步操作。 随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的。 当Redis服务器重启时,可以加载AOF文件进行数据恢复。
Redis 4.0 混合持久化
主从同步—简单了解
消息丢失
min-slaves-to-write 1 min-slaves-max-lag 10
Redis 最终一致
缓存
缓存的收益与成本
加速读写:CPU L1/L2/L3 Cache、浏览器缓存等。因为缓存通常都是全内存的(例如 Redis、Memcache),而 存储层通常读写性能不够强悍(例如 MySQL),通过缓存的使用可以有效 地加速读写,优化用户体验。 降低后端负载:帮助后端减少访问量和复杂计算,在很大程度降低了后端的负载。成本: 数据不一致:缓存层和数据层有时间窗口不一致,和更新策略有关。 代码维护成本:加入缓存后,需要同时处理缓存层和存储层的逻辑, 增大了开发者维护代码的成本。 运维成本:以 Redis Cluster 为例,加入后无形中增加了运维成本。使用场景: 降低后端负载:对高消耗的 SQL:join 结果集/分组统计结果缓存。 加速请求响应:利用 Redis/Memcache 优化 IO 响应时间。 大量写合并为批量写:比如计数器先 Redis 累加再批量写入 DB。
缓存更新策略—算法剔除
LRU:Least Recently Used,最近最少使用。 LFU:Least Frequently Used,最不经常使用。 FIFO:First In First Out,先进先出。
缓存更新策略—超时剔除
缓存更新策略—主动更新
缓存更新策略—总结
缓存可能会遇到的问题
布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被 这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。 另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
六、知识拓展
缓存与数据库同步策略(如何保证缓存(Redis)与数据库(MySQL)的一致性?)
先更新数据库,再更新缓存 先更新缓存,再更新数据库 先删除缓存,再更新数据库 先更新数据库,再删除缓存
删除缓存对比更新缓存
删除缓存: 数据只会写入数据库,不会写入缓存,只会删除缓存 更新缓存: 数据不但写入数据库,还会写入缓存
优点:操作简单,无论更新操作是否复杂,直接删除,并且能防止更新出现的线程安全问题 缺点:删除后,下一次查询无法在 cache 中查到,会有一次 Cache Miss,这时需要重新读取数据库,高并发下可能会出现上面说的缓存问题
优点:命中率高,直接更新缓存,不会有 Cache Miss 的情况 缺点:更新缓存消耗较大,尤其在复杂的操作流程中
更新缓存的代价很小,此时我们应该更倾向于更新缓存,以保证更高的缓存命中率 更新缓存的代价很大,此时我们应该更倾向于删除缓存
先更新数据库,再更新缓存
线程 A 更新了数据库 线程 B 更新了数据库 线程 B 更新了缓存 线程 A 更新了缓存
如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能 如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是也浪费性能的
先更新缓存,再更新数据库
先删除缓存,再更新数据库
请求 A 进行写操作,删除缓存 请求 B 查询发现缓存不存在 请求 B 去数据库查询得到旧值 请求 B 将旧值写入缓存 请求 A 将新值写入数据库
先更新数据库,再删除缓存
失效:应用程序先从 cache 取数据,没有得到,则从数据库中取数据,成功后,放到缓存中 命中:应用程序从 cache 中取数据,渠道后返回 更新:先把数据存到数据库中,成功后再让缓存失效
请求缓存刚好失效 请求A查询数据库,得一个旧值 请求B将新值写入数据库 请求B删除缓存 请求A将查到的旧值写入缓存
方案三四脏数据解决方案
先删除(淘汰)缓存 再写数据库(这两步和原来一样) 休眠1秒,再次删除(淘汰)缓存
先写数据库 再删除(淘汰)缓存(这两步和原来一样) 休眠1秒,再次删除(淘汰)缓存
请求 A 进行写操作,删除缓存 请求 A 将数据写入数据库了,(或者是先更新数据库,后删除缓存) 请求 B 查询缓存发现,缓存没有值 请求 B 去从库查询,这时,还没有完成主从同步,因此查询到的是旧值 请求 B 将旧值写入缓存 数据库完成主从同步,从库变为新值
更新数据库数据 缓存因为种种问题删除失败 将需要删除的 key 发送至消息队列 自己消费消息,获得需要删除的 key 继续重试删除操作,直到成功
更新数据库数据 数据库会将操作信息写入 binlog 日志当中 订阅程序提取出所需要的数据以及key 另起一段非业务代码,获得该信息 尝试删除缓存操作,发现删除失败 将这些信息发送至消息队列 重新从消息队列中获得该数据,重试操作
分布式锁
OK
... do something critical ...
del lock:codehole
(integer) 1
OK
> expire lock:codehole 5 ...
do something critical ...
> del lock:codehole
(integer) 1
OK
... do something critical ...
del lock:codehole
分布式锁存在的问题
SET resource_name unique_value NX PX 30000
// 释放锁 比较unique_value是否相等 避免误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
关于分布式锁的 Redlock 算法
获取当前 Unix 时间,以毫秒为单位 依次尝试从5个实例,使用相同的key和具有唯一性的value获取锁 当向 Redis 请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间,这样可以避免客户端死等 客户端使用当前时间减去开始获取锁时间就得到获取锁使用的时间。当且仅当从半数以上的 Redis 节点取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功 如果取到了锁,key 的真正有效时间等于有效时间减去获取锁所使用的时间,这个很重要 如果因为某些原因,获取锁失败(没有在半数以上实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的 Redis 实例上进行解锁,无论 Redis 实例是否加锁成功,因为可能服务端响应消息丢失了但是实际成功了,毕竟多释放一次也不会有问题