Skip to content

概念

  1. 锁订阅: 锁订阅指的是使用Redis的发布/订阅机制(Pub/Sub)来监听锁的释放事件。在某些场景下,一个进程获取不到锁时,可以订阅一个频道,当锁被释放时发布消息通知订阅的进程,让这些进程尝试重新获取锁,注意、它不可靠。

  2. 锁续期: 锁续期(Lock Renewal)是指在持有锁的进程即将超时失效前,主动延长锁的过期时间,避免锁被意外释放。这通常通过一个后台线程来定期检查锁的过期时间并刷新TTL(Time to Live)。在Redis中,可以通过调用EXPIRE命令来延长锁的生存时间。

  3. Lua原子执行原理:1、 Lua脚本在Redis中是原子执行的。Redis的EVAL命令用于执行Lua脚本,所有操作在脚本执行期间都是原子的,这确保了复杂操作的原子性。客户端与Redis服务器之间的交互通常需要多次往返网络请求,2、而使用Lua脚本可以将多个命令组合成一个脚本,一次发送到服务器执行,减少了网络延迟。保证数据一致性

  4. Sync锁升级: Sync锁升级指的是从一个低级别的锁升级到高级别的锁。例如,一个进程最初可能持有一个读锁(共享锁),在需要写入时,需要将其升级为写锁(独占锁)。在分布式环境中,这需要保证升级操作的原子性,防止在升级过程中其他进程获得锁。在Redis中,可以通过Lua脚本来确保锁升级过程的原子性。

  5. big KeyRedis中的Big Key问题:排查与解决思路-腾讯云开发者社区-腾讯云 (tencent.com)

    以下情况被称为大key,不唯一,看业务

    • String 类型的 key 对应的value超过 10 MB。

    • list、set、hash、zset等集合类型,集合元素个数超过 5000个。

    原因:

    • 对象序列化后的大小过大。
    • 存储大量数据的容器,如set、list等。
    • 大型数据结构,如bitmap、hyperloglog等。

    排查:

    Redis自带的 BIGKEYS 命令可以查询当前Redis中所有key的信息,对整个数据库中的键值对大小情况进行统计分析。BIGKEYS命令会扫描整个数据库,这个命令本身会阻塞Redis,找出所有的大键,并将其以一个列表的形式返回给客户端。

    sql
    redis-cli --bigkeys

    解决思路:

    分割大key

    将Big Key拆分成多个小key。这个方法比较简单,但是需要修改应用程序的代码。就像是把一个大蛋糕切成小蛋糕一样,有点费力,但是可以解决问题。

    或者尝试将Big Key转换成Redis的其他数据结构。例如,将Big Key转换成Hash,List或者Set等数据结构。

    对象压缩

    如果大key的产生原因主要是由于对象序列化后的体积过大,我们可以考虑使用压缩算法来减小对象的大小。需要在客户端使用一些压缩算法对数据进行压缩和解压缩操作,例如LZF、Snappy等。

    直接删除

    如果你使用的是Redis 4.0+的版本,可以直接使用 unlink命令去异步删除大key。4.0以下的版本 可以考虑使用 scan命令,分批次删除。

Redis的三大删除策略:

1 定时删除

创建一个定时器,当key设置有过期时间,且过期时间到达时,由定时器任务立即执行对键的删除操作

2 情性删除

数据到达过期时间,不做处理。等下次访问该数据时,如果未过期,返回数据 ;发现已过期,删除,返回不存在。

3 内存淘汰

周期性轮询redis库中的时效性数据,采用随机抽取的策略(8种),利用过期数据占比的方式控制删除频度

策略

1)检测易失数据(可能会过期的数据集server.db[i].expires ) ① volatile-lru:挑选最近最少使用的数据淘汰 ② volatile-lfu:挑选最近使用次数最少的数据淘汰 ③ volatile-ttl:挑选将要过期的数据淘汰 ④ volatile-random:任意选择数据淘汰

2)检测全库数据(所有数据集server.db[i].dict ) ⑤ allkeys-lru:挑选最近最少使用的数据淘汰 ⑥ allkeys-lfu:挑选最近使用次数最少的数据淘汰 ⑦ allkeys-random:任意选择数据淘汰

3)放弃数据驱逐 ⑧ no-enviction(驱逐):禁止驱逐数据(redis4.0中默认策略),会引发错误OOM(Out Of Memory)

缓存问题

1 缓存穿透

缓存和数据库都没有的数据,被大量请求,由于数据不存在,缓存就也不会存在该数据,所有的请求都会直接穿透到数据库。

如果被恶意用户利用,疯狂请求不存在的数据,就会导致数据库压力过大,甚至垮掉。

注意:穿透的意思是,都没有,直接一路打到数据库。

解决

1、把无效的Key存进Redis中。如果Redis查不到数据,数据库也查不到,我们把这个Key值保存进Redis,设置value="null",当下次再通过这个Key查询时就不需要再查询数据库。这种处理方式肯定是有问题的,假如传进来的这个不存在的Key值每次都是随机的,那存进Redis也没有意义。 2、使用布隆过滤器。布隆过滤器的作用是某个 key 不存在,那么就一定不存在,它说某个 key 存在,那么很大可能是存在(存在一定的误判率)。于是我们可以在缓存之前再加一层布隆过滤器,在查询的时候先去布隆过滤器查询 key 是否存在,如果不存在就直接返回。

2 缓存击穿

数据库原本有得数据,但是缓存中没有,其实跟缓存雪崩有点类似,缓存雪崩是大规模的key失效,而缓存击穿是一个热点的Key,有大并发集中对其进行访问,突然间这个Key失效了,导致大并发全部打在数据库上,导致数据库压力剧增。这种现象就叫做缓存击穿。

解决

1、如果业务允许的话,对于热点的key可以设置永不过期的key。 2、使用互斥锁。如果缓存失效的情况,只有拿到锁才可以查询数据库,降低了在同一时刻打在数据库上的请求,防止数据库打死。当然这样会导致系统的性能变差。

3 缓存雪崩

缓存雪崩是指缓存中有大量的数据,在同一个时间点,或者较短的时间段内,全部过期了,这个时候请求过来,缓存没有数据,都会请求数据库,则数据库的压力就会突增,扛不住就会宕机。

解决

1、在原有的失效时间上加上一个随机值,比如1-5分钟随机。这样就避免了因为采用相同的过期时间导致的缓存雪崩。

如果真的发生了缓存雪崩,有没有什么兜底的措施?

2、使用熔断机制。当流量到达一定的阈值时,就直接返回“系统拥挤”之类的提示,防止过多的请求打在数据库上。至少能保证一部分用户是可以正常使用,其他用户多刷新几次也能得到结果。 3、提高数据库的容灾能力,可以使用分库分表,读写分离的策略。 4、为了防止Redis宕机导致缓存雪崩的问题,可以搭建Redis集群,提高Redis的容灾性。

事务和锁机制

  • Redis的事务是一个单独的隔离操作:事务中所有的命令都会序列化、按顺序执行,执行过程中不会被其他客户端发来的请求所打断

  • Redis事务的主要作用就是串联多个命令防止别的命令插队

  • Redis事务中有三个命令:Multi,Exec,discard

  • Multi命令用于组队,将命令都送入队列,但不会执行

  • Exec命令用于执行已送入的命令

  • discard用于在组队过程中放弃组队

    • 组队的时候报错会直接放弃组队
    • 执行的时候报错只会影响报错命令
  • Redis不保证原子性!一条命令失败,别的照常执行,不会回滚

事务冲突

  • 结合淘宝购物机制,解决方案就是上锁
  • 锁有两种
    • 悲观锁
    • 乐观锁

悲观锁

就是每次操作之前先上锁,别人都无法操作,解锁后才可以 大锁库微服务不能用悲观锁,用乐观锁 大锁库微服务不能用悲观锁,秒杀活动先把库存锁好了爱怎么做怎么做,消息队列或者redis都行

乐观锁

  • 乐观锁是在数据上加一个版本号,操作以后就更新这个版本号(v1.0->v1.1)
  • 谁都可以得到这个版本的数据,谁先操作完就会更新这个版本号,每次操作都会校验版本是否是最新的
  • 如果版本号不一致,则需重新获取版本号进行数据操作
  • 乐观锁适用于多读的应用类型,这样可以提高吞吐量
    • 比如1000个人去抢1张票,但支付的时候只有一个人
  • 演示如下
    • 交替执行
    • 最后一个exec输出为nil

分布式锁

  • 下面案例中的一人一单在分布式的情况下就会失效,所以需要分布式锁

  • 分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁

  • 实现

    • MySQL
    • Redis
    • Zookeeper

image.png

  • 基于Redis
    • 结合一人一单问题看

分布式锁的原理

原理:使用lua脚本和watch dog机制进行分布式锁的使用。

可重入锁:

如果是想设计一个可重入的锁,应该使用hash解决这个问题

key1是锁的名称,key2就是锁持有线程的uuid,value就是重入次数。由于hash不能使用setnx命令设置过期时间,我们需要使用额外的命令pexpire进行设置过期时间,这里可以使用lua脚本保证操作的原子性。

watch dog机制: watch dog子线程会自动为分布式锁进行续期,如果持有锁的客户端发生故障(如崩溃或网络中断),Watchdog机制应能够识别并处理这种情况。

RedLock红锁

主从架构下,线程1申请了主节点的lock,但是随后线程1就挂了,但是从节点没有跟主节点交换数据,并不知道线程1申请了锁,此时从节点被选举为主节点,然后线程2申请了新主节点的lock,此时就会冲突(本质上redis主从复制(增量复制)是异步的。),RedLock就是解决这个的方法

原理:线程获取锁需要半数及以上的节点的设置才算成功。

多线程下争抢:没有获得锁的线程将会使用lua脚本进行还原。

主从下争抢:

一般来说,可以使用zookeeper进行分布式锁的方案。是一个比较好的ap方案。

CAP定理

CAP定理中的三个特性分别是:

  • 一致性(Consistency):所有节点在同一时间看到相同的数据。换句话说,系统在进行读操作时总是返回最新的数据。
  • 可用性(Availability):系统在有限时间内可以响应每个请求(不一定是最新的数据)。即每个请求都能得到一个非错误响应。
  • 分区容忍性(Partition Tolerance):系统能够继续操作,即使出现任意数量的网络消息丢失或延迟(即网络分区)。

CP系统在网络分区的情况下选择一致性。

  • HBase:一个分布式数据库系统,在网络分区的情况下会拒绝写操作以确保一致性。
  • Zookeeper:分布式协调服务,优先保证一致性,在网络分区时可能无法提供服务。

AP系统在网络分区的情况下选择可用性。

  • Cassandra:一个分布式数据库系统,在网络分区的情况下优先保证可用性,允许临时的不一致。
  • DynamoDB:一个NoSQL数据库系统,设计时优先考虑高可用性和分区容忍性。

Redisson

  • 使用setnx有些许问题
    • 同一线程对于同一把锁,不可重入
    • 不可重试(获取锁只获取一次就返回false)
    • 超时释放(如果业务没执行完毕?)
    • Redis主从一致满足不了
  • Redisson是一个在Redis的基础上实现的Java驻内存数据网格。不仅提供一系列分布式的Java常用对象,还提供需要分布式服务(比如分布式锁)
  • 使用
    • 导依赖
    • 写配置
    • 使用
  • 详情:TODO
  • pom
xml
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>
  • java配置文件
java
@Configuration
public class RedisConfig {

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.212.166:6379").setPassword("123456");

        return Redisson.create(config);
    }
}
  • 测试案例
java
ong userId = UserHolder.getUser().getId();
// SimpleRedisLock lock = new SimpleRedisLock("order:"+userId, stringRedisTemplate);  // 自定义实现
RLock lock = this.redissonClient.getLock("lock:order:" + userId);
boolean isLock = lock.tryLock();

lock.unlock();

消息队列

  • Redis是一个缓存中间件,但是也可以变相的当消息队列使用
    • 使用List模拟
    • PubSub(发布订阅)实现
    • Stream模式
  • PubSub
    • 订阅频道 - SUBSCRIBE channelName
    • 发布消息 - PUBLISH channelName message
    • 缺点:
      • 不支持数据持久化
      • 无法避免消息丢失
      • 消息堆积有上限,超出时数据丢失
  • Stream - 5.0以后引入的全新的数据类型
    • TODO

Redis消息队列,List,发布订阅

双写一致性

主从和redis双写一致性都可以用这个。

延时双删,这里引入了Canal作为一个发送删除的一个信息监听者。监听mysql。

Redis使用:

Redis使用

类型作用
String分布式锁、分布式Session、值缓存、浏览数、分库分表主键序列号
List分布式Deque、消息队列、Push式信息流
Hash购物车、对象存储
Set点赞、抽奖、集合运算
Sorted Set热搜、最近播放
stream(5.0+)用作消息队列

Redis限流:

令牌桶

存在一个固定大小的桶来放令牌(token),系统会以一个固定时间放入k个令牌,如果没有令牌,系统就拒绝服务。

使用hash进行实现:

key1是逻辑名称 key2是token令牌数量,value是补充的最后时间戳。

可以使用lua进行计算(推荐)。或者单独开一个线程进行计算

露桶

固定大小的桶,请求会被先丢到桶中,固定速率从桶中取出并处理请求,如果请求超过桶的大小将会被丢弃。

可以使用消息队列进行设计。

区别

令牌桶适用于需要精确控制请求频率的场景

而漏桶算法适用于需要平滑处理突发请求并限制请求处理速率的场景

计数器

把时间分为多个时间窗口,统计每个时间窗口内的请求数量,当请求超过设定的阈值,进行限流。

计数器不太好,因为不好确定哪个请求完成时间,所以引出了滑动窗口。

滑动窗口

实现:

可以使用Redis的Zset来实现,当前时间作为分数,每次取当前时间至窗口的大小进行判断判断即可