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

    1. [root@VM-xavier-CentOS7 downloads]# tar -zxvf redis-6.2.7.tar.gz
  • 编译安装:进入 redis 目录, 执行 make && make install命令

    1. [root@VM-xavier-CentOS7 redis-6.2.7]# make && make install
  • 安装目录:

    1. [root@VM-xavier-CentOS7 bin]# whereis redis-6.2.7
    2. redis-6.2: /usr/src/downloads/redis-6.2.7

Redis 启动

:::info 通过配置文件启动 redis-server /usr/src/downloads/redis-6.2.7/redis.conf :::

  1. 进入redis.conf ( 在redis安装目录中 )
  2. 修改配置
    1. 监听地址
      1. Redis服务器监听的地址,默认是127.0.0.1(只有本地可以访问)
      2. bind 127.0.0.1
    2. 守护进程
      1. daemonize yes
    3. 密码
      1. 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

  1. ---
  2. <a name="aVCp3"></a>
  3. #### Redis - CLI
  4. [=======CLI连接命令Manual========](https://redis.io/docs/manual/cli/#host-port-password-and-database)
  5. ---
  6. <a name="mUzBc"></a>
  7. ### 基本数据类型
  8. ![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数据类型")
  9. <a name="bebvp"></a>
  10. #### Generic Commands
  11. ![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")
  12. <a name="qvOSs"></a>
  13. #### String
  14. ![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/)
  15. :::info
  16. **其他数据类型详见官网 **[Commands Reference](https://redis.io/commands/)
  17. :::
  18. <a name="O0duB"></a>
  19. ### Redis Java Client
  20. <a name="PuGID"></a>
  21. #### Jedis
  22. <a name="mwZb6"></a>
  23. ##### 起步使用:
  24. 1. 依赖**:**
  25. ```java
  26. <dependency>
  27. <groupId>redis.clients</groupId>
  28. <artifactId>jedis</artifactId>
  29. <version>4.2.0</version>
  30. </dependency>
  1. 建立连接
    1. Jedis jedis = new Jedis("121.5.43.28", 6379);
    2. jedis.auth("password");

Jedis连接池

由于 Redis 是线程不安全的,因此要保证为每一个线程提供一个Jedis对象。
因此Jedis提供了 **redis.clients.jedis.JedisPool**对象。
详细使用 见 —> Jedis Connection Pool

SpringDataRedis

SpringDataRedis 提供 **RedisTemplate**工具类,又将操作不同数据类型的API封装到不同的类中
1652251421(1).png

快速起步
  • 依赖

    1. <dependency>
    2. <groupId>org.springframework.boot</groupId>
    3. <artifactId>spring-boot-starter-data-redis</artifactId>
    4. </dependency>
    5. <dependency>
    6. <groupId>org.apache.commons</groupId>
    7. <artifactId>commons-pool2</artifactId>
    8. </dependency>
  • 配置文件

    1. redis:
    2. password: password
    3. lettuce:
    4. pool:
    5. max-wait: 100

    序列化
  • 默认序列化方式(JdkSerializationRedisSerializer

    1. if (this.defaultSerializer == null) {
    2. this.defaultSerializer = new JdkSerializationRedisSerializer
    3. }

    默认序列化方式会出现乱码问题,因此使用**StringRedisTemplate**。它的key和value的编码都采用
    public static final **StringRedisSerializer **_UTF_8_;这种方式,不会出现问题

    1. @Autowired
    2. private StringRedisTemplate stringRedisTemplate;


缓存更新策略

1652447741(1).png
三种方式的选择根据业务场景决定。

主动更新策略

1652448327(1).png
1652449138(1).png
1652451254(1).png
===================缓存更新策略(黑马)=====================


缓存穿透

  • 产生原因:用户请求的数据在缓存和数据库中都不存在,那么请求一定会打到数据库。
  • 解决方案
    • 缓存空值:会造成一定的内存浪费,和短期的不一致性
    • 布隆过滤
    • 增强 Id 复杂度,避免被猜测 Id规律
    • 数据的基础格式校验(主动过滤一些不符合的数据)
    • 加强用户权限校验(权限+限流)
    • 做好热点参数的限流
      1. //如果数据库中不存在店铺信息,缓存空对象
      2. stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
      3. return Result.fail("没有店铺信息");
      1. if ("".equals(shop)) {
      2. return Result.fail("空对象");
      3. }

缓存雪崩

  • 概念:指在同一时段 大量缓存key失效 或Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

:::info 场景一:缓存预热 :::

  • 缓存预热时,从数据库批量导入数据,设置了相同的TTL值,导致这些缓存同时到期。
  • 解决:给不同key的TTL添加随机值

:::info 场景二:Redis宕机 ::: 解决

  1. 建立 Redis集群 ,高可用
  2. 给缓存业务添加降级限流策略
    1. 提前做好容灾、降级、限流,当服务挂掉,采用fail-fast (快速失败) 机制,而不是把请求压到数据库

:::info 场景三:普通场景 ::: 解决:

  • 建立多级缓存
    • Nginx缓存
    • Redis缓存
    • JVM本地缓存
    • 数据库缓存

缓存击穿(热点key问题)

也叫 热点Key问题,就是一个 被高并发访问并且缓存重建业务较复杂的key失效,无数的请求会瞬间给数据库带来巨大压力。

:::info 解决方案: :::

互斥锁

1652511570(1).png

逻辑过期

  • 创建 逻辑过期缓存对象
  • 发现缓存逻辑过期时,开启新线程去重建缓存,并返回旧数据
  • 这种解决方案不保证数据一致性

1652511623(1).png


缓存工具类封装

  1. public <R,ID> R getSolvePenetrate (String Prefix,
  2. ID id,
  3. Class<R> type,
  4. Function<ID,R> dbFallBack,
  5. Long expireTime,
  6. TimeUnit unit) {....}
  • 泛型: R 为所需要查询的类型,ID 为所需查询的 ID
  • 函数接口**Function**调用者传入Lambda自定义处理逻辑,返回 R 类型的结果

基于Setnx实现分布式锁

优惠券秒杀业务中,已经引出了分布式锁。

Redis基于Setnx命令实现分布式锁

分布式锁误删问题

1652766968(1).png

  • 解决:锁的value设置为 UUID+ThreadId,在释放锁的时候根据value校验,是否匹配,如果不匹配说明不是当前线程的锁。

1653111616(1).png :::danger 原因:释放锁的动作有两个过程:1. 判断锁标识 ; 2. 释放锁
由于是两个操作,因此只要保证了原子性,就可以保证并发安全 ::: :::info 解决Lua脚本 可以保证操作原子性 :::

image.png


REDISSON的分布式锁

1653557501(1).png


1653114951(1).png

Redisson可重入锁

1653118751(1).png
1653118814(1).png

  • 原理:用Hash存锁的value(setnx方案中用的是String),field**随机UUID+线程ID**value维护锁被获取(value++)和被释放(value—)。
  • 注意:需要保证操作的原子性,因此用 Lua脚本 维护锁。

重试和watchDog

1653120634(1).png


Redisson解决主从一致性

主从模式(读写分离)

实际生产,搭建Redis主从模式,提高可用性(节点负责并将数据同步给从节点)(节点负责

:::danger 问题:主从数据同步有延时,存在并发安全问题。 ::: 1653556691(1).png :::success 因此,采用多个独立的Redis节点,如下图 ::: 1653551838(1).png

  • 解释: Redisson的Multilock,采用多个独立的Redis节点,从每个节点都获取到锁才算获取锁成功。
  • 优势保留主从模式保证高可用的同时消除了主从同步延迟造成的数据不一致

课程链接


分布式锁总结

1653556189(1).png


Redis实现业务

Redis实现点评应用功能。

1652269071(1).png

登录校验

  • 古早时期,后台只有一台服务器,且不会考虑水平扩展,负载均衡。因此,用session保存登录状态(session 基于tomcat内存)
  • 现代,为解决集群session共享问题,用redis替代session。Redis同样基于内存,且保持数据一致性。

    实现:多台tomcat的数据共享通过Redis实现,用Redsi替代session完成集群内部的数据共享。


查询+缓存业务

缓存加快获得响应的速度,但也有一些问题。

  1. 缓存数据库双写不一致问题
  2. 缓存穿透、热点key,缓存雪崩(多级缓存,不同的TTL等)

缓存穿透解决

基于互斥锁解决缓存击穿问题
  • 这里的互斥锁自定义实现,基于Redis的 **setnx** 的特性,第一个线程 执行setnx 相当于获取锁,后面的线程都无法 setnx,仅由一个线程查询数据库重建缓存。
  • 注意:在第一个线程执行**setnx**,注意设置 expire ,避免锁不释放。

基于逻辑过期解决热点key问题

==========处理逻辑=========


优惠券秒杀业务

不仅优惠券秒杀业务,电商业务中都会涉及订单编号
而订单编号不能采用自增ID,1. 规律太明显,2. 分布式系统下编号会重复
全局唯一ID可以解决这一问题。

分布式唯一ID生成器

:::info 基于Redis自增的唯一ID策略 ::: 1652681964(1).png

  • 格式解读:时间戳+计数器

    • 符号位:0,代表ID为正数
    • 时间戳:以秒为单位,
    • 序列号:32bit,秒内计数器,支持每秒产生2^32个不同的ID。

      1. private static final int SEQUENCE_BIT_LENGTH = 32;
      2. private static final long BEGIN_TIMESTAMP = 1640995200L;
      3. public long IdGenerator(String Prefix) {
      4. //生成时间戳
      5. LocalDateTime now = LocalDateTime.now();
      6. long currentTime = now.toEpochSecond(ZoneOffset.UTC);
      7. long diff = currentTime - BEGIN_TIMESTAMP;
      8. //生成序列号(以incr开头,以时间戳结尾,可以实现统计功能,)
      9. String uniqueTimePostfix = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
      10. long increment = stringRedisTemplate.opsForValue().increment("incr:" + Prefix + ":" + uniqueTimePostfix);
      11. return diff << SEQUENCE_BIT_LENGTH | increment;
      12. }
  • Key"incr:" + Prefix + ":" + uniqueTimePostfix

  • Value:自增

超卖问题

本质上是并发修改问题,解决方案:锁。

悲观锁

用同步锁,Lock机制实现同步

乐观锁
  • 由程序员维护的锁机制,适用于读多写少的场景。例如我们之前在热点key缓存重建中用到的lock

    CAS:一种特殊的乐观锁机制,把 被并发修改的数据 作为版本号,每次在更新之前检查是否被更新。 问题:CAS可能会导致失败率太高。

  1. //事务:减库存,创建订单
  2. //乐观锁,在更新库存前先获取库存,看和数据库中的数据是否一致
  3. boolean updateStock = iSeckillVoucherService.update()
  4. .set("stock", voucher.getStock() - 1)
  5. .eq("voucher_id", voucherId)
  6. .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()) {

    1. QueryWrapper<VoucherOrder> wrapper =
    2. new QueryWrapper<VoucherOrder>()
    3. .eq("userId", UserHolder.getUser().getId());
    4. //查询 号码为userId的用户 下的 优惠券id 为 voucher_id 的优惠券的订单数量
    5. Integer count = query().eq("userId", UserHolder.getUser().getId()).eq("voucher_id", voucherId).count();
    6. if (count>0) return null;
    7. //事务:减库存,创建订单
    8. //乐观锁,在更新库存前先获取库存,看和数据库中的数据是否一致
    9. boolean updateStock = iSeckillVoucherService.update()
    10. .set("stock", voucher.getStock() - 1)
    11. .eq("voucher_id", voucherId)
    12. .gt("stock", 0).update();
  1. //创建订单 设置代金券id
  2. VoucherOrder order = new VoucherOrder();
  3. order.setVoucherId(voucherId);
  4. //利用Redis的自增策略生成唯一ID
  5. long id = redisIdGenerator.IdGenerator(VOUCHER_ORDER_ID_PREFIX);
  6. order.setId(id);
  7. //设置用户id
  8. UserDTO user = UserHolder.getUser();
  9. order.setUserId(user.getId());
  10. //将订单存入数据库
  11. save(order);
  12. return order;
  13. }
  1. :::danger
  2. 但是,在后台服务器**集群架构下**,Nginx做**轮询**,**仍然会导致并发**问题。
  3. :::
  4. ![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 "服务器集群下,同步锁失效原理图")
  5. - **原因**:有多个JVM存在
  6. - 因此要找到一种跨进程的锁,**外部的,共享的**。
  7. - 因此很自然可以想到** **[**之前在解决热点key问题缓存重建时用到的锁**](#KHCMc)**(Redis Setnx)**
  8. > 至此,可以引入 **分布式锁**
  9. <a name="JsE25"></a>
  10. ###### [分布式锁](#Vvm00)
  11. - 满足分布式系统或集群模式下**多进程可见**且互斥的锁
  12. - 满足 `高可用、高性能(并发)、安全性(锁的释放)`
  13. ---
  14. <a name="BMIjo"></a>
  15. #### 秒杀业务优化
  16. ![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 "业务逻辑")
  17. ```lua
  18. -- 1. 参数列表
  19. -- 1.1 优惠券id
  20. local voucherId = ARG[1]
  21. -- 1.2 用户id
  22. local userId = ARG[2]
  23. -- 2. 数据key
  24. local stockKey = "seckill:stock"..voucherId
  25. local orderKey = "seckill:order"..voucherId
  26. -- 3. 脚本业务
  27. -- 3.1 判断库存是否充足
  28. if (tonumber(redis.call('get',stockKey)) <= 0) then
  29. -- 3.1.1 库存不足
  30. return 1
  31. end
  32. -- 3.2 判断用户是否下单
  33. if redis.call('sismember',orderKey,userId) == 1 then
  34. -- 3.3.1 用户已经下单
  35. return 2
  36. end
  37. -- 4. 扣库存,下单
  38. -- 4.1 扣库存
  39. redis.call('incrby',stockKey,-1)
  40. redis.call('sadd',orderKey,userId)
  41. return 0
  1. private static final DefaultRedisScript<Long> SECKILL_AUTH;
  2. static {
  3. SECKILL_AUTH = new DefaultRedisScript<>();
  4. SECKILL_AUTH.setLocation(new ClassPathResource("/Seckill.lua"));/* /resource/Seckill.lua */
  5. SECKILL_AUTH.setResultType(Long.class);
  6. }
  7. //执行lua脚本,用来判断是否有购买资格 /resource/Seckill.lua
  8. Long result = stringRedisTemplate.execute(SECKILL_AUTH, Collections.EMPTY_LIST, voucherId.toString(), UserHolder.getUser().getId().toString());

将业务链条缩短,用lua脚本做购买资格判断(库存,一人一单)
1653567220(1).png

  1. 内存限制问题:jdk的阻塞队列基于JVM内存,高并发时内存空间不足。
  2. 数据安全问题:当前方案将订单信息存入Redis,如果Redis服务宕机,订单信息会丢失。

点击跳转基于消息队列完成优惠券异步秒杀


Feed流

1654172360669.png
1654173270521.png
1654172887856.png

1654173194693.png
1654173214162.png


Feed流滚动分页

1654174629447.png

  • 收件箱的实现:在Redis中实现,使用 **SET**数据结构。
    • key:由于每个粉丝都有自己的收件箱,因此key需要包含用户id
    • value:收件内容的标识,由发布者添加。
    • score:时间戳(针对timeline的feed流实现方案)。
  • 为什么采用**SET**结构?

    • 由于需要实现根据时间戳排序(timeline方案)**滚动分页**(数据在更新变化,不能根据索引进行传统分页)
    • SET结构可以score的范围返回结果,每一次分页查询都维护一个游标,作为下一次查询的起点(参照)
    • 1654176069040.png
    • 1654177766309.png

      附件的人,附件商户

      image.png
      1654321180193.png
      业务实现思路
  • 首先将业务对象(如店铺,或附近的人)的经纬度(longtitude and latitude)和主键数据(作为member)导入Redis,才能进行后续操作。 | 业务需求 | 用户在app中查找附近的人 | | —- | —- | | 实现流程 |
    1. 用户允许访问地理位置权限
    1. 前端发送用户地理坐标,后端接收坐标参数和半径要求,在Redis中处理后返回member值(一般是数据库中的主键)
    1. 需要根据Redis返回的member值(一般是数据库中的主键)到数据库中查询才能返回结果
    |


BitMap实现签到

Redis中使用String实现BitMap,因此最大上限为512M,转换为bit为2^32位


HyperLogLog实现UV统计

1654326163062.png
1654326376356.png


MQ消息队列(基于Redis实现

1653570142(1).png
MQ保证数据安全

  • MQ 会将消息持久化
  • MQ 会要求消费者给出回应,否则消息会一直存在,继续发送给消费者

基于Redis订阅发布

  • 订阅发布模式,基于信号量
  • 支持多生产,多消费

1653617224(1).png :::danger 缺点

  1. PubSub只是信号量(消息),不支持持久化
  2. 如果channel没有订阅者,消息直接丢失
  3. 没有确认重传机制,数据不安全 :::

基于Stream

  • 可持久化的消息队列模型

    初级实现
  • XADD

    • 1653618537(1).png
  • XREAD
    • 1653618480(1).png
      1. 可持久化
      2. 阻塞
      3. 回溯,可以读取之前的消息
      4. 但会消息漏读 (如果起始ID选的是$)

完善实现

1653619294(1).png
1653705181(1).png
1653705254(1).png
1653705856(1).png

1653621195(1).png
1653967432577.png
=======================业务流程======================