Appearance
视频地址: https://www.bilibili.com/video/BV1xm42177Fq
实现分布式全局ID、浏览数、值缓存、分布式session
Stirng 应用-值缓存
举一个例子,存在一个树结构【角色 → 人物】的一个树结构

考虑将列表内容存入到 Redis 中,
- 将查询结果反序列化为一个 Json 数据,存入到 Redis 中
- 作为一个查询缓存

三级下拉框
根据上文,这里扩展一下,一般来说,下拉框数据也可以使用 hash 结构,数据量少的话可以使用 string 类型。
Hash结构,以 国家、省份、城市 为示例
一级
键 countries 值 列表《国家ID,名称 》
二级:
键 privinces:国家ID, 值 列表《省份ID,名称 》
三级:
键 privinces:省份ID, 值 列表《城市ID,名称 》
Stirng应用-浏览数

Stirng应用-分布式系统全局 ID

扩展:在高并发情况下,每次创建一个订单,就需要和 Redis 交互一次,对于 redis 的压力会非常大的
如何优化这个场景?
我们可以每一次取 1000 个订单 ID(各服务取1000个 ID 备用);
每当订单服务需要创建订单的时候,就自增一次服务器内部的这个自增ID,直到 服务器内部的这个备用 ID 都用光了,再去 redis 中取 1000 个订单ID
可以使用命令 incr key count 来实现这个操作

命令
分布式 session
将 session 存入到 redis 中,通过 session ID 来获取 session 信息,实现跨服务器访问应用服务(共享 session)

Redis分布式锁,锁订阅、锁续期、Lua原子执行原理、顺便讲Sync锁升级
可重入锁
单体,JVM 进行通过加锁 synchronized
跨进程:Redis 分布式锁
setnx 设置会返回一个响应,如果存在再设置
问题
- 可重入锁设置
- 使用 hash 结构解决重入锁问题
- hash 的 key1:锁的名称
- hash 的 key2:锁的持有者线程 UUID
- hash 的 value:重入次数

复习一个基础概念:redis 一般是键值对结构,键始终是字符串,值有多种类型,比如 set、string、list、zset、hash 等。
这里的可重入锁的 key 为 锁名称, value 是 一个 hash 结构,结构为:锁持有者,重入计数 (计数器结构)
由于是 hash 结构,因此这里不能使用 setnx 命令,而是使用的 lua 命令
redisson 的部分代码
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return this.commandExecutor.syncedEval(this.getRawName(), LongCodec.INSTANCE, command, "if ((redis.call('exists', KEYS[1]) == 0) or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getRawName()), new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)});
}这条Lua脚本的目的是原子性地处理一个计数器,通常用于实现锁机制。它检查一个锁键是否存在或一个特定的锁标识是否存在,如果满足条件,则递增锁标识的计数器,并设置或更新键的过期时间。如果计数器递增成功,脚本返回nil;如果没有递增(即锁已被其他客户端持有),则返回当前锁的剩余过期时间。

每当我们每次重入一次,则计数 + 1,如果释放锁,计数 - 1
释放锁

锁订阅
锁续期
Lua原子执行原理
Sync锁升级
- 锁升级过程
- 偏向锁 单线程
- 轻量级锁 多线程交互持有
- 重量级锁 锁竞争,发生竞争的判断依据就是根据线程使用CAS指令加锁或者解锁的时候是否失败来决定的
- 重量级锁实现原理
- sychronized底层是 monitor(监视器) 实现的,
- monitor 包含三个东西:owner、entrylist、waitset,另外monitor是属于锁对象的,mark word和monitor都是对象头的一部分。
Redis红锁,主从情况下问题、持久化不彻底问题
Redis消息队列,List,发布订阅,Stream实现及如何保证可靠性,消息确认机制
消息队列
通常三种方式来实现消息队列
- 1、List
- 2、发布订阅
- 3、Stream (Redis 5.0 中加入的数据结构)
通过 List 来实现消息队列
使用 Redis 的列表(list)数据结构来创建一个消息队列。列表的两端分别代表队列的头部(消费者读取端)和尾部(生产者写入端)。
# 使用 RPUSH 命令将消息添加到队列尾部
127.0.0.1:6379> RPUSH order_queue "order_data_1"
# 使用 LPOP 命令从队列头部读取消息
127.0.0.1:6379> LPOP order_queue常用命令
- RPUSH:添加元素到列表的尾部(右边)
- LPOP:移除并返回列表头部(左边)的元素
- BLPOP:BLPOP 是 LPOP 的阻塞版本,它移除并返回列表头部的元素。如果没有元素可供移除,BLPOP 会阻塞直到有新元素被添加或者超时
消息队列实现: LPUSH + RPOP 或者 RPUSH + LPOP ;一般会使用阻塞方式读取消息:BLPOP 或者 BRPOP
基于发布订阅实现消息队列
使用 Redis 的发布订阅会有 4 个基础概念:
- 发布者(Publisher):负责发送消息的客户端或服务。
- 订阅者(Subscriber):负责接收消息的客户端或服务。
- 频道(Channel):消息发布的目的地,订阅者监听这些频道以接收消息。
- 模式(Pattern):一种基于通配符的频道匹配规则,允许订阅者订阅符合特定模式的频道。
使用示例:
#订阅消息,可以多个订阅者订阅一个频道
127.0.0.1:6379> SUBSCRIBE mychannel
#发布消息,发布者使用 `PUBLISH` 命令向一个或多个频道发送消息。
127.0.0.1:6379> PUBLISH mychannel "Hello, Redis!"订阅者在接收到消息后,可以根据业务逻辑进行处理。
订阅者还可以使用 PSUBSCRIBE 命令订阅符合特定模式的频道,这在需要订阅多个类似频道时非常有用。
127.0.0.1:6379> PSUBSCRIBE mychannel:*通过 stream 实现消息队列
基本概念
- Stream:Redis Streams 是一个消息队列,它是一个包含多个消息的序列。
- 消息(Message):Stream 中的每个元素称为消息,每个消息都有一个唯一的 ID。
- 消费者组(Consumer Group):消费者组是一组消费者,它们共享对 Stream 的访问,可以并行处理消息。
- XADD:用于向 Stream 添加消息的命令。
- XREAD:用于从 Stream 读取消息的命令。
- XACK:用于确认消息已经被处理的命令。
- XGROUP:用于创建消费者组的命令。
- XPENDING:查看消费者组未处理的消息数量的命令。
使用示例
#创建 Stream,使用 `XADD` 命令向 Stream 添加消息。
127.0.0.1:6379> XADD mystream * myfield1 "Hello" myfield2 "World"
#创建消费者组:使用 `XGROUP` 命令创建一个消费者组
127.0.0.1:6379> XGROUP CREATE mystream mygroup $ MKSTREAM
#从消费者组中读取消息,消费者使用 `XREADGROUP` 命令从消费者组中读取消息。
127.0.0.1:6379> XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream >
#消息确认,消费者处理完消息后,使用 `XACK` 命令确认消息。
127.0.0.1:6379> XACK mystream mygroup <message_id>代码示例略过
缓存击穿、穿透、雪崩、布隆过滤器的实现
过期时间
一般是缓存数据设置过期时间 + 设置淘汰策略
当热点数据设置有效时间后,会采用预热(应用启动时加载),写入时续期,访问时续期,或者定时任务执行(为即将过期的热点数据执行续期操作);
通过设置有效时间,可以有效过滤掉冷门时间,减少 redis 中的使用内存。
缓存击穿
解决方案
- 1、热点数据不设置过期时间
- 2、互斥锁
- 使用分布式锁