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.7redis-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>### 基本数据类型<a name="bebvp"></a>#### Generic Commands<a name="qvOSs"></a>#### String<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: passwordlettuce:pool:max-wait: 100
序列化
默认序列化方式(
JdkSerializationRedisSerializer)if (this.defaultSerializer == null) {this.defaultSerializer = new JdkSerializationRedisSerializer}
默认序列化方式会出现乱码问题,因此使用
**StringRedisTemplate**。它的key和value的编码都采用public static final **StringRedisSerializer **_UTF_8_;这种方式,不会出现问题@Autowiredprivate 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();
//创建订单 设置代金券idVoucherOrder order = new VoucherOrder();order.setVoucherId(voucherId);//利用Redis的自增策略生成唯一IDlong id = redisIdGenerator.IdGenerator(VOUCHER_ORDER_ID_PREFIX);order.setId(id);//设置用户idUserDTO user = UserHolder.getUser();order.setUserId(user.getId());//将订单存入数据库save(order);return order;}
:::danger但是,在后台服务器**集群架构下**,Nginx做**轮询**,**仍然会导致并发**问题。:::- **原因**:有多个JVM存在- 因此要找到一种跨进程的锁,**外部的,共享的**。- 因此很自然可以想到** **[**之前在解决热点key问题缓存重建时用到的锁**](#KHCMc)**(Redis Setnx)**> 至此,可以引入 **分布式锁**<a name="JsE25"></a>###### [分布式锁](#Vvm00)- 满足分布式系统或集群模式下**多进程可见**且互斥的锁- 满足 `高可用、高性能(并发)、安全性(锁的释放)`---<a name="BMIjo"></a>#### 秒杀业务优化<br /><br />```lua-- 1. 参数列表-- 1.1 优惠券idlocal voucherId = ARG[1]-- 1.2 用户idlocal userId = ARG[2]-- 2. 数据keylocal stockKey = "seckill:stock"..voucherIdlocal orderKey = "seckill:order"..voucherId-- 3. 脚本业务-- 3.1 判断库存是否充足if (tonumber(redis.call('get',stockKey)) <= 0) then-- 3.1.1 库存不足return 1end-- 3.2 判断用户是否下单if redis.call('sismember',orderKey,userId) == 1 then-- 3.3.1 用户已经下单return 2end-- 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.luaLong 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
完善实现






=======================业务流程======================





