Tag: #分布式锁 #查询缓存 #消息队列stream
基础
Redis简介
Remote Dictionary Server,基于内存的键值型NoSQL服务器。
:::info
SQL 与 NoSql 比较
:::
|
| SQL | NoSql |
| —- | —- | —- |
| 存储方式 | 磁盘 | 内存 |
| 扩展性 | 垂直 | 水平 |
| 使用场景 |
1. 对数据一致性有要求
1. 数据结构固定
|
1. 对性能要求高( 秒杀 )
1. 对数据一致性要求不高
1. 数据结构不固定
|
Redis特性:
- 基于 C 编写
- 单线程,命令具有原子性
- 低延迟(基于内存,IO多路复用)
- 支持数据持久化
- 支持集群,主从集群,分片集群(水平扩展)
- 支持多语言客户端
Redis Download :
:::danger 注意:安装Redis需要gcc依赖,yum install -y gcc tcl :::
解压 : tar -zxvf tar -zxvf redis-6.2.7.tar.gz
[root@VM-xavier-CentOS7 downloads]# tar -zxvf redis-6.2.7.tar.gz
编译安装:进入 redis 目录, 执行
make && make install
命令[root@VM-xavier-CentOS7 redis-6.2.7]# make && make install
安装目录:
[root@VM-xavier-CentOS7 bin]# whereis redis-6.2.7
redis-6.2: /usr/src/downloads/redis-6.2.7
Redis 启动
:::info 通过配置文件启动 redis-server /usr/src/downloads/redis-6.2.7/redis.conf :::
- 进入redis.conf ( 在redis安装目录中 )
- 修改配置
- 监听地址
- Redis服务器监听的地址,默认是127.0.0.1(只有本地可以访问)
- bind 127.0.0.1
- 守护进程
- daemonize yes
- 密码
- requirepass password ```shell [root@VM-CentOS7 /]# redis-server /usr/src/downloads/redis-6.2.7/redis.conf
- 监听地址
查看Redis运行时进程
[root@VM-xavier-CentOS7 /]# ps -ef|grep redis root 19622 1 0 13:12 ? 00:00:00 redis-server 127.0.0.1:6379 root 19668 8414 0 13:12 pts/0 00:00:00 grep —color=auto redis
---
<a name="aVCp3"></a>
#### Redis - CLI
[=======CLI连接命令Manual========](https://redis.io/docs/manual/cli/#host-port-password-and-database)
---
<a name="mUzBc"></a>
### 基本数据类型
![image.png](https://cdn.nlark.com/yuque/0/2022/png/25412770/1652167110387-87522af2-ba72-416f-8ca5-f9838748ed97.png#clientId=ub0b47a43-c126-4&crop=0&crop=0&crop=1&crop=0.9915&from=paste&height=356&id=u169331ee&margin=%5Bobject%20Object%5D&name=image.png&originHeight=445&originWidth=874&originalType=binary&ratio=1&rotation=0&showTitle=true&size=148735&status=done&style=none&taskId=u3ec4d5cb-a7c5-429d-bd83-1aa0b405984&title=Redis%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B&width=699 "Redis数据类型")
<a name="bebvp"></a>
#### Generic Commands
![image.png](https://cdn.nlark.com/yuque/0/2022/png/25412770/1652167971007-2e894feb-0996-46a3-9ea9-80e5315b9931.png#clientId=ub0b47a43-c126-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=182&id=ubce64496&margin=%5Bobject%20Object%5D&name=image.png&originHeight=227&originWidth=681&originalType=binary&ratio=1&rotation=0&showTitle=true&size=70814&status=done&style=none&taskId=u913e1891-8042-4273-b4e1-13d2cb557b3&title=Generic%20Commands&width=544.8 "Generic Commands")
<a name="qvOSs"></a>
#### String
![image.png](https://cdn.nlark.com/yuque/0/2022/png/25412770/1652168299589-a23a9195-b44c-47de-9595-e6272368eeac.png#clientId=ub0b47a43-c126-4&crop=0&crop=0.0756&crop=1&crop=1&from=paste&height=378&id=u9af6b0b2&margin=%5Bobject%20Object%5D&name=image.png&originHeight=472&originWidth=885&originalType=binary&ratio=1&rotation=0&showTitle=false&size=179124&status=done&style=none&taskId=u15288c24-3990-4f0f-9e15-c6ed3d5f67c&title=&width=708)<br />[Commands Reference](https://redis.io/commands/)
:::info
**其他数据类型详见官网 **[Commands Reference](https://redis.io/commands/)
:::
<a name="O0duB"></a>
### Redis Java Client
<a name="PuGID"></a>
#### Jedis
<a name="mwZb6"></a>
##### 起步使用:
1. 依赖**:**
```java
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.2.0</version>
</dependency>
- 建立连接
Jedis jedis = new Jedis("121.5.43.28", 6379);
jedis.auth("password");
Jedis连接池
由于 Redis 是线程不安全的,因此要保证为每一个线程提供一个Jedis对象。
因此Jedis提供了 **redis.clients.jedis.JedisPool**
对象。
详细使用 见 —> Jedis Connection Pool
SpringDataRedis
SpringDataRedis 提供 **RedisTemplate**
工具类,又将操作不同数据类型的API封装到不同的类中。
快速起步
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
配置文件
redis:
password: password
lettuce:
pool:
max-wait: 100
序列化
默认序列化方式(
JdkSerializationRedisSerializer
)if (this.defaultSerializer == null) {
this.defaultSerializer = new JdkSerializationRedisSerializer
}
默认序列化方式会出现乱码问题,因此使用
**StringRedisTemplate**
。它的key和value的编码都采用public static final **StringRedisSerializer **_UTF_8_;
这种方式,不会出现问题@Autowired
private StringRedisTemplate stringRedisTemplate;
缓存更新策略
三种方式的选择根据业务场景决定。
主动更新策略
===================缓存更新策略(黑马)=====================
缓存穿透
- 产生原因:用户请求的数据在缓存和数据库中都不存在,那么请求一定会打到数据库。
- 解决方案:
- 缓存空值:会造成一定的内存浪费,和短期的不一致性
- 布隆过滤
- 增强 Id 复杂度,避免被猜测 Id规律
- 数据的基础格式校验(主动过滤一些不符合的数据)
- 加强用户权限校验(权限+限流)
- 做好热点参数的限流
//如果数据库中不存在店铺信息,缓存空对象
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("没有店铺信息");
if ("".equals(shop)) {
return Result.fail("空对象");
}
缓存雪崩
- 概念:指在同一时段 大量缓存key失效 或Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
:::info 场景一:缓存预热 :::
- 缓存预热时,从数据库批量导入数据,设置了相同的TTL值,导致这些缓存同时到期。
- 解决:给不同key的TTL添加随机值
:::info 场景二:Redis宕机 ::: 解决:
- 建立 Redis集群 ,高可用
- 给缓存业务添加降级限流策略
- 提前做好容灾、降级、限流,当服务挂掉,采用fail-fast (快速失败) 机制,而不是把请求压到数据库
:::info 场景三:普通场景 ::: 解决:
- 建立多级缓存
- Nginx缓存
- Redis缓存
- JVM本地缓存
- 数据库缓存
缓存击穿(热点key问题)
也叫 热点Key问题,就是一个 被高并发访问并且缓存重建业务较复杂的key失效,无数的请求会瞬间给数据库带来巨大压力。
互斥锁
- 线程串行执行,一个线程重建缓存(数据库操作)前要先获取X锁,其他线程等待并重试
逻辑过期
- 创建 逻辑过期缓存对象
- 发现缓存逻辑过期时,开启新线程去重建缓存,并返回旧数据。
- 这种解决方案不保证数据一致性
缓存工具类封装
public <R,ID> R getSolvePenetrate (String Prefix,
ID id,
Class<R> type,
Function<ID,R> dbFallBack,
Long expireTime,
TimeUnit unit) {....}
- 泛型
: R 为所需要查询的类型,ID 为所需查询的 ID - 函数接口
**Function**
:调用者传入Lambda自定义处理逻辑,返回 R 类型的结果
基于Setnx实现分布式锁
Redis基于Setnx命令实现分布式锁
分布式锁误删问题
- 解决:锁的value设置为 UUID+ThreadId,在释放锁的时候根据value校验,是否匹配,如果不匹配说明不是当前线程的锁。
:::danger
原因:释放锁的动作有两个过程:1. 判断锁标识 ; 2. 释放锁
由于是两个操作,因此只要保证了原子性,就可以保证并发安全
:::
:::info
解决:Lua脚本 可以保证操作原子性
:::
REDISSON的分布式锁
Redisson可重入锁
- 原理:用Hash存锁的value(setnx方案中用的是String),field存
**随机UUID+线程ID**
,value维护锁被获取(value++)和被释放(value—)。 - 注意:需要保证操作的原子性,因此用 Lua脚本 维护锁。
重试和watchDog
Redisson解决主从一致性
主从模式(读写分离)
实际生产,搭建Redis主从模式,提高可用性(主节点负责写并将数据同步给从节点)(从节点负责读)
:::danger 问题:主从数据同步有延时,存在并发安全问题。 ::: :::success 因此,采用多个独立的Redis节点,如下图 :::
- 解释: Redisson的Multilock,采用多个独立的Redis节点,从每个节点都获取到锁才算获取锁成功。
- 优势:保留主从模式,保证高可用的同时消除了主从同步延迟造成的数据不一致。
分布式锁总结
Redis实现业务
Redis实现点评应用功能。
登录校验
- 古早时期,后台只有一台服务器,且不会考虑水平扩展,负载均衡。因此,用session保存登录状态(session 基于tomcat内存)
- 现代,为解决集群session共享问题,用redis替代session。Redis同样基于内存,且保持数据一致性。
实现:多台tomcat的数据共享通过Redis实现,用Redsi替代session完成集群内部的数据共享。
查询+缓存业务
缓存加快获得响应的速度,但也有一些问题。
- 缓存数据库双写不一致问题
- 缓存穿透、热点key,缓存雪崩(多级缓存,不同的TTL等)
缓存穿透解决
基于互斥锁解决缓存击穿问题
- 这里的互斥锁自定义实现,基于Redis的
**setnx**
的特性,第一个线程 执行setnx 相当于获取锁,后面的线程都无法 setnx,仅由一个线程查询数据库重建缓存。 - 注意:在第一个线程执行
**setnx**
,注意设置 expire ,避免锁不释放。
基于逻辑过期解决热点key问题
==========处理逻辑=========
优惠券秒杀业务
不仅优惠券秒杀业务,电商业务中都会涉及订单编号。
而订单编号不能采用自增ID,1. 规律太明显,2. 分布式系统下编号会重复
全局唯一ID可以解决这一问题。
分布式唯一ID生成器
:::info 基于Redis自增的唯一ID策略 :::
格式解读:时间戳+计数器
- 符号位:0,代表ID为正数
- 时间戳:以秒为单位,
序列号:32bit,秒内计数器,支持每秒产生2^32个不同的ID。
private static final int SEQUENCE_BIT_LENGTH = 32;
private static final long BEGIN_TIMESTAMP = 1640995200L;
public long IdGenerator(String Prefix) {
//生成时间戳
LocalDateTime now = LocalDateTime.now();
long currentTime = now.toEpochSecond(ZoneOffset.UTC);
long diff = currentTime - BEGIN_TIMESTAMP;
//生成序列号(以incr开头,以时间戳结尾,可以实现统计功能,)
String uniqueTimePostfix = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
long increment = stringRedisTemplate.opsForValue().increment("incr:" + Prefix + ":" + uniqueTimePostfix);
return diff << SEQUENCE_BIT_LENGTH | increment;
}
Key:
"incr:" + Prefix + ":" + uniqueTimePostfix
- Value:自增
超卖问题
悲观锁
乐观锁
- 由程序员维护的锁机制,适用于读多写少的场景。例如我们之前在热点key缓存重建中用到的lock。
CAS:一种特殊的乐观锁机制,把 被并发修改的数据 作为版本号,每次在更新之前检查是否被更新。 问题:CAS可能会导致失败率太高。
//事务:减库存,创建订单
//乐观锁,在更新库存前先获取库存,看和数据库中的数据是否一致
boolean updateStock = iSeckillVoucherService.update()
.set("stock", voucher.getStock() - 1)
.eq("voucher_id", voucherId)
.gt("stock", 0).update();
一人一单问题
为防止同一用户下单多次,用
synchronized(userId.toString().intern())
包裹业务逻辑 ```java @Transactional public VoucherOrder getVoucherOrder(Long voucherId, SeckillVoucher voucher) { Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()) {
QueryWrapper<VoucherOrder> wrapper =
new QueryWrapper<VoucherOrder>()
.eq("userId", UserHolder.getUser().getId());
//查询 号码为userId的用户 下的 优惠券id 为 voucher_id 的优惠券的订单数量
Integer count = query().eq("userId", UserHolder.getUser().getId()).eq("voucher_id", voucherId).count();
if (count>0) return null;
//事务:减库存,创建订单
//乐观锁,在更新库存前先获取库存,看和数据库中的数据是否一致
boolean updateStock = iSeckillVoucherService.update()
.set("stock", voucher.getStock() - 1)
.eq("voucher_id", voucherId)
.gt("stock", 0).update();
//创建订单 设置代金券id
VoucherOrder order = new VoucherOrder();
order.setVoucherId(voucherId);
//利用Redis的自增策略生成唯一ID
long id = redisIdGenerator.IdGenerator(VOUCHER_ORDER_ID_PREFIX);
order.setId(id);
//设置用户id
UserDTO user = UserHolder.getUser();
order.setUserId(user.getId());
//将订单存入数据库
save(order);
return order;
}
:::danger
但是,在后台服务器**集群架构下**,Nginx做**轮询**,**仍然会导致并发**问题。
:::
![1652704084(1).png](https://cdn.nlark.com/yuque/0/2022/png/25412770/1652704087471-a31577b2-1188-4074-a64c-04187582f152.png#clientId=u1667cfbd-962f-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=472&id=u4b864e83&margin=%5Bobject%20Object%5D&name=1652704084%281%29.png&originHeight=590&originWidth=1305&originalType=binary&ratio=1&rotation=0&showTitle=true&size=350681&status=done&style=none&taskId=u376300f0-0653-4f3d-b0ec-3ed19457c78&title=%E6%9C%8D%E5%8A%A1%E5%99%A8%E9%9B%86%E7%BE%A4%E4%B8%8B%EF%BC%8C%E5%90%8C%E6%AD%A5%E9%94%81%E5%A4%B1%E6%95%88%E5%8E%9F%E7%90%86%E5%9B%BE&width=1044 "服务器集群下,同步锁失效原理图")
- **原因**:有多个JVM存在
- 因此要找到一种跨进程的锁,**外部的,共享的**。
- 因此很自然可以想到** **[**之前在解决热点key问题缓存重建时用到的锁**](#KHCMc)**(Redis Setnx)**
> 至此,可以引入 **分布式锁**
<a name="JsE25"></a>
###### [分布式锁](#Vvm00)
- 满足分布式系统或集群模式下**多进程可见**且互斥的锁
- 满足 `高可用、高性能(并发)、安全性(锁的释放)`
---
<a name="BMIjo"></a>
#### 秒杀业务优化
![1653560495(1).png](https://cdn.nlark.com/yuque/0/2022/png/25412770/1653560546484-d45e4d67-7067-4b3c-b866-ea38acc7ef71.png#clientId=u8cd3707d-6305-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=406&id=ue97d26e6&margin=%5Bobject%20Object%5D&name=1653560495%281%29.png&originHeight=508&originWidth=1091&originalType=binary&ratio=1&rotation=0&showTitle=true&size=164920&status=done&style=none&taskId=udb8aaaf7-66a3-429c-bb6f-b4ddd816ca6&title=%E5%8E%9F%E4%B8%9A%E5%8A%A1%E9%80%BB%E8%BE%91%E4%B8%B2%E8%A1%8C%E6%89%A7%E8%A1%8C%EF%BC%8C%E6%95%88%E7%8E%87%E4%BD%8E&width=872.8 "原业务逻辑串行执行,效率低")<br />![1653560762(1).png](https://cdn.nlark.com/yuque/0/2022/png/25412770/1653560766716-f0aa1b1b-036b-4921-ad77-7fb967cd08d7.png#clientId=u8cd3707d-6305-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=604&id=u3e1152cc&margin=%5Bobject%20Object%5D&name=1653560762%281%29.png&originHeight=755&originWidth=1595&originalType=binary&ratio=1&rotation=0&showTitle=true&size=442178&status=done&style=none&taskId=u3d847678-ddab-46ce-a21f-e9ac31dbfe0&title=%E4%BC%98%E5%8C%96%E4%B8%9A%E5%8A%A1%E6%B5%81%E7%A8%8B%EF%BC%8C%E5%8A%A0%E5%85%A5%E5%BC%82%E6%AD%A5&width=1276 "优化业务流程,加入异步")<br />![1653561386(1).png](https://cdn.nlark.com/yuque/0/2022/png/25412770/1653561394028-d7743af4-9c3d-4e2c-b352-564ec262d1f1.png#clientId=u8cd3707d-6305-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=594&id=u4647a3fb&margin=%5Bobject%20Object%5D&name=1653561386%281%29.png&originHeight=743&originWidth=1519&originalType=binary&ratio=1&rotation=0&showTitle=true&size=377311&status=done&style=none&taskId=u060205c4-d6e0-4b76-a258-e70ae12ff1a&title=%E4%B8%9A%E5%8A%A1%E9%80%BB%E8%BE%91&width=1215.2 "业务逻辑")
```lua
-- 1. 参数列表
-- 1.1 优惠券id
local voucherId = ARG[1]
-- 1.2 用户id
local userId = ARG[2]
-- 2. 数据key
local stockKey = "seckill:stock"..voucherId
local orderKey = "seckill:order"..voucherId
-- 3. 脚本业务
-- 3.1 判断库存是否充足
if (tonumber(redis.call('get',stockKey)) <= 0) then
-- 3.1.1 库存不足
return 1
end
-- 3.2 判断用户是否下单
if redis.call('sismember',orderKey,userId) == 1 then
-- 3.3.1 用户已经下单
return 2
end
-- 4. 扣库存,下单
-- 4.1 扣库存
redis.call('incrby',stockKey,-1)
redis.call('sadd',orderKey,userId)
return 0
private static final DefaultRedisScript<Long> SECKILL_AUTH;
static {
SECKILL_AUTH = new DefaultRedisScript<>();
SECKILL_AUTH.setLocation(new ClassPathResource("/Seckill.lua"));/* /resource/Seckill.lua */
SECKILL_AUTH.setResultType(Long.class);
}
//执行lua脚本,用来判断是否有购买资格 /resource/Seckill.lua
Long result = stringRedisTemplate.execute(SECKILL_AUTH, Collections.EMPTY_LIST, voucherId.toString(), UserHolder.getUser().getId().toString());
将业务链条缩短,用lua脚本做购买资格判断(库存,一人一单)
- 内存限制问题:jdk的阻塞队列基于JVM内存,高并发时内存空间不足。
- 数据安全问题:当前方案将订单信息存入Redis,如果Redis服务宕机,订单信息会丢失。
Feed流
Feed流滚动分页
- 收件箱的实现:在Redis中实现,使用
**SET**
数据结构。- key:由于每个粉丝都有自己的收件箱,因此key需要包含用户id
- value:收件内容的标识,由发布者添加。
- score:时间戳(针对timeline的feed流实现方案)。
为什么采用
**SET**
结构?首先将业务对象(如店铺,或附近的人)的经纬度(longtitude and latitude)和主键数据(作为member)导入Redis,才能进行后续操作。 | 业务需求 | 用户在app中查找附近的人 | | —- | —- | | 实现流程 |
1. 用户允许访问地理位置权限
1. 前端发送用户地理坐标,后端接收坐标参数和半径要求,在Redis中处理后返回member值(一般是数据库中的主键)
1. 需要根据Redis返回的member值(一般是数据库中的主键)到数据库中查询才能返回结果
|
BitMap实现签到
Redis中使用String实现BitMap,因此最大上限为512M,转换为bit为2^32位。
HyperLogLog实现UV统计
MQ消息队列(基于Redis实现
MQ保证数据安全
- MQ 会将消息持久化
- MQ 会要求消费者给出回应,否则消息会一直存在,继续发送给消费者
基于Redis订阅发布
- 订阅发布模式,基于信号量。
- 支持多生产,多消费
:::danger 缺点:
- PubSub只是信号量(消息),不支持持久化
- 如果channel没有订阅者,消息直接丢失。
- 没有确认重传机制,数据不安全 :::
基于Stream
完善实现
=======================业务流程======================