1 Redis单线程 VS 多线程(入门篇)
1.1 面试题
redis到底是单线程还是多线程?
IO多路复用听说过吗?
redis为什么快?
1.2 Redis为什么选择单线程?
1.2.1 是什么
这种问法其实并不严谨,为啥这么说呢?
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Redis的版本很多3.x、4.x、6.x,版本不同架构也是不同的,不限定版本问是否单线程也不太严谨。 1 版本3.x ,最早版本,也就是大家口口相传的redis是单线程,阳哥2016年讲解的redis就是3.X的版本。 2 版本4.x,严格意义来说也不是单线程,而是负责处理客户端请求的线程是单线程,但是 开始加了点多线程的东西(异步删除)。---貌似 3 2020年5月版本的6.0.x后及2022年出的7.0版本后, 告别了大家印象中的单线程,用一种全新的多线程来解决问题。---实锤 |
有几个里程碑式的重要版本
|-----------------------------------------------------------------------------------------------------------------------|
| 5.0版本是直接升级到6.0版本,对于这个激进的升级,Redis之父antirez表现得很有信心和兴奋, 所以第一时间发文来阐述6.0的一些重大功能"Redis 6.0.0 GA is out!" 当然,Redis7.0后版本更加厉害 |
1.2.2 why
1.2.2.1 厘清⼀个事实我们通常说,Redis是单线程究竟何意?
Redis是单线程
主要是指Redis的网络IO和键值对读写是由一个线程来完成的,Redis在处理客户端的请求时包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的"单线程"。这也是Redis对外提供键值存储服务的主要流程。
但Redis的其他功能,比如持久化RDB、AOF、异步删除、集群数据同步等等,其实是由额外的线程执行的。
Redis命令工作线程是单线程的,但是,整个Redis来说,是多线程的;
1.2.2.2 请说说演进变化情况?
Redis3.x单线程时代但性能依旧很快的主要原因
a)基于内存操作:Redis 的所有数据都存在内存中,因此所有的运算都是内存级别的,所以他的性能比较高;
b)数据结构简单:Redis 的数据结构是专门设计的,而这些简单的数据结构的查找和操作的时间大部分复杂度都是 O(1),因此性能比较高;
c)多路复用和非阻塞 I/O:Redis使用 I/O多路复用功能来监听多个 socket连接客户端,这样就可以使用一个线程连接来处理多个请求,减少线程切换带来的开销,同时也避免了 I/O 阻塞操作
d)避免上下文切换:因为是单线程模型,因此就避免了不必要的上下文切换和多线程竞争,这就省去了多线程切换带来的时间和性能上的消耗,而且单线程不会导致死锁问题的发生
作者原话,官网证据
|--------------------------------------------------------------------------------------------------------------------------------------|
| 他的大体意思是说 Redis 是基于内存操作的, 因此他的瓶颈可能是机器的内存或者网络带宽而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了,况且使用多线程比较麻烦。 但是在 Redis 4.0 中开始支持多线程了,例如后台删除、备份等功能。 |
Redis 4.0之前一直采用单线程的主要原因有以下三个
简单来说,Redis4.0之前一直采用单线程的主要原因有以下三个:
1 使用单线程模型是 Redis 的开发和维护更简单,因为单线程模型方便开发和调试;
2 即使使用单线程模型也并发的处理多客户端的请求,主要使用的是IO多路复用和非阻塞IO;
3 对于Redis系统来说,主要的性能瓶颈是内存或者网络带宽而并非 CPU。
1.3 既然单线程这么好,为什么逐渐又加入了多线程特性?
1.3.1 单线程也有单线程的苦恼
举个例子:
正常情况下使用 del 指令可以很快的删除数据,而当被删除的 key 是一个非常大的对象时,例如删除包含了成千上万个元素的 hash 集合时,那么 del 指令就会造成 Redis 主线程卡顿。
这就是redis3.x单线程时代最经典的故障,大key删除的头疼问题,
由于redis是单线程的,del bigKey .....
等待很久这个线程才会释放,类似加了一个synchronized锁,你可以想象高并发下,程序堵成什么样子?
1.3.2 如何解决
使用惰性删除可以有效的避免 Redis 卡顿的问题
案例:
比如当我(Redis)需要删除一个很大的数据时,因为是单线程原子命令操作,这就会导致 Redis 服务卡顿,
于是在 Redis 4.0 中就新增了多线程的模块,当然此版本中的多线程主要是为了解决删除数据效率比较低的问题的。
|-----------------------------|
| unlink key |
| flushdb async |
| flushall async |
| 把删除工作交给了后台的小弟(子线程)异步来删除数据了。 |
因为Redis是单个主线程处理,redis之父antirez一直强调"Lazy Redis is better Redis".
而lazy free的本质就是把某些cost(主要时间复制度,占用主线程cpu时间片)较高删除操作,
从redis主线程剥离让bio子线程来处理,极大地减少主线阻塞时间。从而减少删除导致性能和稳定性问题。
在Redis 4.0就引入了多个线程来实现数据的异步惰性删除等功能,但是其处理读写请求的仍然只有一个线程,所以仍然算是狭义上的单线程。
1.4 redis6/7的多线程特性和IO多路复用入门篇
1.4.1 对于Redis主要的性能瓶颈是内存或者网络带宽而并非 CPU
1.4.2 最后Redis的瓶颈可以初步定为:网络IO
1.4.2.1 redis6/7,真正多线程登场
在Redis6/7中,非常受关注的第一个新特性就是多线程。
这是因为,Redis一直被大家熟知的就是它的单线程架构,虽然有些命令操作可以用后台线程或子进程执行(比如数据删除、快照生成、AOF重写)。但是,从网络IO处理到实际的读写命令处理,都是由单个线程完成的。
随着网络硬件的性能提升,Redis的性能瓶颈有时会出现在网络IO的处理上,也就是说,单个主线程处理网络请求的速度跟不上底层网络硬件的速度,
为了应对这个问题:
采用多个IO线程来处理网络请求,提高网络请求处理的并行度,Redis6/7就是采用的这种方法。
但是,Redis的多IO线程只是用来处理网络请求的,对于读写操作命令Redis仍然使用单线程来处理。这是因为,Redis处理请求时,网络处理经常是瓶颈,通过多个IO线程并行处理网络操作,可以提升实例的整体处理性能。而继续使用单线程执行命令操作,就不用为了保证Lua脚本、事务的原子性,额外开发多线程互斥加锁机制了(不管加锁操作处理),这样一来,Redis线程模型实现就简单了
1.4.2.2 主线程和IO线程是怎么协作完成请求处理的-精讲版
四个阶段:
1.4.3 Unix网络编程中的五种IO模型
Blocking IO - 阻塞IO
NoneBlocking IO - 非阻塞IO
signal driven IO - 信号驱动IO(偏C)
asynchronous IO - 异步IO(偏C)
IO multiplexing - IO多路复用 ★
1.4.3.1 Linux世界一切皆文件
文件描述符、简称FD,句柄
FileDescriptor:
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
1.4.3.2 首次浅谈IO多路复用,IO多路复用是什么
I/O :网络 I/O,尤其在操作系统层面指数据在内核态和用户态之间的读写操作
多路:多个客户端连接(连接就是套接字描述符,即 socket 或者 channel)
复用:复用一个或几个线程。
IO多路复用:也就是说一个或一组线程处理多个TCP连接,使用单进程就能够实现同时处理多个客户端的连接,无需创建或者维护过多的进程/线程
一句话:一个服务端进程可以同时处理多个套接字描述符。实现IO多路复用的模型有3种:可以分select->poll->epoll三个阶段来描述。
1.4.3.3 场景体验,说人话引出epoll
场景解析
模拟一个tcp服务器处理30个客户socket。
假设你是一个监考老师,让30个学生解答一道竞赛考题,然后负责验收学生答卷,你有下面几个选择:
第一种选择(轮询):按顺序逐个验收,先验收A,然后是B,之后是C、D。。。这中间如果有一个学生卡住,全班都会被耽误,你用循环挨个处理socket,根本不具有并发能力。
第二种选择(来一个new一个,1对1服务):你创建30个分身线程,每个分身线程检查一个学生的答案是否正确。 这种类似于为每一个用户创建一个进程或者线程处理连接。
第三种选择(响应式处理,1对多服务),你站在讲台上等,谁解答完谁举手。这时C、D举手,表示他们解答问题完毕,你下去依次检查C、D的答案,然后继续回到讲台上等。此时E、A又举手,然后去处理E和A。。。这种就是IO复用模型。Linux下的select、poll和epoll就是干这个的。
IO多路复用模型,简单明了版理解
将用户socket对应的文件描述符(FileDescriptor)注册进epoll,然后epoll帮你监听哪些socket上有消息到达,这样就避免了大量的无用操作。此时的socket应该采用非阻塞模式。这样,整个过程只在调用select、poll、epoll这些调用的时候才会阻塞,收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,这就是事件驱动,所谓的reactor反应模式。
在单个线程通过记录跟踪每一个Sockek(I/O流)的状态来同时管理多个I/O流. 一个服务端进程可以同时处理多个套接字描述符。
目的是尽量多的提高服务器的吞吐能力。
大家都用过nginx,nginx使用epoll接收请求,ngnix会有很多链接进来, epoll会把他们都监视起来,然后像拨开关一样,谁有数据就拨向谁,然后调用相应的代码处理。redis类似同理,这就是IO多路复用原理,有请求就响应,没请求不打扰。
1.4.3.4 小总结
只使用一个服务端进程可以同时处理多个套接字描述符连接
1.4.3.5 面试题:redis为什么这么快
备注:
IO多路复用+epoll函数使用,才是redis为什么这么快的直接原因,而不是仅仅单线程命令+redis安装在内存中。
1.4.4 简单说明
Redis工作线程是单线程的,但是,整个Redis来说,是多线程的;
主线程和IO线程是怎么协作完成请求处理的-精简版
|------------------------------------------------------------------------------------------------------------------------------|
| I/O 的读和写本身是堵塞的,比如当 socket 中有数据时,Redis 会通过调用先将数据从内核态空间拷贝到用户态空间,再交给 Redis 调用,而这个拷贝的过程就是阻塞的,当数据量越大时拷贝所需要的时间就越多,而这些操作都是基于单线程完成的。 |
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 从Redis6开始,就新增了多线程的功能来提高 I/O 的读写性能,他的主要实现思路是将 主线程的 IO 读写任务拆分给一组独立的线程去执行,这样就可以使多个 socket 的读写可以并行化了, 采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗), 将最耗时的Socket的读取、请求解析、写入单独外包出去,剩下的命令执行仍然由主线程串行执行并和内存的数据交互。 |
|-------------------------------------------------|
| 结合上图可知,网络IO操作就变成多线程化了,其他核心部分仍然是线程安全的,是个不错的折中办法。 |
结论
|---------------------------------------------------------------------------------|
| Redis6→7将网络数据读写、请求协议解析通过多个IO线程的来处理 , 对于真正的命令执行来说,仍然使用主线程操作,一举两得,便宜占尽!!! o( ̄▽ ̄)d |
1.5 Redis7默认是否开启了多线程?
如果你在实际应用中,发现Redis实例的CPU开销不大但吞吐量却没有提升,可以考虑使用Redis7的多线程机制,加速网络处理,进而提升实例的吞吐量
|--------------------------------------------------------------------------------------------------------------|
| Redis7将所有数据放在内存中,内存的响应时长大约为100纳秒,对于小数据包,Redis服务器可以处理8W到10W的QPS, 这也是Redis处理的极限了, 对于80%的公司来说,单线程的Redis已经足够使用了。 |
在Redis6.0及7后,多线程机制默认是关闭的,如果需要使用多线程功能,需要在redis.conf中完成两个设置
1、设置io-thread-do-reads配置项为yes,表示启动多线程。
2、设置线程个数。关于线程数的设置,官方的建议是如果为 4 核的 CPU,建议线程数设置为 2 或 3,如果为 8 核 CPU 建议线程数设置为 6,线程数一定要小于机器核数,线程数并不是越大越好。
1.6 我还是曾经那个少年
Redis自身出道就是优秀,基于内存操作、数据结构简单、多路复用和非阻塞 I/O、避免了不必要的线程上下文切换等特性,在单线程的环境下依然很快;
但对于大数据的 key 删除还是卡顿厉害,因此在 Redis 4.0 引入了多线程unlink key/flushall async 等命令,主要用于 Redis 数据的异步删除;
而在 Redis6/7中引入了 I/O 多线程的读写,这样就可以更加高效的处理更多的任务了,Redis 只是将 I/O 读写变成了多线程,而命令的执行依旧是由主线程串行执行的,因此在多线程下操作 Redis 不会出现线程安全的问题。
Redis 无论是当初的单线程设计,还是如今与当初设计相背的多线程,目的只有一个:让 Redis 变得越来越快。
所以 Redis 依旧没变,他还是那个曾经的少年,O(∩_∩)O哈哈~
2 BigKey ★★★★★
2.1 面试题
阿里广告平台,海量数据里查询某一固定前缀的key
小红书,你如何生产上限制keys */flushdb/flushall等危险命令以防止误删误用?
美团,MEMORY USAGE 命令你用过吗?
BigKey问题,多大算big?你如何发现?如何删除?如何处理?
BigKey你做过调优吗?惰性释放lazyfree了解过吗?
Morekey问题,生产上redis数据库有1000W记录,你如何遍历?key *可以吗?
2.2 MoreKey案例
2.2.1 大批量往redis里面插入2000W测试数据key
2.2.1.1 Linux Bash下面执行,插入100W
生成100W条redis批量设置kv的语句(key=kn,value=vn)写入到/tmp目录下的redisTest.txt文件中
for((i=1;i<=100*10000;i++)); do echo "set ki vi" >> /tmp/redisTest.txt ;done;
2.2.1.2 通过redis提供的管道--pipe命令插入100W大批量数据
结合自己机器的地址:
cat /tmp/redisTest.txt | /opt/redis-7.0.0/src/redis-cli -h 127.0.0.1 -p 6379 -a 111111 --pipe
多出来的5条,是之前阳哥自己的其它测试数据 ,参考阳哥机器硬件,100w数据插入redis花费5.8秒左右
2.2.2 某快递巨头真实生产案例新闻
2.2.2.1 新闻
2.2.2.2 keys * 你试试100W花费多少秒遍历查询
key * 这个指令有致命的弊端,在实际环境中最好不要使用
2.2.2.3 生产上限制keys */flushdb/flushall等危险命令以防止误删误用?
通过配置设置禁用这些命令,redis.conf在SECURITY这一项中
2.2.3 不用keys *避免卡顿,那该用什么
2.2.3.1 scan命令登场
SCAN | Docs 英文
一句话,类似mysql limit的但不完全相同
2.2.3.2 Scan 命令用于迭代数据库中的数据库键
语法
特点
SCAN 命令是一个基于游标的迭代器,每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。
SCAN 返回一个包含两个元素的数组,
第一个元素是用于进行下一次迭代的新游标,
第二个元素则是一个数组, 这个数组中包含了所有被迭代的元素。如果新游标返回零表示迭代已结束。
SCAN的遍历顺序
非常特别,它不是从第一维数组的第零位一直遍历到末尾,而是采用了高位进位加法来遍历。之所以使用这样特殊的方式进行遍历,是考虑到字典的扩容和缩容时避免槽位的遍历重复和遗漏。
使用
2.3 BigKey案例
2.3.1 多大算Big
2.3.1.1 参考《阿里云Redis开发规范》
2.3.1.2 string和二级结构
string是value,最大512MB但是≥10KB就是bigkey
list、hash、set和zset,个数超过5000就是bigkey
疑问???
2.3.2 哪些危害
内存不均,集群迁移困难
超时删除,大key删除作梗
网络流量阻塞
2.3.3 如何产生
2.3.3.1 社交类
王心凌粉丝列表,典型案例粉丝逐步递增
2.3.3.2 汇总统计
某个报表,月日年经年累月的积累
2.3.4 如何发现
2.3.4.1 redis-cli --bigkeys
好处,见最下面总结
给出每种数据结构Top 1 bigkey,同时给出每种数据类型的键值个数+平均大小
不足
想查询大于10kb的所有key,--bigkeys参数就无能为力了,需要用到memory usage来计算每个键值的字节数
redis-cli --bigkeys -a 111111
|---------------------------------------------------------------------------------------------------|
| redis-cli -h 127.0.0.1 -p 6379 -a 111111 --bigkeys |
| 每隔 100 条 scan 指令就会休眠 0.1s,ops 就不会剧烈抬升,但是扫描的时间会变长 redis-cli -h 127.0.0.1 -p 7001 ---bigkeys -i 0.1 |
2.3.4.2 MEMORY USAGE 键
计算每个键值的字节数
官网
Redis MEMORY USAGE 命令 估计key的内存使用情况
2.3.5 如何删除
2.3.5.1 参考《阿里云Redis开发规范》
2.3.5.2 官网
2.3.5.3 普通命令
String
一般用del,如果过于庞大unlink
hash
使用hscan每次获取少量field-value,再使用hdel删除每个field
命令
阿里手册
list
使用ltrim渐进式逐步删除,直到全部删除完成
命令
阿里手册
set
使用sscan每次获取部分元素,再使用srem命令删除每个元素
命令
阿里手册
zset
使用zscan每次获取部分元素,再使用ZREMRANGEBYRANK命令删除每个元素
命令
阿里手册
2.4 BigKey生产调优
redis.conf配置文件LAZY FREEING相关说明
2.4.1 阻塞和非阻塞删除命令
2.4.2 优化配置
3 缓存双写一致性之更新策略探讨
3.1 反馈回来的面试题
一图
a)问题,上面业务逻辑你用java代码如何写?
b)你只要用缓存,就可能会涉及到redis缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?
c)双写一致性,你先动缓存redis还是数据库mysql哪一个?why?
d)延时双删你做过吗?会有哪些问题?
e)有这么一种情况,微服务查询redis无mysql有,为保证数据双写一致性回写redis你需要注意什么?双检加锁策略你了解过吗?如何尽量避免缓存击穿?
f)redis和mysql双写100%会出纰漏,做不到强一致性,你如何保证最终一致性?
3.2 缓存双写一致性,谈谈你的理解
如果redis中有数据:需要和数据库中的值相同
如果redis中无数据:数据库中的值要是最新值,且准备回写redis
缓存按照操作来分,细分2种:只读缓存(只作查询,不需要回写);读写缓存(大部分需要回写机制)
3.2.1 读写缓存
同步直写策略:
写数据库后也同步写redis缓存,缓存和数据库中的数据⼀致;
对于读写缓存来说,要想保证缓存和数据库中的数据⼀致,就要采⽤同步直写策略
异步缓写策略:
a)正常业务运行中,mysql数据变动了,但是可以在业务上容许出现一定时间后才作用于redis,比如仓库、物流系统
b)异常情况出现了,不得不将失败的动作重新修补,有可能需要借助kafka或者RabbitMQ等消息中间件,实现重试重写
3.2.2 一图代码如何写
问题,上面业务逻辑你用java代码如何写?
采用双检加锁策略
多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它。
其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。
后面的线程进来发现已经有缓存了,就直接走缓存。
java
package com.atguigu.redis.service;
import com.atguigu.redis.entities.User;
import com.atguigu.redis.mapper.UserMapper;
import io.swagger.models.auth.In;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2021-05-01 14:58
*/
@Service
@Slf4j
public class UserService {
public static final String CACHE_KEY_USER = "user:";
@Resource
private UserMapper userMapper;
@Resource
private RedisTemplate redisTemplate;
/**
* 业务逻辑没有写错,对于小厂中厂(QPS《=1000)可以使用,但是大厂不行
* @param id
* @return
*/
public User findUserById(Integer id)
{
User user = null;
String key = CACHE_KEY_USER+id;
//1 先从redis里面查询,如果有直接返回结果,如果没有再去查询mysql
user = (User) redisTemplate.opsForValue().get(key);
if(user == null)
{
//2 redis里面无,继续查询mysql
user = userMapper.selectByPrimaryKey(id);
if(user == null)
{
//3.1 redis+mysql 都无数据
//你具体细化,防止多次穿透,我们业务规定,记录下导致穿透的这个key回写redis
return user;
}else{
//3.2 mysql有,需要将数据写回redis,保证下一次的缓存命中率
redisTemplate.opsForValue().set(key,user);
}
}
return user;
}
/**
* 加强补充,避免突然key失效了,打爆mysql,做一下预防,尽量不出现击穿的情况。
* @param id
* @return
*/
public User findUserById2(Integer id)
{
User user = null;
String key = CACHE_KEY_USER+id;
//1 先从redis里面查询,如果有直接返回结果,如果没有再去查询mysql,
// 第1次查询redis,加锁前
user = (User) redisTemplate.opsForValue().get(key);
if(user == null) {
//2 大厂用,对于高QPS的优化,进来就先加锁,保证一个请求操作,让外面的redis等待一下,避免击穿mysql
synchronized (UserService.class){
//第2次查询redis,加锁后
user = (User) redisTemplate.opsForValue().get(key);
//3 二次查redis还是null,可以去查mysql了(mysql默认有数据)
if (user == null) {
//4 查询mysql拿数据(mysql默认有数据)
user = userMapper.selectByPrimaryKey(id);
if (user == null) {
return null;
}else{
//5 mysql里面有数据的,需要回写redis,完成数据一致性的同步工作
redisTemplate.opsForValue().setIfAbsent(key,user,7L,TimeUnit.DAYS);
}
}
}
}
return user;
}
}
3.3 数据库和缓存一致性的几种更新策略
3.3.1 目的
总之,我们要达到最终一致性
给缓存设置过期时间,定期清理缓存并回写,是保证最终一致性的解决方案。
我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存,达到一致性,切记,要以mysql的数据库写入库为准。
上述方案和后续落地案例是调研后的主流+成熟的做法,但是考虑到各个公司业务系统的差距,
不是100%绝对正确,不保证绝对适配全部情况,请同学们自行酌情选择打法,合适自己的最好。
3.3.2 可以停机的情况
挂牌报错,凌晨升级,温馨提示,服务降级
单线程,这样重量级的数据操作最好不要多线程
3.3.3 讨论4种更新策略
3.3.3.1 先更新数据库,再更新缓存
异常问题1
1 先更新mysql的某商品的库存,当前商品的库存是100,更新为99个。
2 先更新mysql修改为99成功,然后更新redis。
3 此时假设异常出现,更新redis失败了,这导致mysql里面的库存是99而redis里面的还是100 。
4 上述发生,会让数据库里面和缓存redis里面数据不一致,读到redis脏数据
异常问题2
【先更新数据库,再更新缓存】,A、B两个线程发起调用
【正常逻辑】
1 A update mysql 100
2 A update redis 100
3 B update mysql 80
4 B update redis 80
=============================
【异常逻辑】多线程环境下,A、B两个线程有快有慢,有前有后有并行
1 A update mysql 100
3 B update mysql 80
4 B update redis 80
2 A update redis 100
=============================
最终结果,mysql和redis数据不一致,o(╥﹏╥)o,
mysql80,redis100
3.3.3.2 先更新缓存,再更新数据库
不太推荐:业务上一般把mysql作为底单数据库,保证最后解释
异常问题2
【先更新缓存,再更新数据库】,A、B两个线程发起调用
【正常逻辑】
1 A update redis 100
2 A update mysql 100
3 B update redis 80
4 B update mysql 80
====================================
【异常逻辑】多线程环境下,A、B两个线程有快有慢有并行
A update redis 100
B update redis 80
B update mysql 80
A update mysql 100
----mysql100,redis80
3.3.3.3 先删除缓存,再更新数据库
3.3.3.3.1 异常问题
步骤分析1,先删除缓存,再更新数据库
阳哥自己这里写20秒,是自己故意乱写的,表示更新数据库可能失败,实际中不可能...O(∩_∩)O哈哈~
1 A线程先成功删除了redis里面的数据,然后去更新mysql,此时mysql正在更新中,还没有结束。(比如网络延时)
B突然出现要来读取缓存数据。
步骤分析2,先删除缓存,再更新数据库
2 此时redis里面的数据是空的,B线程来读取,先去读redis里数据(已经被A线程delete掉了),此处出来2个问题:
2.1 B从mysql获得了旧值
B线程发现redis里没有(缓存缺失)马上去mysql里面读取,从数据库里面读取来的是旧值。
2.2 B会把获得的旧值写回redis
获得旧值数据后返回前台并回写进redis(刚被A线程删除的旧数据有极大可能又被写回了)。
步骤分析3,先删除缓存,再更新数据库
3
A线程更新完mysql,发现redis里面的缓存是脏数据,A线程直接懵逼了,o(╥﹏╥)o
两个并发操作,一个是更新操作,另一个是查询操作,
A删除缓存后,B查询操作没有命中缓存,B先把老数据读出来后放到缓存中,然后A更新操作更新了数据库。
于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。
上面3步骤串讲梳理
4 总结流程:
(1)请求A进行写操作,删除redis缓存后,工作正在进行中,更新mysql......A还么有彻底更新完mysql,还没commit
(2)请求B开工查询,查询redis发现缓存不存在(被A从redis中删除了)
(3)请求B继续,去数据库查询得到了mysql中的旧值(A还没有更新完)
(4)请求B将旧值写回redis缓存
(5)请求A将新值写入mysql数据库
上述情况就会导致不一致的情形出现。
|----|-------------------------------------|--------------------------------------------------------------------|--------------------------------------------------------------------|
| 时间 | 线程A | 线程B | 出现的问题 |
| t1 | 请求A进行写操作,删除缓存成功后,工作正在mysql进行中...... | | |
| t2 | | 1 缓存中读取不到,立刻读mysql,由于A还没有对mysql更新完,读到的是旧值 2 还把从mysql读取的旧值,写回了redis | 1 A还没有更新完mysql,导致B读到了旧值 2 线程B遵守回写机制,把旧值写回redis,导致其它请求读取的还是旧值,A白干了。 |
| t3 | A更新完mysql数据库的值,over | | redis是被B写回的旧值, mysql是被A更新的新值。 出现了,数据不一致问题。 |
总结一下:
|--------------|--------------------------------------------------------------------------------------------------|
| 先删除缓存,再更新数据库 | 如果数据库更新失败或超时或返回不及时,导致B线程请求访问缓存时发现redis里面没数据,缓存缺失,B再去读取mysql时, 从数据库中读取到旧值,还写回redis,导致A白干了,o(╥﹏╥)o |
3.3.3.3.2 解决方案
采用延时双删策略
双删方案面试题
这个删除该休眠多久呢
线程A sleep的时间,就需要大于线程B读取数据再写入缓存的时间。
这个时间怎么确定呢?
第一种方法:
在业务程序运行的时候,统计下线程读数据和写缓存的操作时间,自行评估自己的项目的读数据业务逻辑的耗时,
以此为基础来进行估算。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上加百毫秒即可。
这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
第二种方法:
新启动一个后台监控程序,比如后面要讲解的WatchDog监控程序,会加时
这种同步淘汰策略,吞吐量降低怎么办?
后续看门狗WatchDog源码分析
3.3.3.4 先更新数据库,再删除缓存
3.3.3.4.1 异常问题
先更新数据库,再删除缓存
|----|----------------|----------------------|----------------------------|
| 时间 | 线程A | 线程B | 出现的问题 |
| t1 | 更新数据库中的值...... | | |
| t2 | | 缓存中立刻命中,此时B读取的是缓存旧值。 | A还没有来得及删除缓存的值,导致B缓存命中读到旧值。 |
| t3 | 更新缓存的数据,over | | |
|--------------|----------------------------------------------|
| 先更新数据库,再删除缓存 | 假如缓存删除失败或者来不及,导致请求再次访问redis时缓存命中, 读取到的是缓存旧值。 |
3.3.3.4.2 业务指导思想
微软云:Cache-Aside pattern - Azure Architecture Center | Microsoft Learn
我们后面的阿里巴巴canal也是类似的思想:上述的订阅binlog程序在mysql中有现成的中间件叫canal,可以完成订阅binlog日志的功能。
3.3.3.4.3 解决方案
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用Kafka/RabbitMQ等)。 2 当程序没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。 3 如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复操作,此时,我们也可以保证数据库和缓存的数据一致了,否则还需要再次进行重试 4 如果重试超过的一定次数后还是没有成功,我们就需要向业务层发送报错信息了,通知运维人员。 |
3.3.3.4.4 类似经典的分布式事务问题,只有一个权威答案
最终一致性:
流量充值,先下发短信实际充值可能滞后5分钟,可以接受
电商发货,短信下发但是物流明天见
3.3.4 小总结
3.3.4.1 如何选择方案?利弊如何
在大多数业务场景下,
阳哥个人建议是(仅代表我个人,不权威),优先使用先更新数据库,再删除缓存的方案(先更库→后删存)。理由如下:
1 先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力导致打满mysql。
2 如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置。
多补充一句:如果使用先更新数据库,再删除缓存的方案
|----------------------------------------------------------------------------------------------------------------------------------------------------|
| 如果业务层要求必须读取一致性的数据,那么我们就需要在更新数据库时,先在Redis缓存客户端暂停并发读请求,等数据库更新完、缓存值删除后,再读取数据,从而保证数据一致性,这是理论可以达到的效果,但 实际,不推荐,因为真实生产环境中,分布式下很难做到实时一致性, 一般都是最终一致性,请大家参考。 |
3.3.4.2 一图总结
|-------------------------|-----------|----------------------------|-----------------------------------------|-------------------------------|
| 策略 | 高并发多线程条件下 | 问题 | 现象 | 解决方案 |
| 先删除redis缓存,再更新mysql | 无 | 缓存删除成功但数据库更新失败 | Java程序从数据库中读到旧值 | 再次更新数据库,重试 |
| | 有 | 缓存删除成功但数据库更新中...... 有并发读请求 | 并发请求从数据库读到旧值并回写到redis,导致后续都是从redis读取到旧值 | 延迟双删 |
| 先更新mysql,再删除redis缓存 | 无 | 数据库更新成功,但缓存删除失败 | Java程序从redis中读到旧值 | 再次删除缓存,重试 |
| | 有 | 数据库更新成功但缓存删除中...... 有并发读请求 | 并发请求从缓存读到旧值 | 等待redis删除完成,这段时间有 数据不一致,短暂存在。 |
4 Redis与MySQL数据双写一致性工程落地案例
4.1 复习+面试题
采用双检加锁策略
多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它。
其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。
后面的线程进来发现已经有缓存了,就直接走缓存。
4.2 canal
4.2.1 是什么
canal [kə'næl],中文翻译为 水道/管道/沟渠/运河,主要用途是用于 MySQL 数据库增量日志数据的订阅、消费和解析,是阿里巴巴开发并开源的,采用Java语言开发;
历史背景是早期阿里巴巴因为杭州和美国双机房部署,存在跨机房数据同步的业务需求,实现方式主要是基于业务 trigger(触发器) 获取增量变更。从2010年开始,阿里巴巴逐步尝试采用解析数据库日志获取增量变更进行同步,由此衍生出了canal项目;
官网地址: Home · alibaba/canal Wiki · GitHub
**一句话:**canal [kə'næl],译意为水道/管道/沟渠,主要用途是基于 MySQL数据库增量日志解析,提供增量数据订阅和消费
4.2.2 能干嘛
数据库镜像
数据库实时备份
索引构建和实时维护(拆分异构索引、倒排索引等)
业务cache刷新
带业务逻辑的增量数据处理
4.2.3 去哪下
Release v1.1.6 · alibaba/canal · GitHub
4.3 工作原理,面试回答
4.3.1 传统MySQL主从复制工作原理
MySQL的主从复制将经过如下步骤:
1、当 master 主服务器上的数据发生改变时,则将其改变写入二进制事件日志文件中;
2、salve 从服务器会在一定时间间隔内对 master 主服务器上的二进制日志进行探测,探测其是否发生过改变,
如果探测到 master 主服务器的二进制事件日志发生了改变,则开始一个 I/O Thread 请求 master 二进制事件日志;
3、同时 master 主服务器为每个 I/O Thread 启动一个dump Thread,用于向其发送二进制事件日志;
4、slave 从服务器将接收到的二进制事件日志保存至自己本地的中继日志文件中;
5、salve 从服务器将启动 SQL Thread 从中继日志中读取二进制日志,在本地重放,使得其数据和主服务器保持一致;
6、最后 I/O Thread 和 SQL Thread 将进入睡眠状态,等待下一次被唤醒;
4.3.2 canal工作原理
4.4 mysql-canal-redis双写一致性Coding
4.4.1 mysql
查看mysql版本
SELECT VERSION();
mysql5.7.28
当前的主机二进制日志
show master status;
查看SHOW VARIABLES LIKE 'log_bin';
开启 MySQL的binlog写入功能
D:\devSoft\mysql\mysql5.7.28目录下打开
最好提前备份
my.ini
|-------------------------------------------------------------------------------------------------------------------|
| log-bin=mysql-bin #开启 binlog binlog-format=ROW #选择 ROW 模式 server_id=1 #配置MySQL replaction需要定义,不要和canal的 slaveId重复 |
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ROW模式 除了记录sql语句之外,还会记录每个字段的变化情况,能够清楚的记录每行数据的变化历史,但会占用较多的空间。 STATEMENT模式只记录了sql语句,但是没有记录上下文信息,在进行数据恢复的时候可能会导致数据的丢失情况; MIX模式比较灵活的记录,理论上说当遇到了表结构变更的时候,就会记录为statement模式。当遇到了数据更新或者删除情况下就会变为row模式; |
window my.ini
linux my.cnf
重启mysql
再次查看SHOW VARIABLES LIKE 'log_bin';
授权canal连接MySQL账号
mysql默认的用户在mysql库的user表里
SELECT * FROM mysql.`user`
默认没有canal账户,此处新建+授权
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| DROP USER IF EXISTS 'canal'@'%'; CREATE USER 'canal'@'%' IDENTIFIED BY 'canal'; GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' IDENTIFIED BY 'canal'; FLUSH PRIVILEGES; SELECT * FROM mysql.user; |
4.4.2 canal服务端
4.4.2.1 下载
下载Linux版本:canal.deployer-1.1.6.tar.gz
注意发布时间+版本,2022.8.11后发布的才用
4.4.2.2 解压
解压后整体放入/mycanal路径下
4.4.2.3 配置
修改/mycanal/conf/example路径下instance.properties文件
instance.properties
换成自己的mysql主机master的IP地址
换成自己的在mysql新建的canal账户
4.4.2.4 启动
/opt/mycanal/bin路径下执行 ./startup.sh
4.4.2.5 查看
判断canal是否启动成功
查看 server 日志
查看 样例example 的日志
4.4.3 canal客户端(Java编写业务程序)
4.4.3.1 SQL脚本
1 随便选个数据库,以你自己为主,本例bigdata,按照下面建表
sql
CREATE TABLE `t_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`userName` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4
4.4.3.2 建module
canal_demo02
4.4.3.3 改POM
XML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.atguigu.canal</groupId>
<artifactId>canal_demo02</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.14</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.12</junit.version>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.16.18</lombok.version>
<mysql.version>5.1.47</mysql.version>
<druid.version>1.1.16</druid.version>
<mapper.version>4.1.5</mapper.version>
<mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
</properties>
<dependencies>
<!--canal-->
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.0</version>
</dependency>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--swagger2-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--SpringBoot与Redis整合依赖-->
<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>
<!--SpringBoot与AOP-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<!--Mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--SpringBoot集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<!--mybatis和springboot整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.spring.boot.version}</version>
</dependency>
<!--通用基础配置junit/devtools/test/log4j/lombok/hutool-->
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.2.3</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
<version>1.0.2</version>
</dependency>
<!--通用Mapper-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>${mapper.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.8.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4.4.3.4 写YML
XML
server.port=5555
# ========================alibaba.druid=====================
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/bigdata?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.druid.test-while-idle=false
4.4.3.5 主启动
java
package com.atguigu.canal;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @auther zzyy
* @create 2022-07-27 11:48
*/
@SpringBootApplication
public class CanalDemo02App
{
//本例不要启动CanalDemo02App实例
}
4.4.3.6 业务类
RedisUtils
java
package com.atguigu.canal.util;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/**
* @auther zzyy
* @create 2022-12-22 12:42
*/
public class RedisUtils
{
public static final String REDIS_IP_ADDR = "192.168.111.185";
public static final String REDIS_pwd = "111111";
public static JedisPool jedisPool;
static {
JedisPoolConfig jedisPoolConfig=new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(20);
jedisPoolConfig.setMaxIdle(10);
jedisPool=new JedisPool(jedisPoolConfig,REDIS_IP_ADDR,6379,10000,REDIS_pwd);
}
public static Jedis getJedis() throws Exception {
if(null!=jedisPool){
return jedisPool.getResource();
}
throw new Exception("Jedispool is not ok");
}
}
RedisCanalClientExample
java
package com.atguigu.canal.biz;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry.*;
import com.alibaba.otter.canal.protocol.Message;
import com.atguigu.canal.util.RedisUtils;
import redis.clients.jedis.Jedis;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2022-12-22 12:43
*/
public class RedisCanalClientExample
{
public static final Integer _60SECONDS = 60;
public static final String REDIS_IP_ADDR = "192.168.111.185";
private static void redisInsert(List<Column> columns)
{
JSONObject jsonObject = new JSONObject();
for (Column column : columns)
{
System.out.println(column.getName() + " : " + column.getValue() + " update=" + column.getUpdated());
jsonObject.put(column.getName(),column.getValue());
}
if(columns.size() > 0)
{
try(Jedis jedis = RedisUtils.getJedis())
{
jedis.set(columns.get(0).getValue(),jsonObject.toJSONString());
}catch (Exception e){
e.printStackTrace();
}
}
}
private static void redisDelete(List<Column> columns)
{
JSONObject jsonObject = new JSONObject();
for (Column column : columns)
{
jsonObject.put(column.getName(),column.getValue());
}
if(columns.size() > 0)
{
try(Jedis jedis = RedisUtils.getJedis())
{
jedis.del(columns.get(0).getValue());
}catch (Exception e){
e.printStackTrace();
}
}
}
private static void redisUpdate(List<Column> columns)
{
JSONObject jsonObject = new JSONObject();
for (Column column : columns)
{
System.out.println(column.getName() + " : " + column.getValue() + " update=" + column.getUpdated());
jsonObject.put(column.getName(),column.getValue());
}
if(columns.size() > 0)
{
try(Jedis jedis = RedisUtils.getJedis())
{
jedis.set(columns.get(0).getValue(),jsonObject.toJSONString());
System.out.println("---------update after: "+jedis.get(columns.get(0).getValue()));
}catch (Exception e){
e.printStackTrace();
}
}
}
public static void printEntry(List<Entry> entrys) {
for (Entry entry : entrys) {
if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {
continue;
}
RowChange rowChage = null;
try {
//获取变更的row数据
rowChage = RowChange.parseFrom(entry.getStoreValue());
} catch (Exception e) {
throw new RuntimeException("ERROR ## parser of eromanga-event has an error,data:" + entry.toString(),e);
}
//获取变动类型
EventType eventType = rowChage.getEventType();
System.out.println(String.format("================> binlog[%s:%s] , name[%s,%s] , eventType : %s",
entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),
entry.getHeader().getSchemaName(), entry.getHeader().getTableName(), eventType));
for (RowData rowData : rowChage.getRowDatasList()) {
if (eventType == EventType.INSERT) {
redisInsert(rowData.getAfterColumnsList());
} else if (eventType == EventType.DELETE) {
redisDelete(rowData.getBeforeColumnsList());
} else {//EventType.UPDATE
redisUpdate(rowData.getAfterColumnsList());
}
}
}
}
public static void main(String[] args)
{
System.out.println("---------O(∩_∩)O哈哈~ initCanal() main方法-----------");
//=================================
// 创建链接canal服务端
CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(REDIS_IP_ADDR,
11111), "example", "", "");
int batchSize = 1000;
//空闲空转计数器
int emptyCount = 0;
System.out.println("---------------------canal init OK,开始监听mysql变化------");
try {
connector.connect();
//connector.subscribe(".*\\..*");
connector.subscribe("bigdata.t_user");
connector.rollback();
int totalEmptyCount = 10 * _60SECONDS;
while (emptyCount < totalEmptyCount) {
System.out.println("我是canal,每秒一次正在监听:"+ UUID.randomUUID().toString());
Message message = connector.getWithoutAck(batchSize); // 获取指定数量的数据
long batchId = message.getId();
int size = message.getEntries().size();
if (batchId == -1 || size == 0) {
emptyCount++;
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
} else {
//计数器重新置零
emptyCount = 0;
printEntry(message.getEntries());
}
connector.ack(batchId); // 提交确认
// connector.rollback(batchId); // 处理失败, 回滚数据
}
System.out.println("已经监听了"+totalEmptyCount+"秒,无任何消息,请重启重试......");
} finally {
connector.disconnect();
}
}
}
题外话
java程序下connector.subscribe配置的过滤正则
关闭资源代码简写
try-with-resources释放资源
5 案例落地实战bitmap/hyperloglog/GEO
5.1 先看看大厂真实需求+面试题反馈
5.1.1 面试题1
抖音电商直播,主播介绍的商品有评论,1个商品对应了1系列的评论,排序+展现+取前10条记录
用户在手机App上的签到打卡信息:1天对应1系列用户的签到记录,新浪微博、钉钉打卡签到,来没来如何统计?
应用网站上的网页访问信息:1个网页对应1系列的访问点击,淘宝网首页,每天有多少人浏览首页?
你们公司系统上线后,说一下UV、PV、DAU分别是多少?
5.1.2 面试题2
面试问
记录对集合中的数据进行统计
在移动应用中,需要统计每天的新增用户数和第2天的留存用户数;
在电商网站的商品评论中,需要统计评论列表中的最新评论;
在签到打卡中,需要统计一个月内连续打卡的用户数;
在网页访问记录中,需要统计独立访客(Unique Visitor,UV)量。
。。。。。。
痛点:
类似今日头条、抖音、淘宝这样的额用户访问级别都是亿级的,请问如何处理?
5.1.3 需求痛点
亿级数据的收集+清洗+统计+展现
一句话:存的进+取得快+多维度
真正有价值的是统计。。。。。。
5.2 统计的类型有哪些?
亿级系统中,常见的四种统计
5.2.1 聚合统计
统计多个集合元素的聚合结果,就是前面讲解过的交差并等集合统计
复习命令
交并差集和聚合函数的应用
5.2.2 排序统计
抖音短视频最新评论留言的场景,请你设计一个展现列表。考察你的数据结构和设计思路
设计案例和回答思路
以抖音vcr最新的留言评价为案例,所有评论需要两个功能,按照时间排序(正序、反序)+分页显示
能够排序+分页显示的redis数据结构是什么合适?
answer
zset
在⾯对需要展示最新列表、排行榜等场景时, 如果数据更新频繁或者需要分页显示,建议使⽤ZSet
5.2.3 二值统计
集合元素的取值就只有0和1两种。在钉钉上班签到打卡的场景中,我们只用记录有签到(1)或没签到(0)
见bitmap
5.2.4 基数统计
指统计⼀个集合中不重复的元素个数
见hyperloglog
5.3 hyperloglog
5.3.1 说名词,行话谈资
什么是UV
Unique Visitor,独立访客,一般理解为客户端IP
需要去重考虑
什么是PV
Page View,页面浏览量
不用去重
什么是DAU
Daily Active User
日活跃用户量:登录或者使用了某个产品的用户数(去重复登录的用户)
常用于反映网站、互联网应用或者网络游戏的运营情况
什么是MAU
MonthIy Active User
月活跃用户量
假如上述术语,你不知道?o(╥﹏╥)o
5.3.2 看需求
很多计数类场景,比如 每日注册 IP 数、每日访问 IP 数、页面实时访问数 PV、访问用户数 UV等。
因为主要的目标高效、巨量地进行计数,所以对存储的数据的内容并不太关心。
也就是说它只能用于统计巨量数量,不太涉及具体的统计对象的内容和精准性。
统计单日一个页面的访问量(PV),单次访问就算一次。
统计单日一个页面的用户访问量(UV),即按照用户为维度计算,单个用户一天内多次访问也只算一次。
多个key的合并统计,某个门户网站的所有模块的PV聚合统计就是整个网站的总PV。
5.3.3 是什么(小白篇讲解过,快速复习一下)
基数
是一种数据集,去重复后的真实个数
案例Case
去重复统计功能的基数估计算法-就是HyperLogLog
基数统计
用于统计一个集合中不重复的元素个数,就是对集合去重复后剩余元素的计算
一句话
去重脱水后的真实数据
基本命令
5.3.4 HyPerLogLog如何做的?如何演化出来的?
基数统计就是HyperLogLog
5.3.4.1 去重复统计你先会想到哪些方式?
HashSet
bitmap
如果数据显较大亿级统计,使用bitmaps同样会有这个问题。
bitmap是通过用位bit数组来表示各元素是否出现,每个元素对应一位,所需的总内存为N个bit。
基数计数则将每一个元素对应到bit数组中的其中一位,比如bit数组010010101(按照从零开始下标,有的就是1、4、6、8)。
新进入的元素只需要将已经有的bit数组和新加入的元素进行按位或计算就行。这个方式能大大减少内存占用且位操作迅速。
But,假设一个样本案例就是一亿个基数位值数据,一个样本就是一亿
如果要统计1亿个数据的基数位值,大约需要内存100000000/8/1024/1024约等于12M,内存减少占用的效果显著。
这样得到统计一个对象样本的基数值需要12M。
如果统计10000个对象样本(1w个亿级),就需要117.1875G将近120G,可见使用bitmaps还是不适用大数据量下(亿级)的基数计数场景,
但是bitmaps方法是精确计算的。
结论
样本元素越多内存消耗急剧增大,难以管控+各种慢,对于亿级统计不太合适,大数据害死人,o(╥﹏╥)o
量变引起质变
办法?
概率算法
通过牺牲准确率来换取空间,对于不要求绝对准确率的场景下可以使用,因为概率算法不直接存储数据本身,
通过一定的概率统计方法预估基数值,同时保证误差在一定范围内,由于又不储存数据故此可以大大节约内存。
HyperLogLog就是一种概率算法的实现。
5.3.4.2 原理说明
只是进行不重复的基数统计,不是集合也不保存数据,只记录数量而不是具体内容。
有误差
Hyperloglog提供不精确的去重计数方案
牺牲准确率来换取空间,误差仅仅只是0.81%左右
这个误差如何来的?论文地址和出处
Redis new data structure: the HyperLogLog - http://antirez.com/news/75
Redis之父安特雷兹回答:
5.3.5 淘宝网站首页亿级UV的Redis统计方案
5.3.5.1 需求
UV的统计需要去重,一个用户一天内的多次访问只能算作一次
淘宝、天猫首页的UV,平均每天是1~1.5个亿左右
每天存1.5个亿的IP,访问者来了后先去查是否存在,不存在加入
5.3.5.2 方案讨论
用mysql
傻X,o(╥﹏╥)o,不解释
用redis的hash结构存储
说明:
redis------hash = <keyDay,<ip,1>>
按照ipv4的结构来说明,每个ipv4的地址最多是15个字节(ip = "192.168.111.1",最多xxx.xxx.xxx.xxx)
某一天的1.5亿 * 15个字节= 2G,一个月60G,redis死定了。o(╥﹏╥)o
hyperloglog
为什么是只需要花费12Kb?
5.3.5.3 HyperLogLogService
java
package com.atguigu.redis.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2021-05-02 18:16
*/
@Service
@Slf4j
public class HyperLogLogService
{
@Resource
private RedisTemplate redisTemplate;
/**
* 模拟后台有用户点击首页,每个用户来自不同ip地址
*/
@PostConstruct
public void init()
{
log.info("------模拟后台有用户点击首页,每个用户来自不同ip地址");
new Thread(() -> {
String ip = null;
for (int i = 1; i <=200; i++) {
Random r = new Random();
ip = r.nextInt(256) + "." + r.nextInt(256) + "." + r.nextInt(256) + "." + r.nextInt(256);
Long hll = redisTemplate.opsForHyperLogLog().add("hll", ip);
log.info("ip={},该ip地址访问首页的次数={}",ip,hll);
//暂停3秒钟线程
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
}
},"t1").start();
}
}
5.3.5.4 HyperLogLogController
java
package com.atguigu.redis.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @auther zzyy
* @create 2021-05-02 18:16
*/
@Api(description = "淘宝亿级UV的Redis统计方案")
@RestController
@Slf4j
public class HyperLogLogController
{
@Resource
private RedisTemplate redisTemplate;
@ApiOperation("获得IP去重后的首页访问量")
@RequestMapping(value = "/uv",method = RequestMethod.GET)
public long uv()
{
//pfcount
return redisTemplate.opsForHyperLogLog().size("hll");
}
}
5.4 GEO
5.4.1 大厂面试题简介
面试题说明:
移动互联网时代LBS应用越来越多,交友软件中附近的小姐姐、外卖软件中附近的美食店铺、打车软件附近的车辆等等。
那这种附近各种形形色色的XXX地址位置选择是如何实现的?
会有什么问题呢?
1.查询性能问题,如果并发高,数据量大这种查询是要搞垮mysql数据库的
2.一般mysql查询的是一个平面矩形访问,而叫车服务要以我为中心N公里为半径的圆形覆盖。
3.精准度的问题,我们知道地球不是平面坐标系,而是一个圆球,这种矩形计算在长距离计算时会有很大误差,mysql不合适
5.4.2 地理知识说明
经纬度
经度与纬度的合称组成一个坐标系统。又称为地理坐标系统,它是一种利用三度空间的球面来定义地球上的空间的球面坐标系统,能够标示地球上的任何一个位置。
经线和纬线
是人们为了在地球上确定位置和方向的,在地球仪和地图上画出来的,地面上并线。
和经线相垂直的线叫做纬线(纬线指示东西方向)。纬线是一条条长度不等的圆圈。最长的纬线就是赤道。
因为经线指示南北方向,所以经线又叫子午线。 国际上规定,把通过英国格林尼治天文台原址的经线叫做0°所以经线也叫本初子午线。在地球上经线指示南北方向,纬线指示东西方向。
东西半球分界线:东经160° 西经20°
经度和维度
经度(longitude):东经为正数,西经为负数。东西经
纬度(latitude):北纬为正数,南纬为负数。南北纬
5.4.3 如何获得某个地址的经纬度
拾取坐标系统http://api.map.baidu.com/lbsapi/getpoint/
5.4.4 命令复习,第二次
5.4.4.1 GEOADD添加经纬度坐标
命令如下:
GEOADD city 116.403963 39.915119 "天安门" 116.403414 39.924091 "故宫" 116.024067 40.362639 "长城"
中文乱码如何处理
5.4.4.2 GEOPOS返回经纬度
|--------------------|
| GEOPOS city 天安门 故宫 |
5.4.4.3 GEOHASH返回坐标的geohash表示
|------------------------|
| GEOHASH city 天安门 故宫 长城 |
geohash算法生成的base32编码值
3维变2维变1维
5.4.4.4 GEODIST 两个位置之间距离
|------------------------|
| GEODIST city 天安门 长城 km |
5.4.4.5 GEORADIUS
georadius 以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。
GEORADIUS city 116.418017 39.914402 10 km withdist withcoord count 10 withhash desc
GEORADIUS city 116.418017 39.914402 10 km withdist withcoord count 10 desc
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。 WITHCOORD: 将位置元素的经度和维度也一并返回。 WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大 COUNT 限定返回的记录数。 |
当前位置(116.418017 39.914402),阳哥在王府井
以半径为中心,查找附近的XXX
5.4.4.6 GEORADIUSBYMEMBER
5.4.5 美团地图位置附近的酒店推送
5.4.5.1 需求分析
美团app附近的酒店
摇个妹子,附近的妹子
高德地图附近的人或者一公里以内的各种营业厅、加油站、理发店、超市.....
找个单车
。。。。。。
5.4.5.2 架构设计
Redis的新类型GEO
命令
http://www.redis.cn/commands/geoadd.htmlhttp://www.redis.cn/commands/geoadd.html
5.4.5.3 编码实现
关键点
GeoController
java
package com.atguigu.redis7.controller;
import com.atguigu.redis7.service.GeoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.geo.*;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @auther zzyy
* @create 2022-12-25 12:12
*/
@Api(tags = "美团地图位置附近的酒店推送GEO")
@RestController
@Slf4j
public class GeoController
{
@Resource
private GeoService geoService;
@ApiOperation("添加坐标geoadd")
@RequestMapping(value = "/geoadd",method = RequestMethod.GET)
public String geoAdd()
{
return geoService.geoAdd();
}
@ApiOperation("获取经纬度坐标geopos")
@RequestMapping(value = "/geopos",method = RequestMethod.GET)
public Point position(String member)
{
return geoService.position(member);
}
@ApiOperation("获取经纬度生成的base32编码值geohash")
@RequestMapping(value = "/geohash",method = RequestMethod.GET)
public String hash(String member)
{
return geoService.hash(member);
}
@ApiOperation("获取两个给定位置之间的距离")
@RequestMapping(value = "/geodist",method = RequestMethod.GET)
public Distance distance(String member1, String member2)
{
return geoService.distance(member1,member2);
}
@ApiOperation("通过经度纬度查找北京王府井附近的")
@RequestMapping(value = "/georadius",method = RequestMethod.GET)
public GeoResults radiusByxy()
{
return geoService.radiusByxy();
}
@ApiOperation("通过地方查找附近,本例写死天安门作为地址")
@RequestMapping(value = "/georadiusByMember",method = RequestMethod.GET)
public GeoResults radiusByMember()
{
return geoService.radiusByMember();
}
}
GeoService
java
package com.atguigu.redis7.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
import org.springframework.data.geo.Circle;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @auther zzyy
* @create 2022-12-25 12:11
*/
@Service
@Slf4j
public class GeoService
{
public static final String CITY ="city";
@Autowired
private RedisTemplate redisTemplate;
public String geoAdd()
{
Map<String, Point> map= new HashMap<>();
map.put("天安门",new Point(116.403963,39.915119));
map.put("故宫",new Point(116.403414 ,39.924091));
map.put("长城" ,new Point(116.024067,40.362639));
redisTemplate.opsForGeo().add(CITY,map);
return map.toString();
}
public Point position(String member) {
//获取经纬度坐标
List<Point> list= this.redisTemplate.opsForGeo().position(CITY,member);
return list.get(0);
}
public String hash(String member) {
//geohash算法生成的base32编码值
List<String> list= this.redisTemplate.opsForGeo().hash(CITY,member);
return list.get(0);
}
public Distance distance(String member1, String member2) {
//获取两个给定位置之间的距离
Distance distance= this.redisTemplate.opsForGeo().distance(CITY,member1,member2, RedisGeoCommands.DistanceUnit.KILOMETERS);
return distance;
}
public GeoResults radiusByxy() {
//通过经度,纬度查找附近的,北京王府井位置116.418017,39.914402
Circle circle = new Circle(116.418017, 39.914402, Metrics.KILOMETERS.getMultiplier());
//返回50条
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(50);
GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults= this.redisTemplate.opsForGeo().radius(CITY,circle, args);
return geoResults;
}
public GeoResults radiusByMember() {
//通过地方查找附近
String member="天安门";
//返回50条
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(50);
//半径10公里内
Distance distance=new Distance(10, Metrics.KILOMETERS);
GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults= this.redisTemplate.opsForGeo().radius(CITY,member, distance,args);
return geoResults;
}
}
5.5 bitmap
5.5.1 大厂真实面试题案例
日活统计
连续签到打卡
最近一周的活跃用户
统计指定用户一年之中的登陆天数
某用户按照一年365天,哪几天登陆过?哪几天没有登陆?全年中登录的天数共计多少?
5.5.2 是什么
说明:用String类型作为底层数据结构实现的一种统计二值状态的数据类型
位图本质是数组,它是基于String数据类型的按位的操作。该数组由多个二进制位组成,每个二进制位都对应一个偏移量(我们可以称之为一个索引或者位格)。Bitmap支持的最大位数是2^32位,它可以极大的节约存储空间,使用512M内存就可以存储多大42.9亿的字节信息(2^32 = 4294967296)
一句话: 由0和1状态表现的二进制位的bit数组
5.5.3 能干嘛
用于状态统计
Y、N,类似AtomicBoolean
看需求
用户是否登陆过Y、N,比如京东每日签到送京豆
电影、广告是否被点击播放过
钉钉打卡上下班,签到统计
。。。。。。
5.5.4 京东签到领取京豆
5.5.4.1 需求说明
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 签到日历仅展示当月签到数据 签到日历需展示最近连续签到天数 假设当前日期是20210618,且20210616未签到 若20210617已签到且0618未签到,则连续签到天数为1 若20210617已签到且0618已签到,则连续签到天数为2 连续签到天数越多,奖励越大 所有用户均可签到 截至2020年3月31日的12个月,京东年度活跃用户数3.87亿,同比增长24.8%,环比增长超2500万,此外,2020年3月移动端日均活跃用户数同比增长46%假设10%左右的用户参与签到,签到用户也高达3千万。。。。。。o(╥﹏╥)o |
5.5.4.2 小厂方法,传统mysql方式
建表SQL
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| CREATE TABLEuser_sign ( keyidBIGINT NOT NULL PRIMARY KEYAUTO_INCREMENT, user_key****VARCHAR(200),#京东用户IDsign_date DATETIME,#**签到日期(20210618) sign_count INT#连续签到天数 ) |
| INSERT INTOuser_sign(user_key,sign_date,sign_count) VALUES('20210618-xxxx-xxxx-xxxx-xxxxxxxxxxxx','2020-06-18 15:11:12',1); |
| SELECTsign_count FROMuser_sign WHERE****user_key= '20210618-xxxx-xxxx-xxxx-xxxxxxxxxxxx'****ANDsign_date BETWEEN**'2020-06-17 00:00:00'AND'2020-06-18 23:59:59' ORDER BYsign_date DESCLIMIT 1; |
困难和解决思路
方法正确但是难以落地实现,o(╥﹏╥)o。
签到用户量较小时这么设计能行,但京东这个体量的用户(估算3000W签到用户,一天一条数据,一个月就是9亿数据)
对于京东这样的体量,如果一条签到记录对应着当日用记录,那会很恐怖......
如何解决这个痛点?
1 一条签到记录对应一条记录,会占据越来越大的空间。
2 一个月最多31天,刚好我们的int类型是32位,那这样一个int类型就可以搞定一个月,32位大于31天,当天来了位是1没来就是0。
3 一条数据直接存储一个月的签到记录,不再是存储一天的签到记录。
5.5.4.3 大厂方法,基于Redis的Bitmaps实现签到日历
建表-按位-redis bitmap
在签到统计时,每个用户一天的签到用1个bit位就能表示,
一个月(假设是31天)的签到情况用31个bit位就可以,一年的签到也只需要用365个bit位,根本不用太复杂的集合类型
5.5.5 命令复习,第二次
setbit
setbit key offset value
setbit 键 偏移位 只能零或者1
Bitmap的偏移量是从零开始算的
getbit
getbit key offset
setbit和getbit案例说明
按照天
按照年
按年去存储一个用户的签到情况,365 天只需要 365 / 8 ≈ 46 Byte,1000W 用户量一年也只需要 44 MB 就足够了。
假如是亿级的系统,
每天使用1个1亿位的Bitmap约占12MB的内存(10^8/8/1024/1024),10天的Bitmap的内存开销约为120MB,内存压力不算太高。在实际使用时,最好对Bitmap设置过期时间,让Redis自动删除不再需要的签到记录以节省内存开销。
bitmap的底层编码说明,get命令操作如何
实质是二进制的ascii编码对应
redis里用type命令看看bitmap实质是什么类型???
man ascii
设置命令
|---------------------------------------|
| 两个setbit命令对k1进行设置后,对应的二进制串就是0100 0001 |
| 二进制串就是0100 0001对应的10进制就是65,所以见下图: |
strlen
统计字节数占用多少
不是字符串长度而是占据几个字节,超过8位后自己按照8位一组一byte再扩容
bitcount
全部键里面含有1的有多少个?
一年365天,全年天天登陆占用多少字节
bittop
连续2天都签到的用户
加入某个网站或者系统,它的用户有1000W,做个用户id和位置的映射
比如0号位对应用户id:uid-092iok-lkj
比如1号位对应用户id:uid-7388c-xxx
。。。。。。
案例实战见下一章,bitmap类型签到+结合布隆过滤器,案例升级
6 布隆过滤器BloomFilter
6.1 先看看大厂真实需求+面试题反馈
现有50亿个电话号码,现有10万个电话号码,如何要快速准确的判断这些电话号码是否已经存在?
我让你判断在50亿记录中有没有,不是让你存。 有就返回1,没有返回零。
1、通过数据库查询-------实现快速有点难。
2、数据预放到内存集合中:50亿*8字节大约40G,内存太大了。
判断是否存在,布隆过滤器了解过吗?
安全连接网址,全球数10亿的网址判断
黑名单校验,识别垃圾邮件
白名单校验,识别出合法用户进行后续处理
。。。。。。
6.2 是什么
6.2.1 一句话
由一个初值都为零的bit数组和多个哈希函数构成,用来快速判断集合中是否存在某个元素
设计思想
|----|------------------------------|
| 目的 | 减少内存占用 |
| 方式 | 不保存数据信息,只是在内存中做一个是否存在的标记flag |
本质就是判断具体数据是否存在于一个大的集合中
6.2.2 备注
布隆过滤器是一种类似set的数据结构,只是统计结果在巨量数据下有点小瑕疵,不够完美
布隆过滤器(英语:Bloom Filter)是 1970 年由布隆提出的。
它实际上是一个很长的二进制数组(00000000)+一系列随机hash算法映射函数,主要用于判断一个元素是否在集合中。
通常我们会遇到很多要判断一个元素是否在某个集合中的业务场景,一般想到的是将集合中所有元素保存起来,然后通过比较确定。
链表、树、哈希表等等数据结构都是这种思路。但是随着集合中元素的增加,我们需要的存储空间也会呈现线性增长,最终达到瓶颈。同时检索速度也越来越慢,上述三种结构的检索时间复杂度分别为O(n),O(logn),O(1)。这个时候,布隆过滤器(Bloom Filter)就应运而生
6.3 能干嘛-特点考点
高效地插入和查询,占用空间少,返回的结果是不确定性+不够完美。
|----|------------------------------|
| 目的 | 减少内存占用 |
| 方式 | 不保存数据信息,只是在内存中做一个是否存在的标记flag |
重点
一个元素如果判断结果:存在时,元素不一定存在,但是判断结果为不存在时,则一定不存在。
布隆过滤器可以添加元素,但是不能删除元素,由于涉及hashcode判断依据,删掉元素会导致误判率增加。
小总结
有,是可能有
无,是肯定无
可以保证的是,如果布隆过滤器判断一个元素不在一个集合中,那这个元素一定不会在集合中
6.4 布隆过滤器原理
6.4.1 布隆过滤器实现原理和数据结构
6.4.1.1 原理
布隆过滤器原理
布隆过滤器(Bloom Filter) 是一种专门用来解决去重问题的高级数据结构。
实质就是一个大型位数组和几个不同的无偏hash函数(无偏表示分布均匀)。由一个初值都为零的bit数组和多个个哈希函数构成,用来快速判断某个数据是否存在。但是跟 HyperLogLog 一样,它也一样有那么一点点不精确,也存在一定的误判概率
6.4.1.2 添加key、查询key
添加key时
使用多个hash函数对key进行hash运算得到一个整数索引值,对位数组长度进行取模运算得到一个位置,
每个hash函数都会得到一个不同的位置,将这几个位置都置1就完成了add操作。
查询key时
只要有其中一位是零就表示这个key不存在,但如果都是1,则不一定存在对应的key。
结论: 有,是可能有 无,是肯定无
6.4.1.3 hash冲突导致数据不精准
当有变量被加入集合时,通过N个映射函数将这个变量映射成位图中的N个点,
把它们置为 1(假定有两个变量都通过 3 个映射函数)。
|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 查询某个变量的时候我们只要看看这些点是不是都是 1, 就可以大概率知道集合中有没有它了 如果这些点, 有任何一个为零则被查询变量一定不在, 如果都是 1,则被查询变量很 可能存在, 为什么说是可能存在,而不是一定存在呢?那是因为映射函数本身就是散列函数,散列函数是会有碰撞的。(见上图3号坑两个对象都1) |
6.4.1.4 hash冲突导致数据不精准2
哈希函数
哈希函数的概念是:将任意大小的输入数据转换成特定大小的输出数据的函数,转换后的数据称为哈希值或哈希编码,也叫散列值
如果两个散列值是不相同的(根据同一函数)那么这两个散列值的原始输入也是不相同的。
这个特性是散列函数具有确定性的结果,具有这种性质的散列函数称为单向散列函数。
散列函数的输入和输出不是唯一对应关系的,如果两个散列值相同,两个输入值很可能是相同的,但也可能不同,这种情况称为"散列碰撞(collision)"。
用 hash表存储大数据量时,空间效率还是很低,当只有一个 hash 函数时,还很容易发生哈希碰撞。
Java中hash冲突java案例
java
/**
* 模拟演示hash冲突
*/
@Test
public void testHash()
{
System.out.println("Aa".hashCode());
System.out.println("BB".hashCode());
System.out.println("柳柴".hashCode());
System.out.println("柴柕".hashCode());
System.out.println();
Set<Integer> sets = new HashSet<>();
int hashCode;
for (int i = 0; i < 200000; i++)
{
hashCode = new Object().hashCode();
if(sets.contains(hashCode))
{
System.out.println("运行到第: "+i+" 次出现hash冲突,hashcode: "+hashCode);
continue;
}
sets.add(hashCode);
}
}
Java8 new了20W个Object,出现了8次hash冲突。 而这就是hash冲突导致数据不精确的直接原因
6.4.2 使用3步骤
6.4.2.1 初始化bitmap
布隆过滤器 本质上 是由长度为 m 的位向量或位列表(仅包含 0 或 1 位值的列表)组成,最初所有的值均设置为 0
6.4.2.2 添加占坑位
当我们向布隆过滤器中添加数据时,为了尽量地址不冲突,会使用多个 hash 函数对 key 进行运算,算得一个下标索引值,然后对位数组长度进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位置。再把位数组的这几个位置都置为 1 就完成了 add 操作。
例如,我们添加一个字符串wmyskxz,对字符串进行多次hash(key) → 取模运行→ 得到坑位
6.4.2.3 判断是否存在
向布隆过滤器查询某个key是否存在时,先把这个 key 通过相同的多个 hash 函数进行运算,查看对应的位置是否都为 1,
只要有一个位为零,那么说明布隆过滤器中这个 key 不存在;
如果这几个位置全都是 1,那么说明极有可能存在;
因为这些位置的 1 可能是因为其他的 key 存在导致的,也就是前面说过的hash冲突。。。。。
就比如我们在 add 了字符串wmyskxz数据之后,很明显下面1/3/5 这几个位置的 1 是因为第一次添加的 wmyskxz 而导致的;
此时我们查询一个没添加过的不存在的字符串inexistent-key,它有可能计算后坑位也是1/3/5 ,这就是误判了......笔记见最下面
6.4.3 布隆过滤器误判率,为什么不要删除
布隆过滤器的误判是指多个输入经过哈希之后在相同的bit位置1了,这样就无法判断究竟是哪个输入产生的,
因此误判的根源在于相同的 bit 位被多次映射且置 1。
这种情况也造成了布隆过滤器的删除问题,因为布隆过滤器的每一个 bit 并不是独占的,很有可能多个元素共享了某一位。
如果我们直接删除这一位的话,会影响其他的元素
特性
布隆过滤器可以添加元素,但是不能删除元素。因为删掉元素会导致误判率增加。
6.4.4 小总结
是否存在
有,是很可能有;无,是肯定无,100%无
使用时最好不要让实际元素数量远大于初始化数量(布隆过滤器bitmap数量),一次给够避免扩容
当实际元素数量超过初始化数量时,应该对布隆过滤器进行重建,重新分配一个 size 更大的过滤器,再将所有的历史元素批量 add 进行
6.5 布隆过滤器的使用场景
6.5.1 解决缓存穿透的问题,和redis结合bitmap使用
缓存穿透是什么
一般情况下,先查询缓存redis是否有该条数据,缓存中没有时,再查询数据库。
当数据库也不存在该条数据时,每次查询都要访问数据库,这就是缓存穿透。
缓存透带来的问题是,当有大量请求查询数据库不存在的数据时,就会给数据库带来压力,甚至会拖垮数据库。
可以使用布隆过滤器解决缓存穿透的问题
把已存在数据的key存在布隆过滤器中,相当于redis前面挡着一个布隆过滤器。
当有新的请求时,先到布隆过滤器中查询是否存在:
如果布隆过滤器中不存在该条数据则直接返回;
如果布隆过滤器中已存在,才去查询缓存redis,如果redis里没查询到则再查询Mysql数据库
6.5.2 黑名单校验,识别垃圾邮件
发现存在黑名单中的,就执行特定操作。比如:识别垃圾邮件,只要是邮箱在黑名单中的邮件,就识别为垃圾邮件。
假设黑名单的数量是数以亿计的,存放起来就是非常耗费存储空间的,布隆过滤器则是一个较好的解决方案。
把所有黑名单都放在布隆过滤器中,在收到邮件时,判断邮件地址是否在布隆过滤器中即可。
6.5.3 安全连接网址,全球上10亿的网址判断
。。。。。。
6.6 尝试手写布隆过滤器,结合bitmap自研一下体会思想
结合bitmap类型手写一个简单的布隆过滤器,体会设计思想
整体架构:
6.6.1 步骤设计
6.6.1.1 redis的setbit/getbit
6.6.1.2 setBit的构建过程
@PostConstruct初始化白名单数据
计算元素的hash值
通过上一步hash值算出对应的二进制数组的坑位
将对应坑位的值的修改为数字1,表示存在
6.6.1.3 getBit查询是否存在
计算元素的hash值
通过上一步hash值算出对应的二进制数组的坑位
返回对应坑位的值,零表示无,1表示存在
6.6.2 springboot+redis+mybatis案例基础与一键编码环境整合
6.6.2.1 MyBatis 通用 Mapper4
mybatis-generator
MyBatis Generator Core -- Introduction to MyBatis Generatorhttp://mybatis.org/generator/
MyBatis 通用 Mapper4官网
https://github.com/abel533/Mapperhttps://github.com/abel533/Mapper
一键生成:
t_customer用户表SQL
sql
CREATE TABLE `t_customer` (
`id` int(20) NOT NULL AUTO_INCREMENT,
`cname` varchar(50) NOT NULL,
`age` int(10) NOT NULL,
`phone` varchar(20) NOT NULL,
`sex` tinyint(4) NOT NULL,
`birth` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_cname` (`cname`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4
建springboot的Module
mybatis_generator
改POM
XML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.atguigu.redis7</groupId>
<artifactId>mybatis_generator</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.10</version>
<relativePath/>
</parent>
<properties>
<!-- 依赖版本号 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<java.version>1.8</java.version>
<hutool.version>5.5.8</hutool.version>
<druid.version>1.1.18</druid.version>
<mapper.version>4.1.5</mapper.version>
<pagehelper.version>5.1.4</pagehelper.version>
<mysql.version>5.1.39</mysql.version>
<swagger2.version>2.9.2</swagger2.version>
<swagger-ui.version>2.9.2</swagger-ui.version>
<mybatis.spring.version>2.1.3</mybatis.spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--Mybatis 通用mapper tk单独使用,自己带着版本号-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<!--mybatis-spring-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.spring.version}</version>
</dependency>
<!-- Mybatis Generator -->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.4.0</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
<!--通用Mapper-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>${mapper.version}</version>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>${basedir}/src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>${basedir}/src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.6</version>
<configuration>
<configurationFile>${basedir}/src/main/resources/generatorConfig.xml</configurationFile>
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>${mapper.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
写YML
无
mgb配置相关src\main\resources路径下新建
config.properties
XML
#t_customer表包名
package.name=com.atguigu.redis7
jdbc.driverClass = com.mysql.jdbc.Driver
jdbc.url = jdbc:mysql://localhost:3306/bigdata
jdbc.user = root
jdbc.password =123456
generatorConfig.xml
XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<properties resource="config.properties"/>
<context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat">
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>
<plugin type="tk.mybatis.mapper.generator.MapperPlugin">
<property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
<property name="caseSensitive" value="true"/>
</plugin>
<jdbcConnection driverClass="${jdbc.driverClass}"
connectionURL="${jdbc.url}"
userId="${jdbc.user}"
password="${jdbc.password}">
</jdbcConnection>
<javaModelGenerator targetPackage="${package.name}.entities" targetProject="src/main/java"/>
<sqlMapGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java"/>
<javaClientGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java" type="XMLMAPPER"/>
<table tableName="t_customer" domainObjectName="Customer">
<generatedKey column="id" sqlStatement="JDBC"/>
</table>
</context>
</generatorConfiguration>
一键生成
双击插件mybatis-generator:gererate ,一键生成
生成entity+mapper接口+xml实现SQL
6.6.2.2 SpringBoot + Mybatis + Redis缓存实战编码
建Module
改造我们的redis7_study工程
POM
XML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.atguigu.redis7</groupId>
<artifactId>redis7_study</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.10</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.12</junit.version>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.16.18</lombok.version>
</properties>
<dependencies>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.3.1</version>
</dependency>
<!--lettuce-->
<!--<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.2.1.RELEASE</version>
</dependency>-->
<!--SpringBoot与Redis整合依赖-->
<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>
<!--swagger2-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--Mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--SpringBoot集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<!--mybatis和springboot整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.2.3</version>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
<version>1.0.2</version>
</dependency>
<!--通用Mapper-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>4.1.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!--通用基础配置junit/devtools/test/log4j/lombok/-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
YML
XML
server.port=7777
spring.application.name=redis7_study
# ========================logging=====================
logging.level.root=info
logging.level.com.atguigu.redis7=info
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n
logging.file.name=D:/mylogs2023/redis7_study.log
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n
# ========================swagger=====================
spring.swagger2.enabled=true
#在springboot2.6.X结合swagger2.9.X会提示documentationPluginsBootstrapper空指针异常,
#原因是在springboot2.6.X中将SpringMVC默认路径匹配策略从AntPathMatcher更改为PathPatternParser,
# 导致出错,解决办法是matching-strategy切换回之前ant_path_matcher
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
# ========================redis单机=====================
spring.redis.database=0
# 修改为自己真实IP
spring.redis.host=192.168.111.185
spring.redis.port=6379
spring.redis.password=111111
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
# ========================alibaba.druid=====================
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/bigdata?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.druid.test-while-idle=false
# ========================mybatis===================
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.atguigu.redis7.entities
# ========================redis集群=====================
#spring.redis.password=111111
## 获取失败 最大重定向次数
#spring.redis.cluster.max-redirects=3
#spring.redis.lettuce.pool.max-active=8
#spring.redis.lettuce.pool.max-wait=-1ms
#spring.redis.lettuce.pool.max-idle=8
#spring.redis.lettuce.pool.min-idle=0
##支持集群拓扑动态感应刷新,自适应拓扑刷新是否使用所有可用的更新,默认false关闭
#spring.redis.lettuce.cluster.refresh.adaptive=true
##定时刷新
#spring.redis.lettuce.cluster.refresh.period=2000
#spring.redis.cluster.nodes=192.168.111.185:6381,192.168.111.185:6382,192.168.111.172:6383,192.168.111.172:6384,192.168.111.184:6385,192.168.111.184:6386
\src\main\resources\目录下新建mapper文件夹并拷贝CustomerMapper.xml
主启动
java
package com.atguigu.redis7;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import tk.mybatis.spring.annotation.MapperScan;
/**
* @auther zzyy
* @create 2022-12-10 23:39
*/
@SpringBootApplication
@MapperScan("com.atguigu.redis7.mapper") //import tk.mybatis.spring.annotation.MapperScan;
public class Redis7Study7777
{
public static void main(String[] args)
{
SpringApplication.run(Redis7Study7777.class,args);
}
}
业务类
数据库表t_customer是否OK
entity
上一步自动生成的拷贝过来
java
package com.atguigu.redis7.entities;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.Date;
@Table(name = "t_customer")
public class Customer implements Serializable
{
@Id
@GeneratedValue(generator = "JDBC")
private Integer id;
private String cname;
private Integer age;
private String phone;
private Byte sex;
private Date birth;
public Customer()
{
}
public Customer(Integer id, String cname)
{
this.id = id;
this.cname = cname;
}
/**
* @return id
*/
public Integer getId() {
return id;
}
/**
* @param id
*/
public void setId(Integer id) {
this.id = id;
}
/**
* @return cname
*/
public String getCname() {
return cname;
}
/**
* @param cname
*/
public void setCname(String cname) {
this.cname = cname;
}
/**
* @return age
*/
public Integer getAge() {
return age;
}
/**
* @param age
*/
public void setAge(Integer age) {
this.age = age;
}
/**
* @return phone
*/
public String getPhone() {
return phone;
}
/**
* @param phone
*/
public void setPhone(String phone) {
this.phone = phone;
}
/**
* @return sex
*/
public Byte getSex() {
return sex;
}
/**
* @param sex
*/
public void setSex(Byte sex) {
this.sex = sex;
}
/**
* @return birth
*/
public Date getBirth() {
return birth;
}
/**
* @param birth
*/
public void setBirth(Date birth) {
this.birth = birth;
}
@Override
public String toString()
{
return "Customer{" +
"id=" + id +
", cname='" + cname + '\'' +
", age=" + age +
", phone='" + phone + '\'' +
", sex=" + sex +
", birth=" + birth +
'}';
}
}
mapper接口
java
package com.atguigu.redis7.mapper;
import com.atguigu.redis7.entities.Customer;
import tk.mybatis.mapper.common.Mapper;
public interface CustomerMapper extends Mapper<Customer> {
}
mapperSQL文件
XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.redis7.mapper.CustomerMapper">
<resultMap id="BaseResultMap" type="com.atguigu.redis7.entities.Customer">
<!--
WARNING - @mbg.generated
-->
<id column="id" jdbcType="INTEGER" property="id" />
<result column="cname" jdbcType="VARCHAR" property="cname" />
<result column="age" jdbcType="INTEGER" property="age" />
<result column="phone" jdbcType="VARCHAR" property="phone" />
<result column="sex" jdbcType="TINYINT" property="sex" />
<result column="birth" jdbcType="TIMESTAMP" property="birth" />
</resultMap>
</mapper>
service类
java
package com.atguigu.redis7.service;
import com.atguigu.redis7.entities.Customer;
import com.atguigu.redis7.mapper.CustomerMapper;
import com.atguigu.redis7.utils.CheckUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @auther zzyy
* @create 2022-07-23 13:55
*/
@Service
@Slf4j
public class CustomerSerivce
{
public static final String CACHE_KEY_CUSTOMER = "customer:";
@Resource
private CustomerMapper customerMapper;
@Resource
private RedisTemplate redisTemplate;
public void addCustomer(Customer customer){
int i = customerMapper.insertSelective(customer);
if(i > 0)
{
//到数据库里面,重新捞出新数据出来,做缓存
customer=customerMapper.selectByPrimaryKey(customer.getId());
//缓存key
String key=CACHE_KEY_CUSTOMER+customer.getId();
//往mysql里面插入成功随后再从mysql查询出来,再插入redis
redisTemplate.opsForValue().set(key,customer);
}
}
public Customer findCustomerById(Integer customerId){
Customer customer = null;
//缓存key的名称
String key=CACHE_KEY_CUSTOMER+customerId;
//1 查询redis
customer = (Customer) redisTemplate.opsForValue().get(key);
//redis无,进一步查询mysql
if(customer==null){
//2 从mysql查出来customer
customer=customerMapper.selectByPrimaryKey(customerId);
// mysql有,redis无
if (customer != null) {
//3 把mysql捞到的数据写入redis,方便下次查询能redis命中。
redisTemplate.opsForValue().set(key,customer);
}
}
return customer;
}
}
controller
java
package com.atguigu.redis7.controller;
import com.atguigu.redis7.entities.Customer;
import com.atguigu.redis7.service.CustomerSerivce;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Random;
import java.util.Date;
import java.util.concurrent.ExecutionException;
/**
* @auther zzyy
* @create 2022-07-23 13:55
*/
@Api(tags = "客户Customer接口+布隆过滤器讲解")
@RestController
@Slf4j
public class CustomerController{
@Resource private CustomerSerivce customerSerivce;
@ApiOperation("数据库初始化2条Customer数据")
@RequestMapping(value = "/customer/add", method = RequestMethod.POST)
public void addCustomer() {
for (int i = 0; i < 2; i++) {
Customer customer = new Customer();
customer.setCname("customer"+i);
customer.setAge(new Random().nextInt(30)+1);
customer.setPhone("1381111xxxx");
customer.setSex((byte) new Random().nextInt(2));
customer.setBirth(Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant()));
customerSerivce.addCustomer(customer);
}
}
@ApiOperation("单个用户查询,按customerid查用户信息")
@RequestMapping(value = "/customer/{id}", method = RequestMethod.GET)
public Customer findCustomerById(@PathVariable int id) {
return customerSerivce.findCustomerById(id);
}
}
启动测试Swagger是否OK
http://localhost:你的微服务端口/swagger-ui.html#/http://localhost/swagger-ui.html#/
http://localhost:7777/swagger-ui.htmlhttp://localhost:7777/swagger-ui.html
6.6.3 新增布隆过滤器案例
6.6.3.1 code
BloomFilterInit(白名单)
java
package com.atguigu.redis7.filter;
import ch.qos.logback.classic.util.StatusViaSLF4JLoggerFactory;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.transaction.managed.ManagedTransaction;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
/**
* @auther zzyy
* @create 2022-12-27 14:55
* 布隆过滤器白名单初始化工具类,一开始就设置一部分数据为白名单所有,
* 白名单业务默认规定:布隆过滤器有,redis是极大可能有。
* 白名单:whitelistCustomer
*/
@Component
@Slf4j
public class BloomFilterInit
{
@Resource
private RedisTemplate redisTemplate;
//@PostConstruct//初始化白名单数据,暂时注释省的后台打印
public void init()
{
//1 白名单客户加载到布隆过滤器
String key = "customer:12";
//2 计算hashValue,由于存在计算出来负数的可能,我们取绝对值
int hashValue = Math.abs(key.hashCode());
//3 通过hashValue和2的32次方后取余,获得对应的下标坑位
long index = (long)(hashValue % Math.pow(2,32));
log.info(key+" 对应的坑位index:{}",index);
//4 设置redis里面的bitmap对应类型白名单:whitelistCustomer的坑位,将该值设置为1
redisTemplate.opsForValue().setBit("whitelistCustomer",index,true);
}
}
@PostConstruct初始化白名单数据,故意差异化数据演示效果......
CheckUtils
java
package com.atguigu.redis7.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @auther zzyy
* @create 2022-12-27 14:56
*/
@Component
@Slf4j
public class CheckUtils
{
@Resource
private RedisTemplate redisTemplate;
public boolean checkWithBloomFilter(String checkItem,String key)
{
int hashValue = Math.abs(key.hashCode());
long index = (long) (hashValue % Math.pow(2, 32));
boolean existOK = redisTemplate.opsForValue().getBit(checkItem, index);
log.info("----->key:"+key+"\t对应坑位index:"+index+"\t是否存在:"+existOK);
return existOK;
}
}
CustomerSerivce
java
package com.atguigu.redis7.service;
import com.atguigu.redis7.entities.Customer;
import com.atguigu.redis7.mapper.CustomerMapper;
import com.atguigu.redis7.utils.CheckUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @auther zzyy
* @create 2022-07-23 13:55
*/
@Service
@Slf4j
public class CustomerSerivce
{
public static final String CACHE_KEY_CUSTOMER = "customer:";
@Resource
private CustomerMapper customerMapper;
@Resource
private RedisTemplate redisTemplate;
@Resource
private CheckUtils checkUtils;
public void addCustomer(Customer customer){
int i = customerMapper.insertSelective(customer);
if(i > 0)
{
//到数据库里面,重新捞出新数据出来,做缓存
customer=customerMapper.selectByPrimaryKey(customer.getId());
//缓存key
String key=CACHE_KEY_CUSTOMER+customer.getId();
//往mysql里面插入成功随后再从mysql查询出来,再插入redis
redisTemplate.opsForValue().set(key,customer);
}
}
public Customer findCustomerById(Integer customerId){
Customer customer = null;
//缓存key的名称
String key=CACHE_KEY_CUSTOMER+customerId;
//1 查询redis
customer = (Customer) redisTemplate.opsForValue().get(key);
//redis无,进一步查询mysql
if(customer==null)
{
//2 从mysql查出来customer
customer=customerMapper.selectByPrimaryKey(customerId);
// mysql有,redis无
if (customer != null) {
//3 把mysql捞到的数据写入redis,方便下次查询能redis命中。
redisTemplate.opsForValue().set(key,customer);
}
}
return customer;
}
/**
* BloomFilter → redis → mysql
* 白名单:whitelistCustomer
* @param customerId
* @return
*/
@Resource
private CheckUtils checkUtils;
public Customer findCustomerByIdWithBloomFilter (Integer customerId)
{
Customer customer = null;
//缓存key的名称
String key = CACHE_KEY_CUSTOMER + customerId;
//布隆过滤器check,无是绝对无,有是可能有
//===============================================
if(!checkUtils.checkWithBloomFilter("whitelistCustomer",key))
{
log.info("白名单无此顾客信息:{}",key);
return null;
}
//===============================================
//1 查询redis
customer = (Customer) redisTemplate.opsForValue().get(key);
//redis无,进一步查询mysql
if (customer == null) {
//2 从mysql查出来customer
customer = customerMapper.selectByPrimaryKey(customerId);
// mysql有,redis无
if (customer != null) {
//3 把mysql捞到的数据写入redis,方便下次查询能redis命中。
redisTemplate.opsForValue().set(key, customer);
}
}
return customer;
}
}
CustomerController
java
package com.atguigu.redis7.controller;
import com.atguigu.redis7.entities.Customer;
import com.atguigu.redis7.service.CustomerSerivce;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Random;
import java.util.Date;
import java.util.concurrent.ExecutionException;
/**
* @auther zzyy
* @create 2022-07-23 13:55
*/
@Api(tags = "客户Customer接口+布隆过滤器讲解")
@RestController
@Slf4j
public class CustomerController
{
@Resource private CustomerSerivce customerSerivce;
@ApiOperation("数据库初始化2条Customer数据")
@RequestMapping(value = "/customer/add", method = RequestMethod.POST)
public void addCustomer() {
for (int i = 0; i < 2; i++) {
Customer customer = new Customer();
customer.setCname("customer"+i);
customer.setAge(new Random().nextInt(30)+1);
customer.setPhone("1381111xxxx");
customer.setSex((byte) new Random().nextInt(2));
customer.setBirth(Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant()));
customerSerivce.addCustomer(customer);
}
}
@ApiOperation("单个用户查询,按customerid查用户信息")
@RequestMapping(value = "/customer/{id}", method = RequestMethod.GET)
public Customer findCustomerById(@PathVariable int id) {
return customerSerivce.findCustomerById(id);
}
@ApiOperation("BloomFilter案例讲解")
@RequestMapping(value = "/customerbloomfilter/{id}", method = RequestMethod.GET)
public Customer findCustomerByIdWithBloomFilter(@PathVariable int id) throws ExecutionException, InterruptedException
{
return customerSerivce.findCustomerByIdWithBloomFilter(id);
}
}
6.6.3.2 测试说明
布隆过滤器有,redis有
布隆过滤器有,redis无
布隆过滤器无,直接返回,不再继续走下去
6.7 布隆过滤器优缺点
6.7.1 优点
高效地插入和查询,内存占用bit空间少
6.7.2 缺点
不能删除元素。因为删掉元素会导致误判率增加,因为hash冲突同一个位置可能存的东西是多个共有的,你删除一个元素的同时可能也把其它的删除了。
存在误判,不能精准过滤:
有,是很可能有;无,是肯定无,100%无
6.8 布谷鸟过滤器(了解)
为了解决布隆过滤器不能删除元素的问题,布谷鸟过滤器横空出世。
论文《Cuckoo Filter:Better Than Bloom》
作者将布谷鸟过滤器和布隆过滤器进行了深入的对比,有兴趣的同学可以自己看看。不过,按照阳哥企业调研,目前用的比较多比较成熟的就是布隆过滤器,企业暂时没有升级换代的需求,考虑到上课时间有限,在此不再展开。
7 缓存预热+缓存雪崩+缓存击穿+缓存穿透
7.1 先看看大厂真实需求+面试题反馈
缓存预热、雪崩、穿透、击穿分别是什么?你遇到过那几个情况?
缓存预热你是怎么做的?
如何避免或者减少缓存雪崩?
穿透和击穿有什么区别?他两是一个意思还是截然不同?
穿透和击穿你有什么解决方案?如何避免?
假如出现了缓存不一致,你有哪些修补方案?
。。。。。。
7.2 缓存预热
@PostConstruct 初始化白名单数据
7.3 缓存雪崩
7.3.1 发生
redis主机挂了,Redis 全盘崩溃,偏硬件运维
redis中有大量key同时过期大面积失效,偏软件开发
7.3.2 预防+解决
redis中key设置为永不过期 or 过期时间错开
redis缓存集群实现高可用
主从+哨兵
Redis Cluster
开启Redis持久化机制aof/rdb,尽快恢复缓存集群
多缓存结合预防雪崩
ehcache本地缓存 + redis缓存
服务降级
Hystrix或者阿里sentinel限流&降级
人民币玩家
阿里云-云数据库Redis版
7.4 缓存穿透
是什么
请求去查询一条记录,先查redis无,后查mysql无,都查询不到该条记录,但是请求每次都会打到数据库上面去,导致后台数据库压力暴增,这种现象我们称为缓存穿透,这个redis变成了一个摆设。。。。。。
简单说就是:本来无一物,两库都没有。既不在Redis缓存库,也不在mysql,数据库存在被多次暴击风险
解决
7.4.1 一图
7.4.2 方案1:空对象缓存或者缺省值
一般OK
第一种解决方案,回写增强
如果发生了缓存穿透,我们可以针对要查询的数据,在Redis里存一个和业务部门商量后确定的缺省值(比如,零、负数、defaultNull等)。
比如,键uid:abcdxxx,值defaultNull作为案例的key和value
先去redis查键uid:abcdxxx没有,再去mysql查没有获得 ,这就发生了一次穿透现象。
but,可以增强回写机制
mysql也查不到的话也让redis存入刚刚查不到的key并保护mysql。
第一次来查询uid:abcdxxx,redis和mysql都没有,返回null给调用者,但是增强回写后第二次来查uid:abcdxxx,此时redis就有值了。
可以直接从Redis中读取default缺省值返回给业务应用程序,避免了把大量请求发送给mysql处理,打爆mysql。
但是,此方法架不住黑客的恶意攻击,有缺陷......,只能解决key相同的情况
黑客或者恶意攻击:
黑客会对你的系统进行攻击,拿一个不存在的id去查询数据,会产生大量的请求到数据库去查询。可能会导致你的数据库由于压力过大而宕掉
key相同打你系统
第一次打到mysql,空对象缓存后第二次就返回defaultNull缺省值,避免mysql被攻击,不用再到数据库中去走一圈了
key不同打你系统
由于存在空对象缓存和缓存回写(看自己业务不限死),redis中的无关紧要的key也会越写越多(记得设置redis过期时间)
7.4.3 方案2:Google布隆过滤器Guava解决缓存穿透
Guava 中布隆过滤器的实现算是比较权威的,所以实际项目中我们可以直接使用Guava布隆过滤器
Guava's BloomFilter源码出处:
7.4.3.1 案例:白名单过滤器
白名单架构说明
误判问题,但是概率小可以接受,不能从布隆过滤器删除
全部合法的key都需要放入Guava版布隆过滤器+redis里面,不然数据就是返回null
Coding实战
建Module
修改redis7_study
改POM
XML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.atguigu.redis7</groupId>
<artifactId>redis7_study</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.10</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.12</junit.version>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.16.18</lombok.version>
</properties>
<dependencies>
<!--guava Google 开源的 Guava 中自带的布隆过滤器-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.3.1</version>
</dependency>
<!--lettuce-->
<!--<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.2.1.RELEASE</version>
</dependency>-->
<!--SpringBoot与Redis整合依赖-->
<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>
<!--swagger2-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--Mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--SpringBoot集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<!--mybatis和springboot整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.2.3</version>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
<version>1.0.2</version>
</dependency>
<!--通用Mapper-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>4.1.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!--通用基础配置junit/devtools/test/log4j/lombok/-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
写YML
XML
server.port=7777
spring.application.name=redis7_study
# ========================logging=====================
logging.level.root=info
logging.level.com.atguigu.redis7=info
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n
logging.file.name=D:/mylogs2023/redis7_study.log
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n
# ========================swagger=====================
spring.swagger2.enabled=true
#在springboot2.6.X结合swagger2.9.X会提示documentationPluginsBootstrapper空指针异常,
#原因是在springboot2.6.X中将SpringMVC默认路径匹配策略从AntPathMatcher更改为PathPatternParser,
# 导致出错,解决办法是matching-strategy切换回之前ant_path_matcher
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
# ========================redis单机=====================
spring.redis.database=0
# 修改为自己真实IP
spring.redis.host=192.168.111.185
spring.redis.port=6379
spring.redis.password=111111
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
# ========================alibaba.druid=====================
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/bigdata?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.druid.test-while-idle=false
# ========================mybatis===================
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.atguigu.redis7.entities
# ========================redis集群=====================
#spring.redis.password=111111
## 获取失败 最大重定向次数
#spring.redis.cluster.max-redirects=3
#spring.redis.lettuce.pool.max-active=8
#spring.redis.lettuce.pool.max-wait=-1ms
#spring.redis.lettuce.pool.max-idle=8
#spring.redis.lettuce.pool.min-idle=0
##支持集群拓扑动态感应刷新,自适应拓扑刷新是否使用所有可用的更新,默认false关闭
#spring.redis.lettuce.cluster.refresh.adaptive=true
##定时刷新
#spring.redis.lettuce.cluster.refresh.period=2000
#spring.redis.cluster.nodes=192.168.111.185:6381,192.168.111.185:6382,192.168.111.172:6383,192.168.111.172:6384,192.168.111.184:6385,192.168.111.184:6386
主启动
java
package com.atguigu.redis7;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import tk.mybatis.spring.annotation.MapperScan;
/**
* @auther zzyy
* @create 2022-12-10 23:39
*/
@SpringBootApplication
@MapperScan("com.atguigu.redis7.mapper") //import tk.mybatis.spring.annotation.MapperScan;
public class Redis7Study7777
{
public static void main(String[] args)
{
SpringApplication.run(Redis7Study7777.class,args);
}
}
业务类
Case01
新建测试案例,hello入门
java
/**
* 创建guava版布隆过滤器,helloworld入门级演示
*/
@Test
public void testGuavaWithBloomFilter()
{
//1 创建guava版布隆过滤器
BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 100);
//2 判断指定的元素是否存在
System.out.println(bloomFilter.mightContain(1));
System.out.println(bloomFilter.mightContain(2));
System.out.println();
//3 讲元素新增进入bloomfilter
bloomFilter.put(1);
bloomFilter.put(2);
System.out.println(bloomFilter.mightContain(1));
System.out.println(bloomFilter.mightContain(2));
}
Case02
GuavaBloomFilterController
java
package com.atguigu.redis7.controller;
import com.atguigu.redis7.service.GuavaBloomFilterService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @auther zzyy
* @create 2022-12-30 16:50
*/
@Api(tags = "google工具Guava处理布隆过滤器")
@RestController
@Slf4j
public class GuavaBloomFilterController
{
@Resource
private GuavaBloomFilterService guavaBloomFilterService;
@ApiOperation("guava布隆过滤器插入100万样本数据并额外10W测试是否存在")
@RequestMapping(value = "/guavafilter",method = RequestMethod.GET)
public void guavaBloomFilter()
{
guavaBloomFilterService.guavaBloomFilter();
}
}
GuavaBloomFilterService
java
package com.atguigu.redis7.service;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* @auther zzyy
* @create 2022-12-30 16:50
*/
@Service
@Slf4j
public class GuavaBloomFilterService
{
//1 定义一个常量
public static final int _1W = 10000;
//2 定义我们guava布隆过滤器,初始容量
public static final int SIZE = 100 * _1W;
//3 误判率,它越小误判的个数也就越少(思考,是否可以是无限小??没有误判岂不是更好)
public static double fpp = 0.01;//0.01 0.000000000000001
//4 创建guava布隆过滤器
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), SIZE,fpp);
public void guavaBloomFilter()
{
//1 先让bloomFilter加入100W白名单数据
for (int i = 1; i <= SIZE ; i++) {
bloomFilter.put(i);
}
//2 故意取10W个不在合法范围内的数据,来进行误判率的演示
ArrayList<Integer> list = new ArrayList<>(10 * _1W);
//3 验证
for (int i = SIZE+1; i <= SIZE+(10 * _1W) ; i++)
{
if(bloomFilter.mightContain(i))
{
log.info("被误判了:{}",i);
list.add(i);
}
}
log.info("误判总数量:{}",list.size());
}
}
取样本100W数据,查查不在100W范围内,其它10W数据是否存在
上一步结论:
现在总共有10万数据是不存在的,误判了3033次,
原始样本:100W
不存在数据:1000001W---1100000W
我们计算下误判率:===========》
debug源码分析下,看看hash函数
误判率fpp越小,要求的精度越高,需要用到的资源也越多。坑位数(比特位)越来越多,hash函数数量越来越多
布隆过滤器说明
7.4.3.2 家庭作业思考题:黑名单使用
7.5 缓存击穿
7.5.1 是什么
大量的请求同时查询一个 key 时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去
简单说就是热点key突然失效了,暴打mysql
备注:穿透和击穿,截然不同
7.5.2 危害
会造成某一时刻数据库请求量过大,压力剧增。
一般技术部门需要知道热点key是哪些个?做到心里有数防止击穿
7.5.3 解决
热点key失效
时间到了自然清除但还被访问到
delete掉的key,刚巧又被访问
方案1:差异失效时间,对于访问频繁的热点key,干脆就不设置过期时间
方案2:互斥跟新,采用双检加锁策略
多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它。
其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存
7.5.4 案例
天猫聚划算功能实现+防止缓存击穿
模拟高并发的天猫聚划算案例code:
是什么
生产案例网址:
问题,热点key突然失效导致了缓存击穿
技术方案实现:
分析过程
|----|--------------------------------------------------|
| 步骤 | 说明 |
| 1 | 100%高并发,绝对不可以用mysql实现 |
| 2 | 先把mysql里面参加活动的数据抽取进redis,一般采用定时器扫描来决定上线活动还是下线取消。 |
| 3 | 支持分页功能,一页20条记录 |
| | 请大家思考,redis里面什么样子的数据类型支持上述功能? |
高并发+定时任务+分页显示。。。。
redis数据类型选型 list
springboot+redis实现高并发的聚划算业务V2
建Module
修改redis7_study
改POM
无
写YML
无
主启动
无
业务类
entity
java
package com.atguigu.redis7.entities;
import io.swagger.annotations.ApiModel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @auther zzyy
* @create 2022-12-31 14:24
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "聚划算活动producet信息")
public class Product
{
//产品ID
private Long id;
//产品名称
private String name;
//产品价格
private Integer price;
//产品详情
private String detail;
}
JHSTaskService
采用定时器将参与聚划算活动的特价商品新增进入redis中
java
package com.atguigu.redis7.service;
import cn.hutool.core.date.DateUtil;
import com.atguigu.redis7.entities.Product;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2022-12-31 14:26
*/
@Service
@Slf4j
public class JHSTaskService
{
public static final String JHS_KEY="jhs";
public static final String JHS_KEY_A="jhs:a";
public static final String JHS_KEY_B="jhs:b";
@Autowired
private RedisTemplate redisTemplate;
/**
* 偷个懒不加mybatis了,模拟从数据库读取100件特价商品,用于加载到聚划算的页面中
* @return
*/
private List<Product> getProductsFromMysql() {
List<Product> list=new ArrayList<>();
for (int i = 1; i <=20; i++) {
Random rand = new Random();
int id= rand.nextInt(10000);
Product obj=new Product((long) id,"product"+i,i,"detail");
list.add(obj);
}
return list;
}
//@PostConstruct
public void initJHS()
{
log.info("启动定时器天猫聚划算功能模拟开始......,O(∩_∩)O哈哈~");
//1 用线程模拟定时任务,后台任务定时将mysql里面的参加活动的商品刷新到redis里
new Thread(() -> {
while (true)
{
//2 模拟从mysql查出数据,用于加载到redis并给聚划算页面显示
List<Product> list = this.getProductsFromMysql();
//3 采用redis list数据结构的lpush命令来实现存储
redisTemplate.delete(JHS_KEY);
//4 加入最新的数据给redis参加活动
redisTemplate.opsForList().leftPushAll(JHS_KEY,list);
//5 暂停1分钟线程,间隔一分钟执行一次,模拟聚划算一天执行的参加活动的品牌
try { TimeUnit.MINUTES.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
}
},"t1").start();
}
}
JHSProductController
java
package com.atguigu.redis7.controller;
import com.atguigu.redis7.entities.Product;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @auther zzyy
* @create 2022-12-31 14:29
*/
@RestController
@Slf4j
@Api(tags = "聚划算商品列表接口")
public class JHSProductController
{
public static final String JHS_KEY="jhs";
public static final String JHS_KEY_A="jhs:a";
public static final String JHS_KEY_B="jhs:b";
@Autowired
private RedisTemplate redisTemplate;
/**
* 分页查询:在高并发的情况下,只能走redis查询,走db的话必定会把db打垮
* @param page
* @param size
* @return
*/
@RequestMapping(value = "/pruduct/find",method = RequestMethod.GET)
@ApiOperation("聚划算案例,每次1页每页5条显示")
public List<Product> find(int page, int size) {
List<Product> list=null;
long start = (page - 1) * size;
long end = start + size - 1;
try
{
// 采用redis list结构里面的lrang命令来实现加载和分页查询
list = redisTemplate.opsForList().range(JHS_KEY,start,end);
if(CollectionUtils.isEmpty(list))
{
//TODO 走mysql查询
}
log.info("参加活动的商家:{}",list);
}catch (Exception e){
// 出异常了,一般redis宕机了或者redis网络抖动导致timeout
log.error("jhs exception:{}",e);
e.printStackTrace();
// ....再次查询mysql
}
return list;
}
}
备注
至此步骤,上述聚划算的功能算是完成,请思考在高并发下有什么经典生产问题?
Bug和隐患说明
热点key突然失效导致可怕的缓存击穿
delete命令执行的一瞬间有空隙,其它请求线程继续找Redis为null
打到了mysql,暴击......
复习again
最终目的
2条命令原子性还是其次,主要是防止热key突然失效暴击mysql打爆系统
进一步升级加固案例
复习,互斥跟新,采用双检加锁策略
多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它。
其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。
差异失效时间
JHSTaskService
java
package com.atguigu.redis7.service;
import cn.hutool.core.date.DateUtil;
import com.atguigu.redis7.entities.Product;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2022-12-31 14:26
*/
@Service
@Slf4j
public class JHSTaskService
{
public static final String JHS_KEY="jhs";
public static final String JHS_KEY_A="jhs:a";
public static final String JHS_KEY_B="jhs:b";
@Autowired
private RedisTemplate redisTemplate;
/**
* 偷个懒不加mybatis了,模拟从数据库读取20件特价商品,用于加载到聚划算的页面中
* @return
*/
private List<Product> getProductsFromMysql() {
List<Product> list=new ArrayList<>();
for (int i = 1; i <=20; i++) {
Random rand = new Random();
int id= rand.nextInt(10000);
Product obj=new Product((long) id,"product"+i,i,"detail");
list.add(obj);
}
return list;
}
//@PostConstruct
public void initJHS()
{
log.info("启动定时器天猫聚划算功能模拟开始......,O(∩_∩)O哈哈~");
//1 用线程模拟定时任务,后台任务定时将mysql里面的参加活动的商品刷新到redis里
new Thread(() -> {
while (true)
{
//2 模拟从mysql查出数据,用于加载到redis并给聚划算页面显示
List<Product> list = this.getProductsFromMysql();
//3 采用redis list数据结构的lpush命令来实现存储
redisTemplate.delete(JHS_KEY);
//4 加入最新的数据给redis参加活动
redisTemplate.opsForList().leftPushAll(JHS_KEY,list);
//5 暂停1分钟线程,间隔一分钟执行一次,模拟聚划算一天执行的参加活动的品牌
try { TimeUnit.MINUTES.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
}
},"t1").start();
}
@PostConstruct
public void initJHSAB(){
log.info("启动AB定时器计划任务天猫聚划算功能模拟.........."+DateUtil.now());
//1 用线程模拟定时任务,后台任务定时将mysql里面的参加活动的商品刷新到redis里
new Thread(() -> {
while (true)
{
//2 模拟从mysql查出数据,用于加载到redis并给聚划算页面显示
List<Product> list = this.getProductsFromMysql();
//3 先更新B缓存且让B缓存过期时间超过A缓存,如果A突然失效了还有B兜底,防止击穿
redisTemplate.delete(JHS_KEY_B);
redisTemplate.opsForList().leftPushAll(JHS_KEY_B,list);
redisTemplate.expire(JHS_KEY_B,86410L,TimeUnit.SECONDS);
//4 再更新A缓存
redisTemplate.delete(JHS_KEY_A);
redisTemplate.opsForList().leftPushAll(JHS_KEY_A,list);
redisTemplate.expire(JHS_KEY_A,86400L,TimeUnit.SECONDS);
//5 暂停1分钟线程,间隔一分钟执行一次,模拟聚划算一天执行的参加活动的品牌
try { TimeUnit.MINUTES.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
}
},"t1").start();
}
}
JHSProductController
java
package com.atguigu.redis7.controller;
import com.atguigu.redis7.entities.Product;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @auther zzyy
* @create 2022-12-31 14:29
*/
@RestController
@Slf4j
@Api(tags = "聚划算商品列表接口")
public class JHSProductController
{
public static final String JHS_KEY="jhs";
public static final String JHS_KEY_A="jhs:a";
public static final String JHS_KEY_B="jhs:b";
@Autowired
private RedisTemplate redisTemplate;
/**
* 分页查询:在高并发的情况下,只能走redis查询,走db的话必定会把db打垮
* @param page
* @param size
* @return
*/
@RequestMapping(value = "/pruduct/find",method = RequestMethod.GET)
@ApiOperation("聚划算案例,每次1页每页5条显示")
public List<Product> find(int page, int size) {
List<Product> list=null;
long start = (page - 1) * size;
long end = start + size - 1;
try
{
// 采用redis list结构里面的lrang命令来实现加载和分页查询
list = redisTemplate.opsForList().range(JHS_KEY,start,end);
if(CollectionUtils.isEmpty(list))
{
//TODO 走mysql查询
}
log.info("参加活动的商家:{}",list);
}catch (Exception e){
// 出异常了,一般redis宕机了或者redis网络抖动导致timeout
log.error("jhs exception:{}",e);
e.printStackTrace();
// ....再次查询mysql
}
return list;
}
@RequestMapping(value = "/pruduct/findab",method = RequestMethod.GET)
@ApiOperation("AB双缓存架构,防止热点key突然失效")
public List<Product> findAB(int page, int size) {
List<Product> list=null;
long start = (page - 1) * size;
long end = start + size - 1;
try
{
list = redisTemplate.opsForList().range(JHS_KEY_A,start,end);
if(CollectionUtils.isEmpty(list))
{
log.info("---A缓存已经过期失效或活动结束了,记得人工修改,B缓存继续顶着");
list = redisTemplate.opsForList().range(JHS_KEY_B,start,end);
if(CollectionUtils.isEmpty(list))
{
//TODO 走mysql查询
}
}
}catch (Exception e){
// 出异常了,一般redis宕机了或者redis网络抖动导致timeout
log.error("jhs exception:{}",e);
e.printStackTrace();
// ....再次查询mysql
}
return list;
}
}
7.6 总结
8 手写Redis分布式锁 ★★★★★
8.1 粉丝反馈回来的题目
Redis除了拿来做缓存,你还见过基于Redis的什么用法?
Redis 做分布式锁的时候有需要注意的问题?
你们公司自己实现的分布式锁是否用的setnx命令实现?这个是最合适的吗?你如何考虑分布式锁的可重入问题?
如果是 Redis 是单点部署的,会带来什么问题?那你准备怎么解决单点问题呢?
Redis集群模式下,比如主从模式,CAP方面有没有什么问题呢?
那你简单的介绍一下 Redlock 吧?你简历上写redisson,你谈谈
Redis分布式锁如何续期?看门狗知道吗?
。。。。。。
8.2 锁的种类
单机版同一个JVM虚拟机内,synchronized或者Lock接口
分布式多个不同JVM虚拟机,单机的线程锁机制不再起作用,资源类在不同的服务器之间共享了。
8.3 一个靠谱分布式锁需要具备的条件和刚需
独占性
OnlyOne,任何时刻只能有且仅有一个线程持有
高可用
若redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况
高并发请求下,依旧性能OK好使
防死锁
杜绝死锁,必须有超时控制机制或者撤销操作,有个兜底终止跳出方案
不乱抢
防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放,自己约的锁含着泪也要自己解
重入性
同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁。
8.4 分布式锁
setnx key value
差评,setnx+expire不安全,两条命令非原子性的
set key value [EX seconds] [PX milliseconds] [NX|XX]
8.5 重点
JUC中AQS锁的规范落地参考+可重入锁考虑+Lua脚本+Redis命令一步步实现分布式锁
8.6 Base案例(boot+redis)
使用场景:
多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)
建Module
redis_distributed_lock2
redis_distributed_lock3
改POM
XML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.atguigu.redislock</groupId>
<artifactId>redis_distributed_lock2</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.12</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<lombok.version>1.16.18</lombok.version>
</properties>
<dependencies>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--SpringBoot与Redis整合依赖-->
<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>
<!--swagger2-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--通用基础配置boottest/lombok/hutool-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.8</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
写YML
XML
server.port=7777
spring.application.name=redis_distributed_lock
# ========================swagger2=====================
# http://localhost:7777/swagger-ui.html
swagger2.enabled=true
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
# ========================redis单机=====================
spring.redis.database=0
spring.redis.host=192.168.111.185
spring.redis.port=6379
spring.redis.password=111111
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
主启动
java
package com.atguigu.redislock;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @auther zzyy
* @create 2022-10-12 22:20
*/
@SpringBootApplication
public class RedisDistributedLockApp7777
{
public static void main(String[] args)
{
SpringApplication.run(RedisDistributedLockApp7777.class,args);
}
}
业务类
Swagger2Config
java
package com.atguigu.redislock.config;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* @auther zzyy
* @create 2022-10-12 21:55
*/
@Configuration
@EnableSwagger2
public class Swagger2Config
{
@Value("${swagger2.enabled}")
private Boolean enabled;
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(enabled)
.select()
.apis(RequestHandlerSelectors.basePackage("com.atguigu.redislock")) //你自己的package
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("springboot利用swagger2构建api接口文档 "+"\t"+ DateTimeFormatter.ofPattern("yyyy-MM-dd").format(LocalDateTime.now()))
.description("springboot+redis整合")
.version("1.0")
.termsOfServiceUrl("https://www.baidu.com/")
.build();
}
}
RedisConfig
java
package com.atguigu.redislock.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @auther zzyy
* @create 2022-07-02 11:25
*/
@Configuration
public class RedisConfig
{
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory)
{
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
//设置key序列化方式string
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置value的序列化方式json
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
InventoryService
java
package com.atguigu.redislock.service;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @auther zzyy
* @create 2022-10-22 15:14
*/
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale()
{
String retMessage = "";
lock.lock();
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
System.out.println(retMessage);
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}finally {
lock.unlock();
}
return retMessage+"\t"+"服务端口号:"+port;
}
}
InventoryController
java
package com.atguigu.redislock.controller;
import cn.hutool.core.util.IdUtil;
import com.atguigu.redislock.service.InventoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @auther zzyy
* @create 2022-10-12 17:05
*/
@RestController
@Api(tags = "redis分布式锁测试")
public class InventoryController
{
@Autowired
private InventoryService inventoryService;
@ApiOperation("扣减库存,一次卖一个")
@GetMapping(value = "/inventory/sale")
public String sale()
{
return inventoryService.sale();
}
}
丝袜哥
http://localhost:7777/swagger-ui.html#/
8.7 手写分布式锁思路分析2023
大家来找茬
上面测试通过求吐槽
8.7.1 初始化版本简单添加
业务类
InventoryService
请将7777的业务逻辑代码原样拷贝到8888
加了synchronized或者Lock
8.7.2 nginx分布式微服务架构
问题
8.7.2.1 V2.0版本代码分布式部署后,单机锁还是出现超卖现象,需要分布式锁
8.7.2.2 Nginx配置负载均衡
命令地址+配置地址
命令地址 /usr/local/nginx/sbin
配置地址 /usr/local/nginx/conf
启动
/usr/local/nginx/sbin ./nginx
启动Nginx并测试通过,浏览器看到nginx欢迎welcome页面
/usr/local/nginx/conf目录下修改配置文件nginx.conf 新增反向代理和负载均衡配置
关闭
/usr/local/nginx/sbin ./nginx -s stop
指定配置启动
在/usr/local/nginx/sbin路径下执行下面的命令
./nginx -c /usr/local/nginx/conf/nginx.conf
重启
/usr/local/nginx/sbin ./nginx -s reload
8.7.2.3 V2.0版本代码修改+启动两个微服务
V2.0,单机版加锁配合Nginx和Jmeter压测后,不满足高并发分布式锁的性能要求,出现超卖
7777
InventoryService
java
package com.atguigu.redislock.service;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @auther zzyy
* @create 2022-10-22 15:14
*/
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale()
{
String retMessage = "";
lock.lock();
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
System.out.println(retMessage);
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}finally {
lock.unlock();
}
return retMessage+"\t"+"服务端口号:"+port;
}
}
8888
java
package com.atguigu.redislock.service;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @auther zzyy
* @create 2022-10-22 15:14
*/
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale()
{
String retMessage = "";
lock.lock();
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
System.out.println(retMessage);
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}finally {
lock.unlock();
}
return retMessage+"\t"+"服务端口号:"+port;
}
}
通过Nginx访问,你的Linux服务器地址IP,反向代理+负载均衡
可以点击看到效果,一边一个,默认轮询
http://192.168.111.185/inventory/sale
8.7.2.4 上面纯手点验证OK,下面高并发模拟
线程组redis
100个商品足够了
http请求
jmeter压测
76号商品被卖出2次,出现超卖故障现象
8.7.2.5 bug-why
为什么加了synchronized或者Lock还是没有控制住?
解释
在单机环境下,可以使用synchronized或Lock来实现。
但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),所以需要一个让所有进程都能访问到的锁来实现(比如redis或者zookeeper来构建)
不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程
8.7.2.6 分布式锁出现
能干嘛
跨进程+跨服务
解决超卖
防止缓存击穿
解决
上redis分布式锁setnx
8.7.3 redis分布式锁
8.7.3.1 修改为3.1版
通过递归重试的方式
问题:
测试手工OK,测试Jmeter压测5000 OK
递归是一种思想没错,但是容易导致StackOverflowError,不太推荐,进一步完善
java
package com.atguigu.redislock.service;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @auther zzyy
* @create 2022-10-22 15:14
*/
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
// V3.1,递归重试,容易导致stackoverflowerror,所以不太推荐;另外,高并发唤醒后推荐用while判断而不是if
public String sale()
{
String retMessage = "";
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue);
//flag=false,抢不到的线程要继续重试。。。。。。
if(!flag)
{
//暂停20毫秒,进行递归重试.....
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
sale();
}else{
//抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存,每次减少一个
if(inventoryNumber > 0)
{
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余:"+inventoryNumber;
System.out.println(retMessage+"\t"+"服务端口号"+port);
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}finally {
stringRedisTemplate.delete(key);
}
}
return retMessage+"\t"+"服务端口号"+port;
}
}
8.7.3.2 修改为3.2版
多线程判断想想JUC里面说过的虚假唤醒,用while替代if
用自旋替代递归重试
java
package com.atguigu.redislock.service;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @auther zzyy
* @create 2022-10-22 15:14
*/
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
/**
* V3.2,存在的问题
* 部署了微服务的Java程序机器挂了,代码层面根本没有走到finally这块,
* 没办法保证解锁(无过期时间该key一直存在),这个key没有被删除,需要加入一个过期时间限定key
* @return
*/
public String sale()
{
String retMessage = "";
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
//不用递归了,高并发下容易出错,我们用自旋替代递归方法重试调用;也不用if了,用while来替代
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue))
{
//暂停20毫秒,进行递归重试.....
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
//抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存,每次减少一个
if(inventoryNumber > 0)
{
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余:"+inventoryNumber;
System.out.println(retMessage+"\t"+"服务端口号"+port);
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}finally {
stringRedisTemplate.delete(key);
}
return retMessage+"\t"+"服务端口号"+port;
}
}
8.7.4 宕机与过期+防止死锁
当前代码为3.2版接上一步
8.7.4.1 问题
部署了微服务的Java程序机器挂了,代码层面根本没有走到finally这块,没办法保证解锁(无过期时间该key一直存在),这个key没有被删除,需要加入一个过期时间限定key
8.7.4.2 解决
修改为4.1版(增加过期时间)
java
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue))
{
//暂停20毫秒,进行递归重试.....
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
stringRedisTemplate.expire(key,30L,TimeUnit.SECONDS);
4.1版本结论
设置key+过期时间分开了,必须要合并成一行具备原子性
修改为4.2版
java
package com.atguigu.redislock.service;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @auther zzyy
* @create 2022-10-22 15:14
*/
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
/*
V4.0,存在问题:stringRedisTemplate.delete(key);
只能自己删除自己的锁,不可以删除别人的,需要添加判断是否是自己的锁来进行操作*/
public String sale()
{
String retMessage = "";
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
//改进点:加锁和过期时间设置必须同一行,保证原子性
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS))
{
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
System.out.println(retMessage);
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}finally {
stringRedisTemplate.delete(key);
}
return retMessage+"\t"+"服务端口号:"+port;
}
}
Jmeter压测OK
4.2版本结论
加锁和过期时间设置必须同一行,保证原子性
8.7.5 防止误删key的问题
当前代码为4.2版接上一步
8.7.5.1 问题
实际业务处理时间如果超过了默认设置key的过期时间??尴尬 ̄□ ̄||
张冠李戴,删除了别人的锁
8.7.5.2 解决
只能自己删除自己的,不许动别人的
修改为5.0版
java
package com.atguigu.redislock.service;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @auther zzyy
* @create 2022-10-22 15:14
*/
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
//V5.0 ,存在问题就是最后的判断+del不是一行原子命令操作,需要用lua脚本进行修改
public String sale()
{
String retMessage = "";
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
while(!stringRedisTemplate.opsForValue().setIfAbsent(key,uuidValue,30L,TimeUnit.SECONDS))
{
//暂停20毫秒,进行递归重试.....
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
//抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存,每次减少一个
if(inventoryNumber > 0)
{
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余:"+inventoryNumber;
System.out.println(retMessage+"\t"+"服务端口号"+port);
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}finally {
//改进点,只能删除属于自己的key,不能删除别人的
// v5.0判断加锁与解锁是不是同一个客户端,同一个才行,自己只能删除自己的锁,不误删他人的
if(stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue))
{
stringRedisTemplate.delete(key);
}
}
return retMessage+"\t"+"服务端口号"+port;
}
}
8.7.6 Lua保证原子性
8.7.6.1 当前代码为5.0版接上一步
java
package com.atguigu.redislock.service;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @auther zzyy
* @create 2022-10-22 15:14
*/
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale()
{
String retMessage = "";
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS))
{
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t"+uuidValue;
System.out.println(retMessage);
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}finally {
// v5.0判断加锁与解锁是不是同一个客户端,同一个才行,自己只能删除自己的锁,不误删他人的
if(stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue))
{
stringRedisTemplate.delete(key);
}
}
return retMessage+"\t"+"服务端口号:"+port;
}
}
8.7.6.2 问题
finally块的判断+del删除操作不是原子性的
8.7.6.3 启用lua脚本编写redis分布式锁判断+删除判断代码
Lua脚本
官网
Docshttps://redis.io/docs/reference/patterns/distributed-locks/
官方脚本
8.7.6.4 Lua脚本浅谈
Lua脚本初识
Redis调用Lua脚本通过eval命令保证代码执行的原子性,直接用return返回脚本执行后的结果值
eval luascript numkeys [key [key ...]] [arg [arg ...]]
helloworld入门
1-hello lua
2-set k1 v1 get k1
3-mset
Lua脚本进一步
Redis分布式锁lua脚本官网练习
|-------------------------------------------------------------------------------------------------------------------------------------------|
| eval "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 zzyyRedisLock 1111-2222-3333 |
条件判断语法
条件判断案例
8.7.6.5 解决
修改为6.0版code
java
package com.atguigu.redislock.service;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @auther zzyy
* @create 2022-10-22 15:14
*/
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale()
{
String retMessage = "";
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS))
{
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t"+uuidValue;
System.out.println(retMessage);
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}finally {
//改进点,修改为Lua脚本的redis分布式锁调用,必须保证原子性,参考官网脚本案例
//V6.0 将判断+删除自己的合并为lua脚本保证原子性
String luaScript =
"if (redis.call('get',KEYS[1]) == ARGV[1]) then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
stringRedisTemplate.execute(new DefaultRedisScript<>(luaScript, Boolean.class), Arrays.asList(key), uuidValue);
}
return retMessage+"\t"+"服务端口号:"+port;
}
}
bug说明
|--------------------------------------------------------------------------------------------------------------------------------------|
| stringRedisTemplate.execute( newDefaultRedisScript<>(script), Arrays. asList(key),value); |
| stringRedisTemplate.execute( newDefaultRedisScript<>(script, Long.class), Arrays. asList(key),value); //使用该构造方法,不然报错 |
8.7.7 可重入锁+设计模式
8.7.7.1 当前代码为6.0版接上一步
while判断并自旋重试获取锁+setnx含自然过期时间+Lua脚本官网删除锁命令
java
//V6.0 ,不满足可重入性,需要重新修改为V7.0
public String sale()
{
String retMessage = "";
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
while(!stringRedisTemplate.opsForValue().setIfAbsent(key,uuidValue,30L,TimeUnit.SECONDS))
{
//暂停20毫秒,进行递归重试.....
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
//redislock();
//抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存,每次减少一个
if(inventoryNumber > 0)
{
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余:"+inventoryNumber;
System.out.println(retMessage+"\t"+"服务端口号"+port);
testReEnter();
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}finally {
//unredislock();
//改进点,修改为Lua脚本的redis分布式锁调用,必须保证原子性,参考官网脚本案例
String luaScript =
"if redis.call('get',KEYS[1]) == ARGV[1] then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
stringRedisTemplate.execute(new DefaultRedisScript(luaScript,Boolean.class), Arrays.asList(key),uuidValue);
}
return retMessage+"\t"+"服务端口号"+port;
}
private void testReEnter()
{
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
while(!stringRedisTemplate.opsForValue().setIfAbsent(key,uuidValue,30L,TimeUnit.SECONDS))
{
//暂停20毫秒,进行递归重试.....
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
redislock();
//biz......
unredislock();
//改进点,修改为Lua脚本的redis分布式锁调用,必须保证原子性,参考官网脚本案例
String luaScript =
"if redis.call('get',KEYS[1]) == ARGV[1] then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
stringRedisTemplate.execute(new DefaultRedisScript(luaScript,Boolean.class), Arrays.asList(key),uuidValue);
}
问题
如何兼顾锁的可重入性问题?
复习写好一个锁的条件和规约
8.7.7.2 可重入锁(又名递归锁)
说明
可重入锁又名递归锁
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
"可重入锁"这四个字分开来解释:
JUC知识复习,可重入锁出bug会如何影响程序
可重入锁种类
隐式锁(即synchronized关键字使用的锁)默认是可重入锁
指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。
同步块
java
package com.atguigu.juc.senior.prepare;
/**
* @auther zzyy
* @create 2020-05-14 11:59
*/
public class ReEntryLockDemo
{
public static void main(String[] args)
{
final Object objectLockA = new Object();
new Thread(() -> {
synchronized (objectLockA)
{
System.out.println("-----外层调用");
synchronized (objectLockA)
{
System.out.println("-----中层调用");
synchronized (objectLockA)
{
System.out.println("-----内层调用");
}
}
}
},"a").start();
}
}
同步方法
java
package com.atguigu.juc.senior.prepare;
/**
* @auther zzyy
* @create 2020-05-14 11:59
* 在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的
*/
public class ReEntryLockDemo
{
public synchronized void m1()
{
System.out.println("-----m1");
m2();
}
public synchronized void m2()
{
System.out.println("-----m2");
m3();
}
public synchronized void m3()
{
System.out.println("-----m3");
}
public static void main(String[] args)
{
ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();
reEntryLockDemo.m1();
}
}
Synchronized的重入的实现机理
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
显式锁(即Lock)也有ReentrantLock这样的可重入锁。
java
package com.atguigu.juc.senior.prepare;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @auther zzyy
* @create 2020-05-14 11:59
* 在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的
*/
public class ReEntryLockDemo
{
static Lock lock = new ReentrantLock();
public static void main(String[] args)
{
new Thread(() -> {
lock.lock();
try
{
System.out.println("----外层调用lock");
lock.lock();
try
{
System.out.println("----内层调用lock");
}finally {
// 这里故意注释,实现加锁次数和释放次数不一样
// 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
lock.unlock(); // 正常情况,加锁几次就要解锁几次
}
}finally {
lock.unlock();
}
},"a").start();
new Thread(() -> {
lock.lock();
try
{
System.out.println("b thread----外层调用lock");
}finally {
lock.unlock();
}
},"b").start();
}
}
8.7.7.3 lock/unlock配合可重入锁进行AQS源码分析讲解
切记,一般而言,你lock了几次就要unlock几次
8.7.7.4 思考,上述可重入锁计数问题,redis中那个数据类型可以代替
K,K,V
hset zzyyRedisLock 29f0ee01ac77414fb8b0861271902a94:1
Map<String,Map<Object,Object>>
案例命令
|---------------------------------------------------------|
| hset key field value |
| hset redis锁名字(zzyyRedisLock) 某个请求线程的UUID+ThreadID 加锁的次数 |
小总结
setnx,只能解决有无的问题
够用但是不完美
hset,不但解决有无,还解决可重入问题
8.7.7.5 思考+设计重点(一横一纵)
目前有2条支线,目的是保证同一个时候只能有一个线程持有锁进去redis做扣减库存动作
2个分支
1 保证加锁/解锁,lock/unlock
2 扣减库存redis命令的原子性
8.7.7.6 lua脚本
redis命令过程分析
加锁lua脚本lock
先判断redis分布式锁这个key是否存在
EXISTS key
返回0说明不存在,hset新建当前线程属于自己的锁BY UUID:ThreadID
HSET zzyyRedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 1
命令 key value = UUID:ThreadID 次数
返回1说明已经有锁,需进一步判断是不是当前线程自己的
HEXISTS key uuid:ThreadID
HEXISTS zzyyRedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1
返回0说明不是自己的
返回1说明是自己的锁,自增1次表示重入
HINCRBY key field increment
HINCRBY zzyyRedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 1
上述设计修改为Lua脚本
V1 //加锁的Lua脚本,对标我们的lock方法
Lua
if redis.call('exists','key') == 0 then
redis.call('hset','key','uuid:threadid',1)
redis.call('expire','key',30)
return 1
elseif redis.call('hexists','key','uuid:threadid') == 1 then
redis.call('hincrby','key','uuid:threadid',1)
redis.call('expire','key',30)
return 1
else
return 0
end
|-------------------------------------|
| 相同部分是否可以替换处理??? hincrby命令可否替代hset命令 |
**V2 //**合并相同的代码,用hincrby替代hset,精简代码
Lua
if redis.call('exists','key') == 0 or redis.call('hexists','key','uuid:threadid') == 1 then
redis.call('hincrby','key','uuid:threadid',1)
redis.call('expire','key',30)
return 1
else
return 0
end
**V3 //**脚本OK了,换上参数来替代
|-------|-----------|------------------------------------|
| key | KEYS[1] | zzyyRedisLock |
| value | ARGV[1] | 2f586ae740a94736894ab9d51880ed9d:1 |
| 过期时间值 | ARGV[2] | 30 秒 |
Lua
if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then
redis.call('hincrby',KEYS[1],ARGV[1],1)
redis.call('expire',KEYS[1],ARGV[2])
return 1
else
return 0
end
测试
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| EVAL "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end" 1 zzyyRedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 30 |
| HGET zzyyRedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 |
| |
解锁lua脚本unlock
设计思路:有锁且还是自己的锁
HEXISTS key uuid:ThreadID
HEXISTS zzyyRedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1
返回零,说明根本没有锁,程序块返回nil
不是零,说明有锁且是自己的锁,直接调用HINCRBY 负一 表示每次减个一,解锁一次。直到它变为零表示可以删除该锁Key,del 锁key
全套流程
上述设计修改为Lua脚本
**V1 //**解锁的Lua脚本,对标我们的unlock方法
Lua
if redis.call('HEXISTS',lock,uuid:threadID) == 0 then
return nil
elseif redis.call('HINCRBY',lock,uuid:threadID,-1) == 0 then
return redis.call('del',lock)
else
return 0
end
**V2 //**脚本OK了,换上参数来替代
Lua
if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then
return nil
elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then
return redis.call('del',KEYS[1])
else
return 0
end
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| eval "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then return nil elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then return redis.call('del',KEYS[1]) else return 0 end" 1 zzyyRedisLock 2f586ae740a94736894ab9d51880ed9d:1 |
测试全套流程
8.7.7.7 将上述lua脚本整合进入微服务Java程序
复原程序为初始无锁版
java
package com.atguigu.redislock.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
/**
* @auther zzyy
* @create 2022-10-22 15:14
*/
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
public String sale()
{
String retMessage = "";
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t";
System.out.println(retMessage);
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
return retMessage+"\t"+"服务端口号:"+port;
}
}
新建RedisDistributedLock类并实现JUC里面的Lock接口
满足JUC里面AQS对Lock锁的接口规范定义来进行实现落地代码
结合设计模式开发属于自己的Redis分布式锁工具类
lock方法的全盘通用讲解
lua脚本
加锁lock
Lua
if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then
redis.call('hincrby',KEYS[1],ARGV[1],1)
redis.call('expire',KEYS[1],ARGV[2])
return 1
else
return 0
end
解锁unlock
Lua
if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then
return nil
elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then
return redis.call('del',KEYS[1])
else
return 0
end
工厂设计模式引入
通过实现JUC里面的Lock接口,实现Redis分布式锁RedisDistributedLock
java
package com.atguigu.redislock.mylock;
import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.support.collections.DefaultRedisList;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @auther zzyy
* @create 2022-10-18 18:32
*/
//@Component 引入DistributedLockFactory工厂模式,从工厂获得而不再从spring拿到
public class RedisDistributedLock implements Lock
{
private StringRedisTemplate stringRedisTemplate;
private String lockName;//KEYS[1]
private String uuidValue;//ARGV[1]
private long expireTime;//ARGV[2]
public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName)
{
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();//UUID:ThreadID
this.expireTime = 30L;
}
@Override
public void lock()
{
tryLock();
}
@Override
public boolean tryLock()
{
try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}
return false;
}
/**
* 干活的,实现加锁功能,实现这一个干活的就OK,全盘通用
* @param time
* @param unit
* @return
* @throws InterruptedException
*/
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException{
if(time != -1L){
this.expireTime = unit.toSeconds(time);
}
String script =
"if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
"redis.call('hincrby',KEYS[1],ARGV[1],1) " +
"redis.call('expire',KEYS[1],ARGV[2]) " +
"return 1 " +
"else " +
"return 0 " +
"end";
System.out.println("script: "+script);
System.out.println("lockName: "+lockName);
System.out.println("uuidValue: "+uuidValue);
System.out.println("expireTime: "+expireTime);
while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
TimeUnit.MILLISECONDS.sleep(50);
}
return true;
}
/**
*干活的,实现解锁功能
*/
@Override
public void unlock()
{
String script =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
" return nil " +
"elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
" return redis.call('del',KEYS[1]) " +
"else " +
" return 0 " +
"end";
// nil = false 1 = true 0 = false
System.out.println("lockName: "+lockName);
System.out.println("uuidValue: "+uuidValue);
System.out.println("expireTime: "+expireTime);
Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime));
if(flag == null)
{
throw new RuntimeException("This lock doesn't EXIST");
}
}
//===下面的redis分布式锁暂时用不到=======================================
//===下面的redis分布式锁暂时用不到=======================================
//===下面的redis分布式锁暂时用不到=======================================
@Override
public void lockInterruptibly() throws InterruptedException
{
}
@Override
public Condition newCondition()
{
return null;
}
}
InventoryService直接使用上面的代码设计,有什么问题
考虑扩展,本次是redis实现分布式锁,以后zookeeper、mysql实现那??
引入工厂模式改造7.1版code
DistributedLockFactory
java
package com.atguigu.redislock.mylock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.locks.Lock;
/**
* @auther zzyy
* @create 2022-10-18 18:53
*/
@Component
public class DistributedLockFactory
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
private String lockName;
public Lock getDistributedLock(String lockType)
{
if(lockType == null) return null;
if(lockType.equalsIgnoreCase("REDIS")){
lockName = "zzyyRedisLock";
return new RedisDistributedLock(stringRedisTemplate,lockName);
} else if(lockType.equalsIgnoreCase("ZOOKEEPER")){
//TODO zookeeper版本的分布式锁实现
return new ZookeeperDistributedLock();
} else if(lockType.equalsIgnoreCase("MYSQL")){
//TODO mysql版本的分布式锁实现
return null;
}
return null;
}
}
RedisDistributedLock
java
package com.atguigu.redislock.mylock;
import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.support.collections.DefaultRedisList;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @auther zzyy
* @create 2022-10-18 18:32
*/
//@Component 不注入原因:引入DistributedLockFactory工厂模式,从工厂获得而不再从spring拿到
public class RedisDistributedLock implements Lock
{
private StringRedisTemplate stringRedisTemplate;
private String lockName;//KEYS[1]
private String uuidValue;//ARGV[1]
private long expireTime;//ARGV[2]
public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName){
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();//UUID:ThreadID
this.expireTime = 30L;
}
@Override
public void lock(){
tryLock();
}
@Override
public boolean tryLock(){
try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}
return false;
}
/**
* 干活的,实现加锁功能,实现这一个干活的就OK,全盘通用
* @param time
* @param unit
* @return
* @throws InterruptedException
*/
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException{
if(time != -1L){
this.expireTime = unit.toSeconds(time);
}
String script =
"if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
"redis.call('hincrby',KEYS[1],ARGV[1],1) " +
"redis.call('expire',KEYS[1],ARGV[2]) " +
"return 1 " +
"else " +
"return 0 " +
"end";
System.out.println("script: "+script);
System.out.println("lockName: "+lockName);
System.out.println("uuidValue: "+uuidValue);
System.out.println("expireTime: "+expireTime);
while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
TimeUnit.MILLISECONDS.sleep(50);
}
return true;
}
/**
*干活的,实现解锁功能
*/
@Override
public void unlock()
{
String script =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
" return nil " +
"elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
" return redis.call('del',KEYS[1]) " +
"else " +
" return 0 " +
"end";
// nil = false 1 = true 0 = false
System.out.println("lockName: "+lockName);
System.out.println("uuidValue: "+uuidValue);
System.out.println("expireTime: "+expireTime);
Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime));
if(flag == null)
{
throw new RuntimeException("This lock doesn't EXIST");
}
}
//===下面的redis分布式锁暂时用不到=======================================
//===下面的redis分布式锁暂时用不到=======================================
//===下面的redis分布式锁暂时用不到=======================================
@Override
public void lockInterruptibly() throws InterruptedException
{
}
@Override
public Condition newCondition()
{
return null;
}
}
InventoryService使用工厂模式版
java
package com.atguigu.redislock.service;
import ch.qos.logback.core.joran.conditional.ThenAction;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.atguigu.redislock.mylock.DistributedLockFactory;
import com.atguigu.redislock.mylock.RedisDistributedLock;
import lombok.extern.slf4j.Slf4j;
import org.omg.IOP.TAG_RMI_CUSTOM_MAX_STREAM_FORMAT;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @auther zzyy
* @create 2022-10-12 17:04
*/
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
@Autowired
private DistributedLockFactory distributedLockFactory;
public String sale()
{
String retMessage = "";
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0)
{
inventoryNumber = inventoryNumber - 1;
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t服务端口:" +port;
System.out.println(retMessage);
return retMessage;
}
retMessage = "商品卖完了,o(╥﹏╥)o"+"\t服务端口:" +port;
}catch (Exception e){
e.printStackTrace();
}finally {
redisLock.unlock();
}
return retMessage;
}
}
单机+并发测试通过
http://localhost:7777/inventory/sale
8.7.7.8 可重入性测试重点
可重入测试???
InventoryService类新增可重入测试方法
java
package com.atguigu.redislock.service;
import com.atguigu.redislock.mylock.DistributedLockFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.locks.Lock;
/**
* @auther zzyy
* @create 2022-10-30 12:28
*/
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
@Autowired
private DistributedLockFactory distributedLockFactory;
public String sale()
{
String retMessage = "";
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t";
System.out.println(retMessage);
testReEnter();
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}catch (Exception e){
e.printStackTrace();
}finally {
redisLock.unlock();
}
return retMessage+"\t"+"服务端口号:"+port;
}
private void testReEnter()
{
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try
{
System.out.println("################测试可重入锁#######");
}finally {
redisLock.unlock();
}
}
}
/**
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t";
System.out.println(retMessage);
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
*/
http://localhost:7777/inventory/sale
结果
ThreadID一致了但是UUID不OK,o(╥﹏╥)o
引入工厂模式改造7.2版code
DistributedLockFactory
java
package com.atguigu.redislock.mylock;
import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.locks.Lock;
/**
* @auther zzyy
* @create 2022-10-23 22:40
*/
@Component
public class DistributedLockFactory
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
private String lockName;
private String uuidValue;
public DistributedLockFactory()
{
this.uuidValue = IdUtil.simpleUUID();//UUID
}
public Lock getDistributedLock(String lockType)
{
if(lockType == null) return null;
if(lockType.equalsIgnoreCase("REDIS")){
lockName = "zzyyRedisLock";
return new RedisDistributedLock(stringRedisTemplate,lockName,uuidValue);
} else if(lockType.equalsIgnoreCase("ZOOKEEPER")){
//TODO zookeeper版本的分布式锁实现
return new ZookeeperDistributedLock();
} else if(lockType.equalsIgnoreCase("MYSQL")){
//TODO mysql版本的分布式锁实现
return null;
}
return null;
}
}
RedisDistributedLock
java
package com.atguigu.redislock.mylock;
import cn.hutool.core.util.IdUtil;
import lombok.SneakyThrows;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @auther zzyy
* @create 2022-10-23 22:36
*/
public class RedisDistributedLock implements Lock
{
private StringRedisTemplate stringRedisTemplate;
private String lockName;
private String uuidValue;
private long expireTime;
public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName,String uuidValue)
{
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuidValue = uuidValue+":"+Thread.currentThread().getId();
this.expireTime = 30L;
}
@Override
public void lock()
{
this.tryLock();
}
@Override
public boolean tryLock()
{
try
{
return this.tryLock(-1L,TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
{
if(time != -1L)
{
expireTime = unit.toSeconds(time);
}
String script =
"if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
"redis.call('hincrby',KEYS[1],ARGV[1],1) " +
"redis.call('expire',KEYS[1],ARGV[2]) " +
"return 1 " +
"else " +
"return 0 " +
"end";
System.out.println("lockName: "+lockName+"\t"+"uuidValue: "+uuidValue);
while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime)))
{
try { TimeUnit.MILLISECONDS.sleep(60); } catch (InterruptedException e) { e.printStackTrace(); }
}
return true;
}
@Override
public void unlock()
{
String script =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
"return nil " +
"elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
System.out.println("lockName: "+lockName+"\t"+"uuidValue: "+uuidValue);
Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
if(flag == null)
{
throw new RuntimeException("没有这个锁,HEXISTS查询无");
}
}
//=========================================================
@Override
public void lockInterruptibly() throws InterruptedException
{
}
@Override
public Condition newCondition()
{
return null;
}
}
InventoryService类新增可重入测试方法
java
package com.atguigu.redislock.service;
import cn.hutool.core.util.IdUtil;
import com.atguigu.redislock.mylock.DistributedLockFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @auther zzyy
* @create 2022-10-22 15:14
*/
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
@Autowired
private DistributedLockFactory distributedLockFactory;
public String sale()
{
String retMessage = "";
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
System.out.println(retMessage);
this.testReEnter();
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}catch (Exception e){
e.printStackTrace();
}finally {
redisLock.unlock();
}
return retMessage+"\t"+"服务端口号:"+port;
}
private void testReEnter()
{
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try
{
System.out.println("################测试可重入锁####################################");
}finally {
redisLock.unlock();
}
}
}
单机+并发+可重入性,测试通过
8.7.8 自动续期
8.7.8.1 确保redisLock过期时间大于业务执行时间的问题
Redis分布式锁如何续期?
8.7.8.2 CAP
Redis集群是AP
AP
redis异步复制造成的锁丢失,
比如:主节点没来的及把刚刚set进来这条数据给从节点,master就挂了,从机上位但从机上无该数据
Zookeeper集群是CP
CP
故障
顺便复习,Eureka集群是AP
AP
顺便复习,Nacos集群是AP
8.7.8.3 加个钟,lua脚本
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| hset zzyyRedisLock 111122223333:11 3 |
| EXPIRE zzyyRedisLock 30 |
| ttl zzyyRedisLock |
| 。。。。。 |
| eval "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end" 1 zzyyRedisLock 111122223333:11 30 |
| ttl zzyyRedisLock |
|-----------------------------------------------------------------------------------------------------------------------------------------------|
| //==============自动续期 if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end |
8.7.8.4 8.0版新增自动续期功能
修改为V8.0版程序
del掉之前的lockName zzyyRedisLock
RedisDistributedLock
java
package com.atguigu.redislock.mylock;
import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.support.collections.DefaultRedisList;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @auther zzyy
* @create 2022-10-18 18:32
*/
public class RedisDistributedLock implements Lock
{
private StringRedisTemplate stringRedisTemplate;
private String lockName;//KEYS[1]
private String uuidValue;//ARGV[1]
private long expireTime;//ARGV[2]
public RedisDistributedLock(StringRedisTemplate stringRedisTemplate,String lockName,String uuidValue)
{
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuidValue = uuidValue+":"+Thread.currentThread().getId();
this.expireTime = 30L;
}
@Override
public void lock()
{
tryLock();
}
@Override
public boolean tryLock()
{
try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}
return false;
}
/**
* 干活的,实现加锁功能,实现这一个干活的就OK,全盘通用
* @param time
* @param unit
* @return
* @throws InterruptedException
*/
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
{
if(time != -1L)
{
this.expireTime = unit.toSeconds(time);
}
String script =
"if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
"redis.call('hincrby',KEYS[1],ARGV[1],1) " +
"redis.call('expire',KEYS[1],ARGV[2]) " +
"return 1 " +
"else " +
"return 0 " +
"end";
System.out.println("script: "+script);
System.out.println("lockName: "+lockName);
System.out.println("uuidValue: "+uuidValue);
System.out.println("expireTime: "+expireTime);
while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
TimeUnit.MILLISECONDS.sleep(50);
}
this.renewExpire();
return true;
}
/**
*干活的,实现解锁功能
*/
@Override
public void unlock()
{
String script =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
" return nil " +
"elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
" return redis.call('del',KEYS[1]) " +
"else " +
" return 0 " +
"end";
// nil = false 1 = true 0 = false
System.out.println("lockName: "+lockName);
System.out.println("uuidValue: "+uuidValue);
System.out.println("expireTime: "+expireTime);
Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime));
if(flag == null)
{
throw new RuntimeException("This lock doesn't EXIST");
}
}
private void renewExpire()
{
String script =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " +
"return redis.call('expire',KEYS[1],ARGV[2]) " +
"else " +
"return 0 " +
"end";
new Timer().schedule(new TimerTask()
{
@Override
public void run()
{
if (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
renewExpire();
}
}
},(this.expireTime * 1000)/3);
}
//===下面的redis分布式锁暂时用不到=======================================
//===下面的redis分布式锁暂时用不到=======================================
//===下面的redis分布式锁暂时用不到=======================================
@Override
public void lockInterruptibly() throws InterruptedException
{
}
@Override
public Condition newCondition()
{
return null;
}
}
InventoryService
java
package com.atguigu.redislock.service;
import cn.hutool.core.util.IdUtil;
import com.atguigu.redislock.mylock.DistributedLockFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @auther zzyy
* @create 2022-10-22 15:14
*/
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
@Autowired
private DistributedLockFactory distributedLockFactory;
//V8.0,实现自动续期功能的完善,后台自定义扫描程序,如果规定时间内没有完成业务逻辑,会调用加钟自动续期的脚本
public String sale()
{
String retMessage = "";
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
System.out.println(retMessage);
//暂停几秒钟线程,为了测试自动续期
try { TimeUnit.SECONDS.sleep(120); } catch (InterruptedException e) { e.printStackTrace(); }
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}catch (Exception e){
e.printStackTrace();
}finally {
redisLock.unlock();
}
return retMessage+"\t"+"服务端口号:"+port;
}
private void testReEnter()
{
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try
{
System.out.println("################测试可重入锁####################################");
}finally {
redisLock.unlock();
}
}
}
记得去掉可重入测试testReEnter()
InventoryService业务逻辑里面故意sleep一段时间测试自动续期
8.7.9 总结
synchronized 单机版OK,上分布式死翘翘
nginx分布式微服务 单机锁不行/(ㄒoㄒ)/~~
取消单机锁,上redis分布式锁setnx:
只加了锁,没有释放锁,出异常的话,可能无法释放锁,必须要在代码层面finally释放锁
宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定
为redis的分布式锁key,增加过期时间此外,还必须要setnx+过期时间必须同一行
必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴,1删2,2删3
unlock变为Lua脚本保证
锁重入,hset替代setnx+lock变为Lua脚本保证
自动续期
9 Redlock算法和底层源码分析
9.1 当前代码为8.0版接上一步
9.1.1 自研一把分布式锁,面试中回答的主要考点
9.1.1.1 按照JUC里面java.util.concurrent.locks.Lock接口规范编写
9.1.1.2 lock()加锁关键逻辑
|-----------------------------------------|
| 加锁的Lua脚本,通过redis里面的hash数据模型,加锁和可重入性都要保证 |
| 加锁不成,需要while进行重试并自旋 |
| 自动续期,加个钟 |
加锁
加锁实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间
自旋
续期
9.1.1.3 unlock解锁关键逻辑
|----------------------|
| 考虑可重入性的递减,加锁几次就要减锁几次 |
| 最后到零了,直接del删除 |
将Key键删除。但也不能乱删,不能说客户端1的请求将客户端2的锁给删除掉,只能自己删除自己的锁
上面自研的redis锁对于一般中小公司,不是特别高并发场景足够用了,单机redis小业务也撑得住
9.2 Redis分布式锁-Redlock红锁算法Distributed locks with Redis
9.2.1 官网说明
9.2.2 为什么学习这个?怎么产生的?
之前我们手写的分布式锁有什么缺点?
官网证据
翻译
说人话
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 线程 1 首先获取锁成功,将键值对写入 redis 的 master 节点,在 redis 将该键值对同步到 slave 节点之前,master 发生了故障;redis 触发故障转移,其中一个 slave 升级为新的 master,此时新上位的master并不包含线程1写入的键值对,因此线程 2 尝试获取锁也可以成功拿到锁, 此时相当于有两个线程获取到了锁,可能会导致各种预期之外的情况发生,例如最常见的脏数据。 |
我们加的是排它独占锁,同一时间只能有一个建redis锁成功并持有锁,严禁出现2个以上的请求线程拿到锁。危险的
9.2.3 Redlock算法设计理念
9.2.3.1 redis之父提出了Redlock算法解决这个问题
Redis也提供了Redlock算法,用来实现基于多个实例的分布式锁。
锁变量由多个实例维护,即使有实例发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。
Redlock算法是实现高可靠分布式锁的一种有效解决方案,可以在实际开发中使用。最下方还有笔记
9.2.3.2 设计理念
该方案也是基于(set 加锁、Lua 脚本解锁)进行改良的,所以redis之父antirez 只描述了差异的地方,大致方案如下。
假设我们有N个Redis主节点,例如 N = 5这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统,
为了取到锁客户端执行以下操作:
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 | 获取当前时间,以毫秒为单位; |
| 2 | 依次尝试从5个实例,使用相同的 key 和随机值(例如 UUID)获取锁。当向Redis 请求获取锁时,客户端应该设置一个超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为 10 秒,则超时时间应该在 5-50 毫秒之间。这样可以防止客户端在试图与一个宕机的 Redis 节点对话时长时间处于阻塞状态。如果一个实例不可用,客户端应该尽快尝试去另外一个 Redis 实例请求获取锁; |
| 3 | 客户端通过当前时间减去步骤 1 记录的时间来计算获取锁使用的时间。当且仅当从大多数(N/2+1,这里是 3 个节点)的 Redis 节点都取到锁,并且获取锁使用的时间小于锁失效时间时,锁才算获取成功; |
| 4 | 如果取到了锁,其真正有效时间等于初始有效时间减去获取锁所使用的时间(步骤 3 计算的结果)。 |
| 5 | 如果由于某些原因未能获得锁(无法在至少 N/2 + 1 个 Redis 实例获取锁、或获取锁的时间超过了有效时间),客户端应该在所有的 Redis 实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。 |
该方案为了解决数据不一致的问题,直接舍弃了异步复制只使用 master 节点,同时由于舍弃了 slave,为了保证可用性,引入了 N 个节点,官方建议是 5。阳哥本次教学演示用3台实例来做说明。
客户端只有在满足下面的这两个条件时,才能认为是加锁成功。
条件1:客户端从超过半数(大于等于N/2+1)的Redis实例上成功获取到了锁;
条件2:客户端获取锁的总耗时没有超过锁的有效时间。
9.2.3.3 解决方案
为什么是奇数? N = 2X + 1 (N是最终部署机器数,X是容错机器数)
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 先知道什么是容错 失败了多少个机器实例后我还是可以容忍的,所谓的容忍就是数据一致性还是可以Ok的,CP数据一致性还是可以满足 加入在集群环境中,redis失败1台,可接受。2X+1 = 2 * 1+1 =3,部署3台,死了1个剩下2个可以正常工作,那就部署3台。 加入在集群环境中,redis失败2台,可接受。2X+1 = 2 * 2+1 =5,部署5台,死了2个剩下3个可以正常工作,那就部署5台。 2 为什么是奇数? 最少的机器,最多的产出效果 加入在集群环境中,redis失败1台,可接受。2N+2= 2 * 1+2 =4,部署4台 加入在集群环境中,redis失败2台,可接受。2N+2 = 2 * 2+2 =6,部署6台 |
容错公式
9.2.4 天上飞的理念(RedLock)必然有落地的实现(Redisson)
redisson实现
Redisson是java的redis客户端之一,提供了一些api方便操作redis
redisson之官网
redisson之Github
redisson之解决分布式锁
9.3 使用Redisson进行编码改造V9.0
9.3.1 你怎么知道该这样使用?
官网说话
8. 分布式锁和同步器 · redisson/redisson Wiki · GitHub
9.3.2 V9.0版本修改
9.3.2.1 POM
XML
<!--redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
9.3.2.2 RedisConfig
java
package com.atguigu.redislock.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @auther zzyy
* @create 2022-10-22 15:14
*/
@Configuration
public class RedisConfig
{
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory)
{
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
//设置key序列化方式string
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置value的序列化方式json
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
//单Redis节点模式
@Bean
public Redisson redisson()
{
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.111.175:6379").setDatabase(0).setPassword("111111");
return (Redisson) Redisson.create(config);
}
}
9.3.2.3 InventoryController
java
package com.atguigu.redislock.controller;
import com.atguigu.redislock.service.InventoryService;
import com.atguigu.redislock.service.InventoryService2;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @auther zzyy
* @create 2022-10-22 15:23
*/
@RestController
@Api(tags = "redis分布式锁测试")
public class InventoryController
{
@Autowired
private InventoryService inventoryService;
@ApiOperation("扣减库存,一次卖一个")
@GetMapping(value = "/inventory/sale")
public String sale()
{
return inventoryService.sale();
}
@ApiOperation("扣减库存saleByRedisson,一次卖一个")
@GetMapping(value = "/inventory/saleByRedisson")
public String saleByRedisson()
{
return inventoryService.saleByRedisson();
}
}
9.3.2.4 从现在开始不再用我们自己手写的锁了
9.3.2.5 InventoryService
java
package com.atguigu.redislock.service;
import cn.hutool.core.util.IdUtil;
import com.atguigu.redislock.mylock.DistributedLockFactory;
import com.atguigu.redislock.mylock.RedisDistributedLock;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
/**
* @auther zzyy
* @create 2022-10-25 16:07
*/
@Service
@Slf4j
public class InventoryService2
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
@Autowired
private DistributedLockFactory distributedLockFactory;
//V9.1,引入Redisson对应的官网推荐RedLock算法实现类
@Autowired
private Redisson redisson;
public String saleByRedisson()
{
String retMessage = "";
String key = "zzyyRedisLock";
RLock redissonLock = redisson.getLock(key);
redissonLock.lock();
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
System.out.println(retMessage);
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}finally {
redissonLock.unlock();
}
return retMessage+"\t"+"服务端口号:"+port;
}
}
9.3.2.6 测试
单机 OK
JMeter
BUG
解决
业务代码修改为V9.1版
java
package com.atguigu.redislock.service;
import cn.hutool.core.util.IdUtil;
import com.atguigu.redislock.mylock.DistributedLockFactory;
import com.atguigu.redislock.mylock.RedisDistributedLock;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
/**
* @auther zzyy
* @create 2022-10-25 16:07
*/
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
@Autowired
private DistributedLockFactory distributedLockFactory;
@Autowired
private Redisson redisson;
public String saleByRedisson()
{
String retMessage = "";
String key = "zzyyRedisLock";
RLock redissonLock = redisson.getLock(key);
redissonLock.lock();
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
System.out.println(retMessage);
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}finally {
if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread())
{
redissonLock.unlock();
}
}
return retMessage+"\t"+"服务端口号:"+port;
}
}
9.4 Redisson源码解析
加锁
可重入
续命
解锁
分析步骤
9.4.1 Redis 分布式锁过期了,但是业务逻辑还没处理完怎么办
还记得之前说过的缓存续命吗?
9.4.2 守护线程"续命"
额外起一个线程,定期检查线程是否还持有锁,如果有则延长过期时间。
Redisson 里面就实现了这个方案,使用"看门狗"定期检查(每1/3的锁时间检查1次),如果线程还持有锁,则刷新过期时间;
9.4.3 在获取锁成功后,给锁加一个 watchdog,watchdog会起一个定时任务,在锁没有被释放且快要过期的时候会续期
9.4.4 上述源码分析1
通过redisson新建出来的锁key,默认是30秒
9.4.5 上述源码分析2
RedissonLock.java
lock()---tryAcquire()---tryAcquireAsync()---
9.4.6 上述源码分析3
流程解释
通过exists判断,如果锁不存在,则设置值和过期时间,加锁成功
通过hexists判断,如果锁已存在,并且锁的是当前线程,则证明是重入锁,加锁成功
如果锁已存在,但锁的不是当前线程,则证明有其他线程持有锁。返回当前锁的过期时间(代表了锁key的剩余生存时间),加锁失败
9.4.7 上述源码分析4
|-----------------------------------------------------------------------------------------------------------------|
| 这里面初始化了一个定时器,dely 的时间是 internalLockLeaseTime/3。 在 Redisson 中,internalLockLeaseTime 是 30s,也就是每隔 10s 续期一次,每次 30s。 |
watch dog自动延期机制
客户端A加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端A还持有锁key,那么就会不断的延长锁key的生存时间,默认每次续命又从30秒新开始
自动续期lua脚本分析
9.4.8 解锁
9.5 多机案例
理论参考来源
redis之父提出了Redlock算法解决这个问题
官网
具体
小总结
这个锁的算法实现了多redis实例的情况,相对于单redis节点来说,优点在于 防止了 单节点故障造成整个服务停止运行的情况且在多节点中锁的设计,及多节点同时崩溃等各种意外情况有自己独特的设计方法。
Redisson 分布式锁支持 MultiLock 机制可以将多个锁合并为一个大锁,对一个大锁进行统一的申请加锁以及释放锁。
最低保证分布式锁的有效性及安全性的要求如下:
1.互斥;任何时刻只能有一个client获取锁
2.释放死锁;即使锁定资源的服务崩溃或者分区,仍然能释放锁
3.容错性;只要多数redis节点(一半以上)在使用,client就可以获取和释放锁
网上讲的基于故障转移实现的redis主从无法真正实现Redlock:
因为redis在进行主从复制时是异步完成的,比如在clientA获取锁后,主redis复制数据到从redis过程中崩溃了,导致没有复制到从redis中,然后从redis选举出一个升级为主redis,造成新的主redis没有clientA 设置的锁,这是clientB尝试获取锁,并且能够成功获取锁,导致互斥失效;
代码参考来源
2023年第8章第4小节
2023年第8章第3小节
MultiLock多重锁
9.5.1 docker走起3台redis的master机器,本次设置3台master各自独立无从属关系
docker run -p 6381:6379 --name redis-master-1 -d redis
docker run -p 6382:6379 --name redis-master-2 -d redis
docker run -p 6383:6379 --name redis-master-3 -d redis
执行成功见下:
9.5.2 进入上一步刚启动的redis容器实例
docker exec -it redis-master-1 /bin/bash 或者 docker exec -it redis-master-1 redis-cli
docker exec -it redis-master-2 /bin/bash 或者 docker exec -it redis-master-2 redis-cli
docker exec -it redis-master-3 /bin/bash 或者 docker exec -it redis-master-3 redis-cli
9.5.3 建Module
redis_redlock
9.5.4 改POM
XML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.atguigu.redis.redlock</groupId>
<artifactId>redis_redlock</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.19.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!--swagger-ui-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.11</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
9.5.5 写YML
XML
server.port=9090
spring.application.name=redlock
spring.swagger2.enabled=true
spring.redis.database=0
spring.redis.password=
spring.redis.timeout=3000
spring.redis.mode=single
spring.redis.pool.conn-timeout=3000
spring.redis.pool.so-timeout=3000
spring.redis.pool.size=10
spring.redis.single.address1=192.168.111.185:6381
spring.redis.single.address2=192.168.111.185:6382
spring.redis.single.address3=192.168.111.185:6383
9.5.6 主启动
java
package com.atguigu.redis.redlock;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RedisRedlockApplication
{
public static void main(String[] args)
{
SpringApplication.run(RedisRedlockApplication.class, args);
}
}
9.5.7 业务类
配置
CacheConfiguration
java
package com.atguigu.redis.redlock.config;
import org.apache.commons.lang3.StringUtils;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Configuration
@EnableConfigurationProperties(RedisProperties.class)
public class CacheConfiguration {
@Autowired
RedisProperties redisProperties;
@Bean
RedissonClient redissonClient1() {
Config config = new Config();
String node = redisProperties.getSingle().getAddress1();
node = node.startsWith("redis://") ? node : "redis://" + node;
SingleServerConfig serverConfig = config.useSingleServer()
.setAddress(node)
.setTimeout(redisProperties.getPool().getConnTimeout())
.setConnectionPoolSize(redisProperties.getPool().getSize())
.setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
if (StringUtils.isNotBlank(redisProperties.getPassword())) {
serverConfig.setPassword(redisProperties.getPassword());
}
return Redisson.create(config);
}
@Bean
RedissonClient redissonClient2() {
Config config = new Config();
String node = redisProperties.getSingle().getAddress2();
node = node.startsWith("redis://") ? node : "redis://" + node;
SingleServerConfig serverConfig = config.useSingleServer()
.setAddress(node)
.setTimeout(redisProperties.getPool().getConnTimeout())
.setConnectionPoolSize(redisProperties.getPool().getSize())
.setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
if (StringUtils.isNotBlank(redisProperties.getPassword())) {
serverConfig.setPassword(redisProperties.getPassword());
}
return Redisson.create(config);
}
@Bean
RedissonClient redissonClient3() {
Config config = new Config();
String node = redisProperties.getSingle().getAddress3();
node = node.startsWith("redis://") ? node : "redis://" + node;
SingleServerConfig serverConfig = config.useSingleServer()
.setAddress(node)
.setTimeout(redisProperties.getPool().getConnTimeout())
.setConnectionPoolSize(redisProperties.getPool().getSize())
.setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
if (StringUtils.isNotBlank(redisProperties.getPassword())) {
serverConfig.setPassword(redisProperties.getPassword());
}
return Redisson.create(config);
}
/**
* 单机
* @return
*/
/*@Bean
public Redisson redisson()
{
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.111.147:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}*/
}
RedisPoolProperties
java
package com.atguigu.redis.redlock.config;
import lombok.Data;
@Data
public class RedisPoolProperties {
private int maxIdle;
private int minIdle;
private int maxActive;
private int maxWait;
private int connTimeout;
private int soTimeout;
/**
* 池大小
*/
private int size;
}
RedisProperties
java
package com.atguigu.redis.redlock.config;
import lombok.Data;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "spring.redis", ignoreUnknownFields = false)
@Data
public class RedisProperties {
private int database;
/**
* 等待节点回复命令的时间。该时间从命令发送成功时开始计时
*/
private int timeout;
private String password;
private String mode;
/**
* 池配置
*/
private RedisPoolProperties pool;
/**
* 单机信息配置
*/
private RedisSingleProperties single;
}
RedisSingleProperties
java
package com.atguigu.redis.redlock.config;
import lombok.Data;
@Data
public class RedisSingleProperties {
private String address1;
private String address2;
private String address3;
}
controller
java
package com.atguigu.redis.redlock.controller;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.RedissonMultiLock;
import org.redisson.RedissonRedLock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@RestController
@Slf4j
public class RedLockController {
public static final String CACHE_KEY_REDLOCK = "ATGUIGU_REDLOCK";
@Autowired
RedissonClient redissonClient1;
@Autowired
RedissonClient redissonClient2;
@Autowired
RedissonClient redissonClient3;
boolean isLockBoolean;
@GetMapping(value = "/multiLock")
public String getMultiLock() throws InterruptedException
{
String uuid = IdUtil.simpleUUID();
String uuidValue = uuid+":"+Thread.currentThread().getId();
RLock lock1 = redissonClient1.getLock(CACHE_KEY_REDLOCK);
RLock lock2 = redissonClient2.getLock(CACHE_KEY_REDLOCK);
RLock lock3 = redissonClient3.getLock(CACHE_KEY_REDLOCK);
RedissonMultiLock redLock = new RedissonMultiLock(lock1, lock2, lock3);
redLock.lock();
try
{
System.out.println(uuidValue+"\t"+"---come in biz multiLock");
try { TimeUnit.SECONDS.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(uuidValue+"\t"+"---task is over multiLock");
} catch (Exception e) {
e.printStackTrace();
log.error("multiLock exception ",e);
} finally {
redLock.unlock();
log.info("释放分布式锁成功key:{}", CACHE_KEY_REDLOCK);
}
return "multiLock task is over "+uuidValue;
}
}
9.5.8 测试
http://localhost:9090/multilock
命令
ttl ATGUIGU_REDLOCK
HGETALL ATGUIGU_REDLOCK
shutdown
docker start redis-master-1
docker exec -it redis-master-1 redis-cli
结论
9.6 Redis的缓存过期淘汰策略
9.6.1 粉丝反馈的面试题
生产上你们的redis内存设置多少?
如何配置、修改redis的内存大小
如果内存满了你怎么办
redis清理内存的方式?定期删除和惰性删除了解过吗
redis缓存淘汰策略有哪些?分别是什么?你用那个?
redis的LRU了解过吗?请手写LRU
算法手写code见阳哥大厂第3季视频
lru和lfu算法的区别是什么
。。。。。。
9.6.2 Redis内存满了怎么办
9.6.2.1 redis默认内存多少?在哪里查看?如何设置修改?
查看Redis最大占用内存
|-----------------------------------------------------|
| 打开redis配置文件,设置maxmemory参数,maxmemory是bytes字节类型,注意转换。 |
redis默认内存多少可以用?
一般生产上你如何配置?
一般推荐Redis设置内存为最大物理内存的四分之三
如何修改redis内存设置
通过修改文件配置
通过命令修改
104,857,600B = 1024*1024*100 = 100MB
什么命令查看redis内存使用情况?
info memory
config get maxmemory
9.6.2.2 真要打满了会怎么样?如果Redis内存使用超出了设置的最大值会怎样?
改改配置,故意把最大值设为1个byte试试
9.6.2.3 结论
设置了maxmemory的选项,假如redis内存使用达到上限
没有加上过期时间就会导致数据写满maxmemory为了避免类似情况,引出下一章内存淘汰策略
9.6.3 往redis里写的数据是怎么没了的?它如何删除的?
9.6.3.1 redis过期键的删除策略
如果一个键是过期的,那它到了过期时间之后是不是马上就从内存中被被删除呢??
如果回答yes,立即删除,你自己走还是面试官送你走?
如果不是,那过期后到底什么时候被删除呢??是个什么操作?
如果一个键是过期的,那它到了过期时间之后是不是马上就从内存中被被删除呢??
如果回答yes,立即删除,你自己走还是面试官送你走?
如果不是,那过期后到底什么时候被删除呢??是个什么操作?
9.6.3.2 三种不同的删除策略
立即删除
Redis不可能时时刻刻遍历所有被设置了生存时间的key,来检测数据是否已经到达过期时间,然后对它进行删除。
立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放。但是立即删除对cpu是最不友好的。因为删除操作会占用cpu的时间,如果刚好碰上了cpu很忙的时候,比如正在做交集或排序等计算的时候,就会给cpu造成额外的压力,让CPU心累,时时需要删除,忙死。。。。。。。
这会产生大量的性能消耗,同时也会影响数据的读取操作。
总结:对CPU不友好,用处理器性能换取存储空间 (拿时间换空间)
惰性删除
数据到达过期时间,不做处理。等下次访问该数据时,
如果未过期,返回数据 ;
发现已过期,删除,返回不存在。
惰性删除策略的缺点是,它对内存是最不友好的。
如果一个键已经过期,而这个键又仍然保留在redis中,那么只要这个过期键不被删除,它所占用的内存就不会释放。
在使用惰性删除策略时,如果数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么它们也许永远也不会被删除(除非用户手动执行FLUSHDB),我们甚至可以将这种情况看作是一种内存泄漏--无用的垃圾数据占用了大量的内存,而服务器却不会自己去释放它们,这对于运行状态非常依赖于内存的Redis服务器来说,肯定不是一个好消息
总结:对memory不友好,用存储空间换取处理器性能(拿空间换时间)
开启憜性淘汰,lazyfree-lazy-eviction=yes
上面两种方案都走极端
定期删除
定期删除策略是前两种策略的折中:
定期删除策略每隔一段时间执行一次删除过期键操作并通过限制删除操作执行时长和频率来减少删除操作对CPU时间的影响。
|-----------------------------------------------------------------------------------------------------------------------------------|
| 周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度 特点1:CPU性能占用设置有峰值,检测频度可自定义设置 特点2:内存压力不是很大,长期占用内存的冷数据会被持续清理 总结:周期性抽查存储空间 (随机抽查,重点抽查) |
举例:
|------------------------------------------------------------------------------------------------------------------------------------------------|
| redis默认每隔100ms检查是否有过期的key,有过期key则删除。 注意:redis不是每隔100ms将所有的key检查一次而是随机抽取进行检查( 如果每隔100ms,全部key进行检查,redis直接进去ICU)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。 |
定期删除策略的难点是确定删除操作执行的时长和频率:如果删除操作执行得太频繁或者执行的时间太长,定期删除策略就会退化成立即删除策略,以至于将CPU时间过多地消耗在删除过期键上面。如果删除操作执行得太少,或者执行的时间太短,定期删除策略又会和惰性删除束略一样,出现浪费内存的情况。因此,如果采用定期删除策略的话,服务器必须根据情况,合理地设置删除操作的执行时长和执行频率。
定期抽样key,判断是否过期
漏网之鱼
9.6.3.3 上述步骤都过堂了,还有漏洞吗?
1 定期删除时,从来没有被抽查到
2 惰性删除时,也从来没有被点中使用过
上述两个步骤======> 大量过期的key堆积在内存中,导致redis内存空间紧张或者很快耗尽
必须要有一个更好的兜底方案......
9.6.3.4 redis缓存淘汰策略登场。。。。。。O(∩_∩)O哈哈~
9.6.4 redis缓存淘汰策略
9.6.4.1 redis配置文件
【MEMORY MANAGEMENT】
9.6.4.2 lru和lfu算法的区别是什么
区别
LRU:最近最少使用页面置换算法,淘汰最长时间未被使用的页面,看页面最后一次被使用到发生调度的时间长短,首先淘汰最长时间未被使用的页面。
LFU:最近最不常用页面置换算法,淘汰一定时期内被访问次数最少的页,看一定时间段内页面被使用的频率,淘汰一定时期内被访问次数最少的页
举个栗子
某次时期Time为10分钟,如果每分钟进行一次调页,主存块为3,若所需页面走向为2 1 2 1 2 3 4
假设到页面4时会发生缺页中断
若按LRU算法,应换页面1(1页面最久未被使用),但按LFU算法应换页面3(十分钟内,页面3只使用了一次)
可见LRU关键是看页面最后一次被使用到发生调度的时间长短,而LFU关键是看一定时间段内页面被使用的频率!
9.6.4.3 有哪些(redis7版本)
1 noeviction: 不会驱逐任何key,表示即使内存达到上限也不进行置换,所有能引起内存增加的命令都会返回error
2 allkeys-lru: 对所有key使用LRU算法进行删除,优先删除掉最近最不经常使用的key,用以保存新数据
3 volatile-lru: 对所有设置了过期时间的key使用LRU算法进行删除
4 allkeys-random: 对所有key随机删除
5 volatile-random: 对所有设置了过期时间的key随机删除
6 volatile-ttl: 删除马上要过期的key
7 allkeys-lfu: 对所有key使用LFU算法进行删除
8 volatile-lfu: 对所有设置了过期时间的key使用LFU算法进行删除
9.6.4.4 上面总结
2 * 4 得8
2个维度
过期键中筛选
所有键中筛选
4个方面
LRU
LFU
random
ttl
8个选项
9.6.4.5 你平时用哪一种
9.6.4.6 如何配置、修改
直接用config命令
直接redis.conf配置文件
9.6.5 redis缓存淘汰策略配置性能建议
避免存储bigkey
开启憜性淘汰,lazyfree-lazy-eviction=yes
10 Redis经典五大类型源码及底层实现(面试)
10.1 粉丝反馈回来的题目
10.1.1 Redis数据类型的底层数据结构
SDS动态字符串
双向链表
压缩列表ziplist
哈希表hashtable
跳表skiplist
整数集合intset
快速列表quicklist
紧凑列表listpack
10.1.2 阅读源码意义
90%没有太大意义,完全为了面试
10%大厂自己内部中间件,比如贵公司自己内部redis重构,阿里云redis,美团tair,滴滴kedis等等
10.2 redis源码在哪里
\redis-7.0.5\src
https://github.com/redis/redishttps://github.com/redis/redis
10.3 源码分析参考书,阳哥个人推荐
《Redis设计与实现》
《Redis5设计与源码分析》
10.4 Redis源代码的核心部分
src源码包下面该如何看?
10.4.1 源码分析思路
这么多你如何看?
外面考什么就看什么
分类
10.4.2 Redis基本的数据结构(骨架)
https://github.com/redis/redis
10.4.3 Redis数据库的实现
数据库的底层实现db.c
持久化rdb.c和aof.c
10.4.4 Redis服务端和客户端实现
事件驱动ae.c和ae_epoll.c
网络连接anet.c和networking.c
服务端程序server.c
客户端程序redis-cli.c
10.4.5 其他
主从复制replication.c
哨兵sentinel.c
集群cluster.c
其他数据结构,如hyperloglog.c、geo.c等
其他功能,如pub/sub、Lua脚本
10.5 我们平时说redis是字典数据库KV键值对到底是什么
10.5.1 怎样实现键值对(key-value)数据库的
redis是key-value 存储系统
key一般都是String类型的字符串对象
value类型则为redis对象(redisObject)
value可以是字符串对象,也可以是集合数据类型的对象,比如 List 对象、Hash 对象、Set 对象和 Zset 对象
图说
10.5.2 10大类型说明(粗分)
传统的5大类型
String
List
Hash
Set
ZSet
新介绍的5大类型
bitmap
实质String
hyperLogLog
实质String
GEO
实质Zset
Stream
实质Stream
BITFIELD
看具体key
10.5.3 上帝视角
10.5.4 Redis定义了redisObjec结构体来表示string、hash、list、set、zset等数据类型
10.5.4.1 C语言struct结构体语法简介
10.5.4.2 Redis中每个对象都是一个 redisObject 结构
10.5.4.3 字典、KV是什么(重点)
每个键值对都会有一个dictEntry
源码位置:dict.h
重点:从dictEntry到RedisObject
10.5.4.4 这些键值对是如何保存进Redis并进行读取操作,O(1)复杂度
10.5.4.5 redisObject +Redis数据类型+Redis 所有编码方式(底层实现)三者之间的关系
10.6 五大结构底层C语言源码分析
10.6.1 重点:redis数据类型与数据结构总纲图
10.6.1.1 源码分析总体数据结构大纲
1.SDS动态字符串
2.双向链表
3.压缩列表ziplist
4.哈希表hashtable
5.跳表skiplist
6.整数集合intset
7.快速列表quicklist
8.紧凑列表listpack
10.6.1.2 Redis6.0.5
redis6之前,老版本
10.6.1.3 2021.11.29号之后
10.6.1.4 Redis7
java
/** redis6相关的底层模型和结构
* String = SDS
* Set = intset + hashtable
* ZSet = skipList + zipList
* List = quicklist + zipList
* Hash = hashtable + zipList
*
* ===========================================
*
* redis7相关的底层模型和结构
* String = SDS
* Set = intset + hashtable
* ZSet = skipList + listpack紧凑列表
* List = quicklist
* Hash = hashtable + listpack紧凑列表
*/
10.6.1.5 复习一下基础篇介绍的redis7新特性,看看数据结构
10.6.2 源码分析总体数据结构大纲
程序员写代码时脑子底层思维
上帝视角最右边编码如何来的
redisObject操作底层定义来自哪里?
笔记最下还有
10.6.3 从set hello world说起
每个键值对都会有一个dictEntry
set hello word为例,因为Redis是KV键值对的数据库,每个键值对都会有一个dictEntry(源码位置:dict.h),
里面指向了key和value的指针,next 指向下一个 dictEntry。
key 是字符串,但是 Redis 没有直接使用 C 的字符数组,而是存储在redis自定义的 SDS中。
value 既不是直接作为字符串存储,也不是直接存储在 SDS 中,而是存储在redisObject 中。
实际上五种常用的数据类型的任何一种,都是通过 redisObject 来存储的。
看看类型
type 键
看看编码
object encoding hello
10.6.4 redisObjec结构的作用
为了便于操作,Redis采用redisObjec结构来统一五种不同的数据类型,这样所有的数据类型就都可以以相同的形式在函数间传递而不用使用特定的类型结构。同时,为了识别不同的数据类型,redisObjec中定义了type和encoding字段对不同的数据类型加以区别。简单地说,redisObjec就是string、hash、list、set、zset的父类,可以在函数间传递时隐藏具体的类型信息,所以作者抽象了redisObjec结构来到达同样的目的。
RedisObject各字段的含义
1 4位的type表示具体的数据类型
2 4位的encoding表示该类型的物理编码方式见下表,同一种数据类型可能有不同的编码方式。
(比如String就提供了3种:int embstr raw)
3 lru字段表示当内存超限时采用LRU算法清除内存中的对象。
4 refcount表示对象的引用计数。
5 ptr指针指向真正的底层数据结构的指针。
案例
set age 17
|----------|------------------|
| type | 类型 |
| encoding | 编码,此处是数字类型 |
| lru | 最近被访问的时间 |
| refcount | 等于1,表示当前对象被引用的次数 |
| ptr | value值是多少,当前就是17 |
10.6.5 经典5大数据结构解析
10.6.5.1 各个类型的数据结构的编码映射和定义
10.6.5.2 Debug Object key
再看一次set age 17
|----------|------------------|
| type | 类型 |
| encoding | 编码,此处是数字类型 |
| lru | 最近被访问的时间 |
| refcount | 等于1,表示当前对象被引用的次数 |
| ptr | value值是多少,当前就是17 |
上述解读
命令
案例
开启前
开启后
Value at: 内存地址
refcount: 引用次数
encoding: 物理编码类型
serializedlength: 序列化后的长度(注意这里的长度是序列化后的长度,保存为rdb文件时使用了该算法,不是真正存贮在内存的大小),会对字串做一些可能的压缩以便底层优化
lru:记录最近使用时间戳
lru_seconds_idle:空闲时间
10.6.5.3 String数据结构介绍
10.6.5.3.1 三大物理编码方式
RedisObject内部对应3大物理编码
Int
保存long 型(长整型)的64位(8个字节)有符号整数
9223372036854775807
上面数字最多19位
补充
只有整数才会使用 int,如果是浮点数, Redis内部其实先将浮点数转化为字符串值,然后再保存。
embstr
代表 embstr 格式的 SDS(Simple Dynamic String简单动态字符串),保存长度小于44字节的字符串
EMBSTR 顾名思义即:embedded string,表示嵌入式的String
raw(未经加工的,原生的)
保存长度大于44字节的字符串
10.6.5.3.2 三大物理编码案例
案例测试
C语言中字符串的展现
Redis没有直接复用C语言的字符串,而是****新建了属于自己的结构-----SDS
在Redis数据库里,包含字符串值的键值对都是由SDS实现的(Redis中所有的键都是由字符串对象实现的即底层是由SDS实现,Redis中所有的值对象中包含的字符串对象底层也是由SDS实现)。
SDS简单动态字符串
sds.h源码分析
说明
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Redis中字符串的实现,SDS有多种结构(sds.h): sdshdr5、(2^5=32byte) sdshdr8、(2 ^ 8=256byte) sdshdr16、(2 ^ 16=65536byte=64KB) sdshdr32、 (2 ^ 32byte=4GB) sdshdr64,2的64次方byte=17179869184G用于存储不同的长度的字符串。 len 表示 SDS 的长度,使我们在获取字符串长度的时候可以在 O(1)情况下拿到,而不是像 C 那样需要遍历一遍字符串。 alloc 可以用来计算 free 就是字符串已经分配的未使用的空间,有了这个值就可以引入预分配空间的算法了,而不用去考虑内存分配的问题。 buf 表示字符串数组,真存数据的。 |
官网
https://github.com/antirez/sds
Redis为什么重新设计一个 SDS 数据结构?
C语言没有Java里面的String类型,只能是靠自己的char[]来实现,字符串在 C 语言中的存储方式,想要获取 「Redis」的长度,需要从头开始遍历,直到遇到 '\0' 为止。所以,Redis 没有直接使用 C 语言传统的字符串标识,而是自己构建了一种名为简单动态字符串 SDS(simple dynamic string)的抽象类型,并将 SDS 作为 Redis 的默认字符串。
|-------------|--------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| | C语言 | SDS |
| 字符串长度处理 | 需要从头开始遍历,直到遇到 '\0' 为止,时间复杂度O(N) | 记录当前字符串的长度,直接读取即可,时间复杂度 O(1) |
| 内存重新分配 | 分配内存空间超过后,会导致数组下标越级或者内存分配溢出 | 空间预分配 SDS 修改后,len 长度小于 1M,那么将会额外分配与 len 相同长度的未使用空间。如果修改后长度大于 1M,那么将分配1M的使用空间。 惰性空间释放 有空间分配对应的就有空间释放。SDS 缩短时并不会回收多余的内存空间,而是使用 free 字段将多出来的空间记录下来。如果后续有变更操作,直接使用 free 中记录的空间,减少了内存的分配。 |
| 二进制安全 | 二进制数据并不是规则的字符串格式,可能会包含一些特殊的字符,比如 '\0' 等。前面提到过,C中字符串遇到 '\0' 会结束,那 '\0' 之后的数据就读取不上了 | 根据 len 长度来判断字符串结束的,二进制安全的问题就解决了 |
源码分析
用户API
set k1 v1底层发生了什么?调用关系
3大物理编码方式
INT 编码格式
set k1 123
命令示例: set k1 123
当字符串键值的内容可以用一个64位有符号整形来表示时,Redis会将键值转化为long型来进行存储,此时即对应 OBJ_ENCODING_INT 编码类型。内部的内存结构表示如下:
|--------------------------------------------------------------------------------------------------------------------------------------------------------|
| Redis 启动时会预先建立 10000 个分别存储 0~9999 的 redisObject 变量作为共享对象,这就意味着如果 set字符串的键值在 0~10000 之间的话,则可以 直接指向共享对象 而不需要再建立新对象,此时键值不占空间! set k1 123 set k2 123 |
redis源代码:server.h,笔记下面还有
redis6源代码:object.c笔记下面还有
redis7源代码:object.c笔记下面还有
EMBSTR编码格式
set k1 abc
redis源代码:object.c
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 对于长度小于 44的字符串,Redis 对键值采用OBJ_ENCODING_EMBSTR 方式,EMBSTR 顾名思义即:embedded string,表示 嵌入式的String。从内存结构上来讲 即字符串 sds结构体与其对应的 redisObject 对象分配在同一块连续的内存空间,字符串sds嵌入在redisObject对象之中一样。 |
进一步createEmbeddedStringObject方法
redis源代码:object.c
RAW 编码格式
set k1 大于44长度的一个字符串,随便写
当字符串的键值为长度大于44的超长字符串时,Redis 则会将键值的内部编码方式改为OBJ_ENCODING_RAW格式,这与OBJ_ENCODING_EMBSTR编码方式的不同之处在于,此时动态字符串sds的内存与其依赖的redisObject的内存不再连续了
明明没有超过阈值,为什么变成 raw 了
判断不出来,就取最大Raw
转变逻辑图
案例结论
只有整数才会使用 int,如果是浮点数, Redis 内部其实先将浮点数转化为字符串值,然后再保存。
embstr 与 raw 类型底层的数据结构其实都是 SDS (简单动态字符串,Redis 内部定义 sdshdr 一种结构)。
那这两者的区别见下图:
|----------|---------------------------------------------------------------------------------------------------------------------------------------------|
| 1 int | Long类型整数时,RedisObject中的ptr指针直接赋值为整数数据,不再额外的指针再指向整数了,节省了指针的空间开销。 |
| 2 embstr | 当保存的是字符串数据且字符串小于等于44字节时,embstr类型将会调用内存分配函数, 只分配一块连续的内存空间,空间中依次包含 redisObject 与 sdshdr 两个数据结构,让元数据、指针和SDS是一块连续的内存区域,这样就可以避免内存碎片 |
| 3 raw | 当字符串大于44字节时,SDS的数据量变多变大了,SDS和RedisObject布局分家各自过,会给SDS分配多的空间并用指针指向SDS结构,raw 类型将会 调用两次内存分配函数,分配两块内存空间,一块用于包含 redisObject结构,而另一块用于包含 sdshdr 结构 |
10.6.5.3.3 总结
Redis内部会根据用户给的不同键值而使用不同的编码格式,自适应地选择较优化的内部编码格式,而这一切对用户完全透明!
10.6.5.4 Hash数据结构介绍
案例
10.6.5.4.1 redis6
案例
hash-max-ziplist-entries:使用压缩列表保存时哈希集合中的最大元素个数。
hash-max-ziplist-value:使用压缩列表保存时哈希集合中单个元素的最大长度。
Hash类型键的字段个数 小于 hash-max-ziplist-entries 并且每个字段名和字段值的长度 小于 hash-max-ziplist-value 时,
Redis才会使用 OBJ_ENCODING_ZIPLIST来存储该键,前述条件任意一个不满足则会转换为 OBJ_ENCODING_HT的编码方式
结构
hash-max-ziplist-entries:使用压缩列表保存时哈希集合中的最大元素个数。
hash-max-ziplist-value:使用压缩列表保存时哈希集合中单个元素的最大长度。
结论
1.哈希对象保存的键值对数量小于512个
2.所有的键值对的健和值的字符串长度都小于等于 64byte(一个英文字母一个字节)时用ziplist,反之用hashtable
ziplist升级到hashtable可以,反过来降级不可以
一旦从压缩列表转为了哈希表,Hash类型就会一直用哈希表进行保存而不会再转回压缩列表了。
在节省内存空间方面哈希表就没有压缩列表高效了。
流程
源码分析
t_hash.c
在 Redis 中,hashtable 被称为字典(dictionary),它是一个数组+链表的结构
OBJ_ENCODING_HT 编码分析
OBJ_ENCODING_HT 这种编码方式内部才是真正的哈希表结构,或称为字典结构,其可以实现O(1)复杂度的读写操作,因此效率很高。
在 Redis内部,从 OBJ_ENCODING_HT类型到底层真正的散列表数据结构是一层层嵌套下去的,组织关系见面图:
源代码:dict.h
每个键值对都会有一个dictEntry
hset命令解读
t_hash.c
类型
ziplist.c(★)
Ziplist 压缩列表是一种紧凑编码格式,总体思想是多花时间来换取节约空间,即以部分读写性能为代价,来换取极高的内存空间利用率,
因此只会用于 字段个数少,且字段值也较小 的场景。压缩列表内存利用率极高的原因与其连续内存的特性是分不开的。
想想我们的学过的一种GC垃圾回收机制:标记--压缩算法
当一个 hash对象 只包含少量键值对且每个键值对的键和值要么就是小整数要么就是长度比较短的字符串,那么它用 ziplist 作为底层实现
ziplist,什么样
源代码:ziplist.c
为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组
ziplist是一个经过特殊编码的双向链表,它不存储指向前一个链表节点prev和指向下一个链表节点的指针next而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能,来换取高效的内存空间利用率,节约内存,是一种时间换空间的思想。只用在字段个数少,字段值小的场景里面
ziplist各个组成单元什么意思
zlentry,压缩列表节点的构成
官网源码
zlentry实体结构解析
ziplist.c
ziplist存取情况
|----------|--------------------|
| prevlen | 记录了前一个节点的长度; |
| encoding | 记录了当前节点实际数据的类型以及长度 |
| data | 记录了当前节点的实际数据 |
zlentry解析
压缩列表zlentry节点结构:每个zlentry由前一个节点的长度、encoding和entry-data三部分组成
前节点:(前节点占用的内存字节数)表示前1个zlentry的长度,privious_entry_length有两种取值情况:1字节或5字节。取值1字节时,表示上一个entry的长度小于254字节。虽然1字节的值能表示的数值范围是0到255,但是压缩列表中zlend的取值默认是255,因此,就默认用255表示整个压缩列表的结束,其他表示长度的地方就不能再用255这个值了。所以,当上一个entry长度小于254字节时,prev_len取值为1字节,否则,就取值为5字节。记录长度的好处:占用内存小,1或者5个字节
enncoding:记录节点的content保存数据的类型和长度。
content:保存实际数据内容
为什么zlentry这么设计?数组和链表数据结构对比
privious_entry_length,encoding长度都可以根据编码方式推算,真正变化的是content,而content长度记录在encoding里 ,因此entry的长度就知道了。entry总长度 = privious_entry_length字节数+encoding字节数+content字节数
为什么entry这么设计?记录前一个节点的长度?
链表在内存中,一般是不连续的,遍历相对比较慢,而ziplist可以很好的解决这个问题。如果知道了当前的起始地址,因为entry是连续的,entry后一定是另一个entry,想知道下一个entry的地址,只要将当前的起始地址加上当前entry总长度。如果还想遍历下一个entry,只要继续同样的操作。
明明有链表了,为什么出来一个压缩链表?明明有链表了,为什么出来一个压缩链表?
1 普通的双向链表会有两个指针,在存储数据很小的情况下,我们存储的实际数据的大小可能还没有指针占用的内存大,得不偿失。ziplist 是一个特殊的双向链表没有维护双向指针:previous next;而是存储上一个 entry的长度和当前entry的长度,通过长度推算下一个元素在什么地方。牺牲读取的性能,获得高效的存储空间,因为(简短字符串的情况)存储指针比存储entry长度更费内存。这是典型的"时间换空间"。
2 链表在内存中一般是不连续的,遍历相对比较慢而ziplist可以很好的解决这个问题,普通数组的遍历是根据数组里存储的数据类型找到下一个元素的(例如int类型的数组访问下一个元素时每次只需要移动一个sizeof(int)就行),但是ziplist的每个节点的长度是可以不一样的,而我们面对不同长度的节点又不可能直接sizeof(entry),所以ziplist只好将一些必要的偏移量信息记录在了每一个节点里,使之能跳到上一个节点或下一个节点。
备注:sizeof实际上是获取了数据在内存中所占用的存储空间,以字节为单位来计数。
3 头节点里有头节点里同时还有一个参数 len,和string类型提到的 SDS 类似,这里是用来记录链表长度的。因此获取链表长度时不用再遍历整个链表,直接拿到len值就可以了,这个时间复杂度是 O(1)
ziplist总结
ziplist为了节省内存,采用了紧凑的连续存储。
ziplist是一个双向链表,可以在时间复杂度为 O(1) 下从头部、尾部进行 pop 或 push。
新增或更新元素可能会出现连锁更新现象(致命缺点导致被listpack替换)。
不能保存过多的元素,否则查询效率就会降低,数量小和内容小的情况下可以使用。
10.6.5.4.2 redis7
案例
hash-max-listpack-entries:使用压缩列表保存时哈希集合中的最大元素个数。
hash-max-listpack-value:使用压缩列表保存时哈希集合中单个元素的最大长度。
Hash类型键的字段个数 小于 hash-max-listpack-entries且每个字段名和字段值的长度 小于 hash-max-listpack-value 时,
Redis才会使用OBJ_ENCODING_LISTPACK来存储该键,前述条件任意一个不满足则会转换为 OBJ_ENCODING_HT的编码方式
结构
hash-max-listpack-entries:使用紧凑列表保存时哈希集合中的最大元素个数。
hash-max-listpack-value:使用紧凑列表保存时哈希集合中单个元素的最大长度。
结论
1.哈希对象保存的键值对数量小于512个;
2.所有的键值对的健和值的字符串长度都小于等于 64byte(一个英文字母一个字节)时用listpack,反之用hashtable
listpack升级到hashtable可以,反过来降级不可以
流程(同前,只不过ziplist修改为listpack)源码分析
源码分析
源码说明
实现:object.c
实现:listpack.c
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| lpNew 函数创建了一个空的 listpack,一开始分配的大小是 LP_HDR_SIZE 再加 1 个字节。LP_HDR_SIZE 宏定义是在 listpack.c 中,它默认是 6 个字节,其中 4 个字节是记录 listpack 的总字节数,2 个字节是记录 listpack 的元素数量。 此外,listpack 的最后一个字节是用来标识 listpack 的结束,其默认值是宏定义 LP_EOF。 和 ziplist 列表项的结束标记一样,LP_EOF 的值也是 255 |
实现2:object.c
明明有ziplist了,为什么出来一个listpack紧凑列表?(面试题)
复习
ziplist的连锁更新问题
压缩列表新增某个元素或修改某个元素时,如果空间不不够,压缩列表占用的内存空间就需要重新分配。而当新插入的元素较大时,可能会导致后续元素的 prevlen 占用空间都发生变化,从而引起「连锁更新」问题,导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降。
案例说明:压缩列表每个节点正因为需要保存前一个节点的长度字段,就会有连锁更新的隐患
第一步:现在假设一个压缩列表中有多个连续的、长度在 250~253 之间的节点,如下图:
因为这些节点长度值小于 254 字节,所以 prevlen 属性需要用 1 字节的空间来保存这个长度值,一切OK,O(∩_∩)O哈哈~
第二步:这时,如果将一个长度大于等于 254 字节的新节点加入到压缩列表的表头节点,即新节点将成为entry1的前置节点,如下图:
因为entry1节点的prevlen属性只有1个字节大小,无法保存新节点的长度,此时就需要对压缩列表的空间重分配操作并将entry1节点的prevlen 属性从原来的 1 字节大小扩展为 5 字节大小。
第三步:连续更新问题出现
entry1节点原本的长度在250~253之间,因为刚才的扩展空间,此时entry1节点的长度就大于等于254,因此原本entry2节点保存entry1节点的 prevlen属性也必须从1字节扩展至5字节大小。entry1节点影响entry2节点,entry2节点影响entry3节点......一直持续到结尾。这种在特殊情况下产生的连续多次空间扩展操作就叫做「连锁更新」
结论
listpack 是 Redis 设计用来取代掉 ziplist 的数据结构,它通过每个节点记录自己的长度且放在节点的尾部,来彻底解决掉了 ziplist 存在的连锁更新的问题
listpack结构
官网
listpack/listpack.md at master · antirez/listpack · GitHub
listpack由4部分组成:total Bytes、Num Elem、Entry以及End
|----------------------|--------------------------------------------------------|
| Total Bytes | 为整个listpack的空间大小,占用4个字节,每个listpack最多占用4294967295Bytes。 |
| num-elements | 为listpack中的元素个数,即Entry的个数占用2个字节 |
| element-1~element-N | 为每个具体的元素 |
| listpack-end-byte | 为listpack结束标志,占用1个字节,内容为0xFF。 |
entry结构
当前元素的编码类型(entry-encoding)
元素数据 (entry-data)
以及编码类型和元素数据这两部分的长度 (entry-len)
listpackEntry结构定义:listpack.h
ziplist内存布局 VS listpack内存布局
和ziplist 列表项类似,listpack 列表项也包含了元数据信息和数据本身。不过,为了避免ziplist引起的连锁更新问题,listpack 中的每个列表项不再像ziplist列表项那样保存其前一个列表项的长度。
10.6.5.4.3 hash的两种编码格式
10.6.5.5 List数据结构介绍
java
/**
*
* java List
* 1 ArrayList ===> Object[]
* 2 LinkedList ===> 放入node节点的一个双端链表
*
* redis list
* 都是双端链表结构,借鉴java的思想,redis也给用户新建了一个全新的数据结构,俗称
* 1 redis6 ===》 quicklist
* 2 redis7 ===》 quicklist
*
* 总纲
*
* 分
*/
案例:
redis6
案例
这是Redis6的案例说明
(1) ziplist压缩配置:list-compress-depth 0
表示一个quicklist两端不被压缩的节点个数。这里的节点是指quicklist双向链表的节点,而不是指ziplist里面的数据项个数
参数list-compress-depth的取值含义如下:
0: 是个特殊值,表示都不压缩。这是Redis的默认值。
1: 表示quicklist两端各有1个节点不压缩,中间的节点压缩。
2: 表示quicklist两端各有2个节点不压缩,中间的节点压缩。
3: 表示quicklist两端各有3个节点不压缩,中间的节点压缩。
依此类推...
(2) ziplist中entry配置:list-max-ziplist-size -2
当取正值的时候,表示按照数据项个数来限定每个quicklist节点上的ziplist长度。比如,当这个参数配置成5的时候,表示每个quicklist节点的ziplist最多包含5个数据项。当取负值的时候,表示按照占用字节数来限定每个quicklist节点上的ziplist长度。这时,它只能取-1到-5这五个值,
每个值含义如下:
-5: 每个quicklist节点上的ziplist大小不能超过64 Kb。(注:1kb => 1024 bytes)
-4: 每个quicklist节点上的ziplist大小不能超过32 Kb。
-3: 每个quicklist节点上的ziplist大小不能超过16 Kb。
-2: 每个quicklist节点上的ziplist大小不能超过8 Kb。(-2是Redis给出的默认值)
-1: 每个quicklist节点上的ziplist大小不能超过4 Kb。
Redis6版本前的List的一种编码格式
list用quicklist来存储,quicklist存储了一个双向链表,每个节点都是一个ziplist
在Redis3.0之前,list采用的底层数据结构是ziplist压缩列表+linkedList双向链表
然后在高版本的Redis中底层数据结构是quicklist(替换了ziplist+linkedList),而quicklist也用到了ziplist
结论:quicklist就是「双向链表 + 压缩列表」组合,因为一个 quicklist 就是一个链表,而链表中的每个元素又是一个压缩列表
quicklist总纲
quicklist 实际上是 zipList 和 linkedList 的混合体,它将 linkedList按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。
是ziplist和linkedlist的结合体
源码分析
quicklist.h,head和tail指向双向列表的表头和表尾
quicklist结构
quicklistNode结构
quicklistNode中的*zl指向一个ziplist,一个ziplist可以存放多个元素
redis7
案例
listpack紧凑列表
是用来替代 ziplist 的新数据结构,在 7.0 版本已经没有 ziplist 的配置了(6.0版本仅部分数据类型作为过渡阶段在使用)
源码说明
实现:t_list.c
本图最下方有lpush命令执行后直接调用pushGenericCommand命令
看看redis6的相同文件t_list.c
实现:object.c
Redis7的List的一种编码格式
list用quicklist来存储,quicklist存储了一个双向链表,每个节点都是一个listpack
quicklist
是listpack和linkedlist的结合体
10.6.5.6 Set数据结构介绍
案例
Redis用intset或hashtable存储set。如果元素都是整数类型,就用intset存储。
如果不是整数类型,就用hashtable(数组+链表的存来储结构)。key就是元素的值,value为null。
set-proc-title 修改进程标题以显示一些运行时信息
Set的两种编码格式
intset
hashtable
源码分析
t_set.c
10.6.5.7 ZSet数据结构介绍
案例
redis6
当有序集合中包含的元素数量超过服务器属性 server.zset_max_ziplist_entries 的值(默认值为 128 ),或者有序集合中新添加元素的 member 的长度大于服务器属性 server.zset_max_ziplist_value 的值(默认值为 64 )时,redis会使用跳跃表作为有序集合的底层实现。
否则会使用ziplist作为有序集合的底层实现
redis7
ZSet的两种编码格式
redis6
ziplist
skiplist
redis7
listpack
skiplist
redis6源码分析
t_zset.c
redis7源码分析
t_zset.c
10.6.6 小总结
10.6.6.1 redis6类型-物理编码-对应表
10.6.6.2 redis6数据类型对应的底层数据结构
1. 字符串
int:8个字节的长整型。
embstr:小于等于44个字节的字符串。
raw:大于44个字节的字符串。
Redis会根据当前值的类型和长度决定使用哪种内部编码实现。
2. 哈希
ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries 配置(默认512个)、同时所有值都小于hash-max-ziplist-value配置(默认64 字节)时,
Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的 结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。
hashtable(哈希表):当哈希类型无法满足ziplist的条件时,Redis会使 用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)。
3. 列表
ziplist(压缩列表):当列表的元素个数小于list-max-ziplist-entries配置 (默认512个),同时列表中每个元素的值都小于list-max-ziplist-value配置时 (默认64字节),
Redis会选用ziplist来作为列表的内部实现来减少内存的使 用。
linkedlist(链表):当列表类型无法满足ziplist的条件时,Redis会使用 linkedlist作为列表的内部实现。quicklist ziplist和linkedlist的结合以ziplist为节点的链表(linkedlist)
4. 集合
intset(整数集合):当集合中的元素都是整数且元素个数小于set-max-intset-entries配置(默认512个)时,Redis会用intset来作为集合的内部实现,从而减少内存的使用。
hashtable(哈希表):当集合类型无法满足intset的条件时,Redis会使用hashtable作为集合的内部实现。
5. 有序集合
ziplist(压缩列表):当有序集合的元素个数小于zset-max-ziplist- entries配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value配 置(默认64字节)时,
Redis会用ziplist来作为有序集合的内部实现,ziplist 可以有效减少内存的使用。
skiplist(跳跃表):当ziplist条件不满足时,有序集合会使用skiplist作 为内部实现,因为此时ziplist的读写效率会下降。
10.6.6.3 redis6数据类型以及数据结构的关系
10.6.6.4 redis7数据类型以及数据结构的关系
10.6.6.5 redis数据类型以及数据结构的时间复杂度
10.7 skiplist跳表面试题
10.7.1 为什么引出跳表
10.7.1.1 先从一个单链表来讲
对于一个单链表来讲,即便链表中存储的数据是有序的,如果我们要想在其中查找某个数据,也只能从头到尾遍历链表。
这样查找效率就会很低,时间复杂度会很高O(N)
10.7.1.2 痛点
解决方法:升维,也叫空间换时间。
优化
从这个例子里,我们看出,加来一层索引之后,查找一个结点需要遍历的结点个数减少了,也就是说查找效率提高了。
优化2
画了一个包含64个结点的链表,按照前面讲的这种思路,建立了五级索引
10.7.2 是什么
跳表是可以实现二分查找的有序链表
skiplist是一种以空间换取时间的结构。
由于链表,无法进行二分查找,因此借鉴数据库索引的思想,提取出链表中关键节点(索引),先在关键节点上查找,再进入下层链表查找,提取多层关键节点,就形成了跳跃表
but
由于索引也要占据一定空间的,所以,索引添加的越多,空间占用的越多
总结来讲 跳表 = 链表 + 多级索引
10.7.3 跳表时间+空间复杂度介绍
跳表的时间复杂度
跳表查询的时间复杂度分析,如果链表里有N个结点,会有多少级索引呢?
按照我们前面讲的,两两取首。每两个结点会抽出一个结点作为上一级索引的结点,以此估算:
第一级索引的结点个数大约就是n/2,
第二级索引的结点个数大约就是n/4,
第三级索引的结点个数大约就是n/8,依次类推......
也就是说,第k级索引的结点个数是第k-1级索引的结点个数的1/2,那第k级索引结点的个数就是n/(2^k)
时间复杂度是O(logN)
跳表的空间复杂度
跳表查询的空间复杂度分析
比起单纯的单链表,跳表需要存储多级索引,肯定要消耗更多的存储空间。那到底需要消耗多少额外的存储空间呢?
我们来分析一下跳表的空间复杂度。
第一步:首先原始链表长度为n,
第二步:两两取首,每层索引的结点数:n/2, n/4, n/8 ... , 8, 4, 2 每上升一级就减少一半,直到剩下2个结点,以此类推;如果我们把每层索引的结点数写出来,就是一个等比数列。
这几级索引的结点总和就是n/2+n/4+n/8...+8+4+2=n-2。所以,跳表的空间复杂度是O(n) 。也就是说,如果将包含n个结点的单链表构造成跳表,我们需要额外再用接近n个结点的存储空间。
第三步:思考三三取首,每层索引的结点数:n/3, n/9, n/27 ... , 9, 3, 1 以此类推;
第一级索引需要大约n/3个结点,第二级索引需要大约n/9个结点。每往上一级,索引结点个数都除以3。为了方便计算,我们假设最高一级的索
引结点个数是1。我们把每级索引的结点个数都写下来,也是一个等比数列
通过等比数列求和公式,总的索引结点大约就是n/3+n/9+n/27+...+9+3+1=n/2。尽管空间复杂度还是O(n) ,但比上面的每两个结点抽一个结点的索引构建方法,要减少了一半的索引结点存储空间。
所以空间复杂度是O(n);
所以空间复杂度是O(N)
10.7.4 优缺点
优点:
跳表是一个最典型的空间换时间解决方案,而且只有在数据量较大的情况下才能体现出来优势。而且应该是读多写少的情况下才能使用,所以它的适用范围应该还是比较有限的
缺点:
维护成本相对要高,
在单链表中,一旦定位好要插入的位置,插入结点的时间复杂度是很低的,就是O(1)
but
新增或者删除时需要把所有索引都更新一遍,为了保证原始链表中数据的有序性,我们需要先找
到要动作的位置,这个查找操作就会比较耗时最后在新增和删除的过程中的更新,时间复杂度也是O(log n)
11 Redis为什么快?高性能设计之epoll和IO多路复用深度解析
11.1 before
11.1.1 多路复用要解决的问题
并发多客户端连接,在多路复用之前最简单和典型的方案:同步阻塞网络IO模型
这种模式的特点就是用一个进程来处理一个网络连接(一个用户请求),比如一段典型的示例代码如下。
直接调用 recv 函数从一个 socket 上读取数据。
int main()
{
...
recv(sock, ...) //从用户角度来看非常简单,一个recv一用,要接收的数据就到我们手里了。
}
我们来总结一下这种方式:
优点就是这种方式非常容易让人理解,写起代码来非常的自然,符合人的直线型思维。
缺点就是性能差,每个用户请求到来都得占用一个进程来处理,来一个请求就要分配一个进程跟进处理,
类似一个学生配一个老师,一位患者配一个医生,可能吗?进程是一个很笨重的东西。一台服务器上创建不了多少个进程。
11.1.2 结论
进程在 Linux 上是一个开销不小的家伙,先不说创建,光是上下文切换一次就得几个微秒。所以为了高效地对海量用户提供服务,必须要让一个进程能同时处理很多个 tcp 连接才行。现在假设一个进程保持了 10000 条连接,那么如何发现哪条连接上有数据可读了、哪条连接可写了 ?
我们当然可以采用循环遍历的方式来发现 IO 事件,但这种方式太低级了。
我们希望有一种更高效的机制,在很多连接中的某条上有 IO 事件发生的时候直接快速把它找出来。
其实这个事情 Linux 操作系统已经替我们都做好了,它就是我们所熟知的 IO 多路复用机制。
这里的复用指的就是对进程的复用
11.2 I/O多路复用模型
11.2.1 是什么
I/O :网络 I/O
多路:多个客户端连接(连接就是套接字描述符,即 socket 或者 channel),指的是多条 TCP 连接
复用:用一个进程来处理多条的连接,使用单进程就能够实现同时处理多个客户端的连接
一句话:
实现了用一个进程来处理大量的用户连接
IO多路复用类似一个规范和接口,落地实现:可以分select->poll->epoll三个阶段来描述。
动画演示
11.2.2 Redis单线程如何处理那么多并发客户端连接,为什么单线程,为什么快
Redis的IO多路复用
Redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,一次放到文件事件分派器,事件分派器将事件分发给事件处理器。
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现 所谓 I/O 多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。这种机制的使用需要 select 、 poll 、 epoll 来配合。 多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。 Redis 服务采用 Reactor 的方式来实现文件事件处理器(每一个网络连接其实都对应一个文件描述符) Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分: 多个套接字、 IO多路复用程序、 文件事件分派器、 事件处理器。 因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型 |
11.2.3 参考《Redis 设计与实现》
结论
|-----------------------------------------------------------------------------------|
| 从Redis6开始,将网络数据读写、请求协议解析通过多个IO线程的来处理 , 对于真正的命令执行来说,仍然使用单线程操作,一举两得,便宜占尽!!! o( ̄▽ ̄)d |
11.2.4 从吃米线开始,读读read...
从吃米线开始,读读read...
上午开会,错过了公司食堂的饭点, 中午就和公司的首席架构师一起去楼下的米线店去吃米线。我们到了一看,果然很多人在排队。
架构师马上发话了:嚯,请求排队啊!你看这位收银点菜的,像不像nginx的反向代理?只收请求,不处理,把请求都发给后厨去处理。
我们交了钱,拿着号离开了点餐收银台,找了个座位坐下等餐。
架构师:你看,这就是异步处理,我们下了单就可以离开等待,米线做好了会通过小喇叭"回调"我们去取餐;
如果同步处理,我们就得在收银台站着等餐,后面的请求无法处理,客户等不及肯定会离开了。
接下里架构师盯着手中的纸质号牌。
架构师:你看,这个纸质号牌在后厨"服务器"那里也有,这不就是表示会话的ID吗?
有了它就可以把大家给区分开,就不会把我的排骨米线送给别人了。过了一会, 排队的人越来越多,已经有人表示不满了,可是收银员已经满头大汗,忙到极致了。
架构师:你看他这个系统缺乏弹性扩容, 现在这么多人,应该增加收银台,可是没有其他收银设备,老板再着急也没用。
老板看到在收银这里帮不了忙,后厨的订单也累积得越来越多, 赶紧跑到后厨亲自去做米线去了。
架构师又发话了:幸亏这个系统的后台有并行处理能力,可以随意地增加资源来处理请求(做米线)。
我说:他就这点儿资源了,除了老板没人再会做米线了。
不知不觉,我们等了20分钟, 但是米线还没上来。
架构师:你看,系统的处理能力达到极限,超时了吧。
这时候收银台前排队的人已经不多了,但是还有很多人在等米线。
老板跑过来让这个打扫卫生的去收银,让收银小妹也到后厨帮忙。打扫卫生的做收银也磕磕绊绊的,没有原来的小妹灵活。
架构师:这就叫服务降级,为了保证米线的服务,把别的服务都给关闭了。
又过了20分钟,后厨的厨师叫道:237号, 您点的排骨米线没有排骨了,能换成番茄的吗?
架构师低声对我说:瞧瞧, 人太多, 系统异常了。然后他站了起来:不行,系统得进行补偿操作:退费。
说完,他拉着我,饿着肚子,头也不回地走了。
同步
调用者要一直等待调用结果的通知后才能进行后续的执行,现在就要,我可以等,等出结果为止
异步
指被调用方先返回应答让调用者先回去,然后再计算调用结果,计算完最终结果后再通知并返回给调用方
异步调用要想获得结果一般通过回调
同步与异步的理解
同步、异步的讨论对象是被调用者(服务提供者),重点在于获得调用结果的消息通知方式上
阻塞
调用方一直在等待而且别的事情什么都不做,当前进/线程会被挂起,啥都不干
非阻塞
调用在发出去后,调用方先去忙别的事情,不会阻塞当前进/线程,而会立即返回
阻塞与非阻塞的理解
阻塞、非阻塞的讨论对象是调用者(服务请求者),重点在于等消息时候的行为,调用者是否能干其它事
总结
4种组合方式:
同步阻塞:服务员说快到你了,先别离开我后台看一眼马上通知你。客户在海底捞火锅前台干等着,啥都不干。
同步非阻塞:服务员说快到你了,先别离开。客户在海底捞火锅前台边刷抖音边等着叫号
异步阻塞:服务员说还要再等等,你先去逛逛,一会儿通知你。客户怕过号在海底捞火锅前台拿着排号小票啥都不干,一直等着店员通知
异步非阻塞:服务员说还要再等等,你先去逛逛,一会儿通知你。拿着排号小票+刷着抖音,等着店员通知
11.2.5 Unix网络编程中的五种IO模型
Blocking IO - 阻塞IO
NoneBlocking IO - 非阻塞IO
IO multiplexing - IO多路复用
signal driven IO - 信号驱动IO 面试无关,暂不讲解
asynchronous IO - 异步IO 面试无关,暂不讲解
11.2.6 Java验证
11.2.6.1 背景
一个redisServer+2个Client
11.2.6.2 BIO
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。所以, BIO的特点就是在IO执行的两个阶段都被block了。 |
先演示accept
accept监听
code案例
RedisServer
java
package com.atguigu.redis7.iomultiplex.bio.accept;
import cn.hutool.core.util.IdUtil;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @auther zzyy
* @create 2021-06-01 10:29
*/
public class RedisServer
{
public static void main(String[] args) throws IOException
{
ServerSocket serverSocket = new ServerSocket(6379);
while(true)
{
System.out.println("模拟RedisServer启动-----111 等待连接");
Socket socket = serverSocket.accept();
System.out.println("-----222 成功连接: "+ IdUtil.simpleUUID());
System.out.println();
}
}
}
RedisClient01
java
package com.atguigu.redis7.iomultiplex.bio.accept;
import com.atguigu.redis7.demo.JedisDemo;
import java.io.IOException;
import java.net.Socket;
/**
* @auther zzyy
* @create 2021-06-01 10:31
*/
public class RedisClient01
{
public static void main(String[] args) throws IOException
{
System.out.println("------RedisClient01 start");
Socket socket = new Socket("127.0.0.1", 6379);
System.out.println("------RedisClient01 connection over");
}
}
RedisClient02
java
package com.atguigu.redis7.iomultiplex.bio.accept;
import java.io.IOException;
import java.net.Socket;
/**
* @auther zzyy
* @create 2021-06-01 10:32
*/
public class RedisClient02
{
public static void main(String[] args) throws IOException
{
System.out.println("------RedisClient02 start");
Socket socket = new Socket("127.0.0.1", 6379);
System.out.println("------RedisClient02 connection over");
}
}
再演示read
read读取
code案例
1
先启动RedisServerBIO,再启动RedisClient01验证后再启动2号客户端
RedisServerBIO
java
package com.atguigu.redis7.iomultiplex.bio.read;
import cn.hutool.core.util.IdUtil;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @auther zzyy
* @create 2021-06-01 10:35
*/
public class RedisServerBIO
{
public static void main(String[] args) throws IOException
{
ServerSocket serverSocket = new ServerSocket(6379);
while(true)
{
System.out.println("-----111 等待连接");
Socket socket = serverSocket.accept();//阻塞1 ,等待客户端连接
System.out.println("-----222 成功连接");
InputStream inputStream = socket.getInputStream();
int length = -1;
byte[] bytes = new byte[1024];
System.out.println("-----333 等待读取");
while((length = inputStream.read(bytes)) != -1)//阻塞2 ,等待客户端发送数据
{
System.out.println("-----444 成功读取"+new String(bytes,0,length));
System.out.println("===================="+"\t"+ IdUtil.simpleUUID());
System.out.println();
}
inputStream.close();
socket.close();
}
}
}
RedisClient01
java
package com.atguigu.redis7.iomultiplex.bio.read;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
/**
* @auther zzyy
* @create 2021-06-01 10:36
*/
public class RedisClient01
{
public static void main(String[] args) throws IOException
{
Socket socket = new Socket("127.0.0.1",6379);
OutputStream outputStream = socket.getOutputStream();
while(true)
{
Scanner scanner = new Scanner(System.in);
String string = scanner.next();
if (string.equalsIgnoreCase("quit")) {
break;
}
socket.getOutputStream().write(string.getBytes());
System.out.println("------RedisClient01 input quit keyword to finish......");
}
outputStream.close();
socket.close();
}
}
RedisClient02
java
package com.atguigu.redis7.iomultiplex.bio.read;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
/**
* @auther zzyy
* @create 2021-06-01 10:36
*/
public class RedisClient02
{
public static void main(String[] args) throws IOException
{
Socket socket = new Socket("127.0.0.1",6379);
OutputStream outputStream = socket.getOutputStream();
while(true)
{
Scanner scanner = new Scanner(System.in);
String string = scanner.next();
if (string.equalsIgnoreCase("quit")) {
break;
}
socket.getOutputStream().write(string.getBytes());
System.out.println("------RedisClient02 input quit keyword to finish......");
}
outputStream.close();
socket.close();
}
}
存在的问题
上面的模型存在很大的问题,如果客户端与服务端建立了连接,如果这个连接的客户端迟迟不发数据,进程就会一直堵塞在read()方法上,这样其他客户端也不能进行连接,也就是一次只能处理一个客户端,对客户很不友好
知道问题所在了,请问如何解决??
2
多线程模式
利用多线程
只要连接了一个socket,操作系统分配一个线程来处理,这样read()方法堵塞在每个具体线程上而不堵塞主线程,
就能操作多个socket了,哪个线程中的socket有数据,就读哪个socket,各取所需,灵活统一。
程序服务端只负责监听是否有客户端连接,使用 accept() 阻塞
客户端1连接服务端,就开辟一个线程(thread1)来执行 read() 方法,程序服务端继续监听
客户端2连接服务端,也开辟一个线程(thread2)来执行 read() 方法,程序服务端继续监听
客户端3连接服务端,也开辟一个线程(thread3)来执行 read() 方法,程序服务端继续监听
。。。。。。
任何一个线程上的socket有数据发送过来,read()就能立马读到,cpu就能进行处理。
RedisServerBIOMultiThread
java
package com.atguigu.redis7.iomultiplex.bio.read.mthread;
import cn.hutool.core.util.IdUtil;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @auther zzyy
* @create 2021-06-01 10:41
*/
public class RedisServerBIOMultiThread
{
public static void main(String[] args) throws IOException
{
ServerSocket serverSocket = new ServerSocket(6379);
while(true)
{
System.out.println("-----RedisServerBIOMultiThread 111 等待连接");
Socket socket = serverSocket.accept();//阻塞1 ,等待客户端连接
System.out.println("-----RedisServerBIOMultiThread 222 成功连接");
new Thread(() -> {
try {
InputStream inputStream = socket.getInputStream();
int length = -1;
byte[] bytes = new byte[1024];
System.out.println("-----333 等待读取"+ IdUtil.simpleUUID());
while((length = inputStream.read(bytes)) != -1)//阻塞2 ,等待客户端发送数据
{
System.out.println("-----444 成功读取"+new String(bytes,0,length));
System.out.println("====================");
System.out.println();
}
inputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
},Thread.currentThread().getName()).start();
new Thread().start();
}
}
}
RedisClient01
java
package com.atguigu.redis7.iomultiplex.bio.read.mthread;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
/**
* @auther zzyy
* @create 2021-06-01 10:42
*/
public class RedisClient01
{
public static void main(String[] args) throws IOException
{
Socket socket = new Socket("127.0.0.1",6379);
OutputStream outputStream = socket.getOutputStream();
while(true)
{
Scanner scanner = new Scanner(System.in);
String string = scanner.next();
if (string.equalsIgnoreCase("quit")) {
break;
}
socket.getOutputStream().write(string.getBytes());
System.out.println("------RedisClient01 input quit keyword to finish......");
}
outputStream.close();
socket.close();
}
}
RedisClient02
java
package com.atguigu.redis7.iomultiplex.bio.read.mthread;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
/**
* @auther zzyy
* @create 2021-06-01 10:42
*/
public class RedisClient02
{
public static void main(String[] args) throws IOException
{
Socket socket = new Socket("127.0.0.1",6379);
OutputStream outputStream = socket.getOutputStream();
while(true)
{
Scanner scanner = new Scanner(System.in);
String string = scanner.next();
if (string.equalsIgnoreCase("quit")) {
break;
}
socket.getOutputStream().write(string.getBytes());
System.out.println("------RedisClient02 input quit keyword to finish......");
}
outputStream.close();
socket.close();
}
}
存在的问题
多线程模型
每来一个客户端,就要开辟一个线程,如果来1万个客户端,那就要开辟1万个线程。
在操作系统中用户态不能直接开辟线程,需要调用内核来创建的一个线程,
这其中还涉及到用户状态的切换(上下文的切换),十分耗资源。
知道问题所在了,请问如何解决??
解决
第一个办法:使用线程池
这个在客户端连接少的情况下可以使用,但是用户量大的情况下,你不知道线程池要多大,太大了内存可能不够,也不可行。
第二个办法:NIO(非阻塞式IO)方式
因为read()方法堵塞了,所有要开辟多个线程,如果什么方法能使read()方法不堵塞,这样就不用开辟多个线程了,这就用到了另一个IO模型,NIO(非阻塞式IO)
总结
tomcat7之前就是用BIO多线程来解决多连接
目前我们的两个痛点
两个痛点
accept
read
在阻塞式 I/O 模型中,应用程序在从调用 recvfrom开始到它返回有数据报准备好这段时间是阻塞的,recvfrom返回成功后,应用进程才能开始处理数据报。
阻塞式IO小总结
思考
每个线程分配一个连接,必然会产生多个,既然是多个socket链接必然需要放入进容器,纳入统一管理
11.2.6.3 NIO
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。 所以,NIO特点是用户进程需要不断的主动询问内核数据准备好了吗?一句话,用轮询替代阻塞! |
先把面试回答拿下
在NIO模式中,一切都是非阻塞的:
accept()方法是非阻塞的,如果没有客户端连接,就返回无连接标识
read()方法是非阻塞的,如果read()方法读取不到数据就返回空闲中标识,如果读取到数据时只阻塞read()方法读数据的时间
在NIO模式中,只有一个线程:
当一个客户端与服务端进行连接,这个socket就会加入到一个数组中,隔一段时间遍历一次,
看这个socket的read()方法能否读到数据,这样一个线程就能处理多个客户端的连接和读取了
code案例
上述以前的socket是阻塞的,另外开发一套API
ServerSocketChannel
RedisServerNIO
java
package com.atguigu.redis7.iomultiplex.nio;
import cn.hutool.core.util.IdUtil;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2021-06-01 10:30
*/
public class RedisServerNIO
{
static ArrayList<SocketChannel> socketList = new ArrayList<>();
static ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
public static void main(String[] args) throws IOException
{
System.out.println("---------RedisServerNIO 启动等待中......");
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.bind(new InetSocketAddress("127.0.0.1",6379));
serverSocket.configureBlocking(false);//设置为非阻塞模式
/**
* select 其实就是把NIO中用户态要遍历的fd数组(我们的每一个socket链接,安装进ArrayList里面的那个)拷贝到了内核态,
* 让内核态来遍历,因为用户态判断socket是否有数据还是要调用内核态的,所有拷贝到内核态后,
* 这样遍历判断的时候就不用一直用户态和内核态频繁切换了
*/
while (true) {
for (SocketChannel element : socketList) {
int read = element.read(byteBuffer);
if(read > 0)
{
System.out.println("-----读取数据: "+read);
byteBuffer.flip();
byte[] bytes = new byte[read];
byteBuffer.get(bytes);
System.out.println(new String(bytes));
byteBuffer.clear();
}
}
SocketChannel socketChannel = serverSocket.accept();
if(socketChannel != null) {
System.out.println("-----成功连接: ");
socketChannel.configureBlocking(false);//设置为非阻塞模式
socketList.add(socketChannel);
System.out.println("-----socketList size: "+socketList.size());
}
}
}
}
RedisClient01
java
package com.atguigu.redis7.iomultiplex.nio;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
/**
* @auther zzyy
* @create 2021-06-01 11:05
*/
public class RedisClient01
{
public static void main(String[] args) throws IOException
{
System.out.println("------RedisClient01 start");
Socket socket = new Socket("127.0.0.1",6379);
OutputStream outputStream = socket.getOutputStream();
while(true)
{
Scanner scanner = new Scanner(System.in);
String string = scanner.next();
if (string.equalsIgnoreCase("quit")) {
break;
}
socket.getOutputStream().write(string.getBytes());
System.out.println("------input quit keyword to finish......");
}
outputStream.close();
socket.close();
}
}
RedisClient02
java
package com.atguigu.redis7.iomultiplex.nio;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
/**
* @auther zzyy
* @create 2021-06-01 11:05
*/
public class RedisClient02
{
public static void main(String[] args) throws IOException
{
System.out.println("------RedisClient02 start");
Socket socket = new Socket("127.0.0.1",6379);
OutputStream outputStream = socket.getOutputStream();
while(true)
{
Scanner scanner = new Scanner(System.in);
String string = scanner.next();
if (string.equalsIgnoreCase("quit")) {
break;
}
socket.getOutputStream().write(string.getBytes());
System.out.println("------input quit keyword to finish......");
}
outputStream.close();
socket.close();
}
}
存在的问题和优缺点
NIO成功的解决了BIO需要开启多线程的问题,NIO中一个线程就能解决多个socket,但是还存在2个问题。
问题一:
这个模型在客户端少的时候十分好用,但是客户端如果很多,
比如有1万个客户端进行连接,那么每次循环就要遍历1万个socket,如果一万个socket中只有10个socket有数据,也会遍历一万个socket,就会做很多无用功,每次遍历遇到 read 返回 -1 时仍然是一次浪费资源的系统调用。
问题二:
而且这个遍历过程是在用户态进行的,用户态判断socket是否有数据还是调用内核的read()方法实现的,这就涉及到用户态和内核态的切换,每遍历一个就要切换一次,开销很大因为这些问题的存在。
优点:不会阻塞在内核的等待数据过程,每次发起的 I/O 请求可以立即返回,不用阻塞等待,实时性较好。
缺点:轮询将会不断地询问内核,这将占用大量的 CPU 时间,系统资源利用率较低,所以一般 Web 服务器不使用这种 I/O 模型。
结论:让Linux内核搞定上述需求,我们将一批文件描述符通过一次系统调用传给内核由内核层去遍历,才能真正解决这个问题。IO多路复用应运而生,也即将上述工作直接放进Linux内核,不再两态转换而是直接从内核获得结果,因为内核是非阻塞的。
问题升级:如何用单线程处理大量的链接?
非阻塞式IO小总结
11.2.6.4 IO Multiplexing(IO多路复用)
11.2.6.4.1 是什么
词牌
模型
I/O多路复用在英文中其实叫 I/O multiplexing
多个Socket复用一根网线这个功能是在内核+驱动层实现的
I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态来同时管理多个I/O流. 目的是尽量多的提高服务器的吞吐能力。
大家都用过nginx,nginx使用epoll接收请求,ngnix会有很多链接进来, epoll会把他们都监视起来,然后像拨开关一样,谁有数据就拨向谁,然后调用相应的代码处理。redis类似同理
FileDescriptor
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
IO多路复用
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| IO multiplexing就是我们说的select,poll,epoll,有些技术书籍也称这种IO方式为event driven IO事件驱动IO。就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。可以基于一个阻塞对象并同时在多个描述符上等待就绪,而不是使用多个线程(每个文件描述符一个线程,每次new一个线程),这样可以大大节省系统资源。所以,I/O 多路复用的特点是 通过一种机制一个进程能同时等待多个文件描述符而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select,poll,epoll等 函数就可以返回。 |
11.2.6.4.2 说人话
模拟一个tcp服务器处理30个客户socket,一个监考老师监考多个学生,谁举手就应答谁。
假设你是一个监考老师,让30个学生解答一道竞赛考题,然后负责验收学生答卷,你有下面几个选择:
第一种选择:按顺序逐个验收,先验收A,然后是B,之后是C、D。。。这中间如果有一个学生卡住,全班都会被耽误,你用循环挨个处理socket,根本不具有并发能力。
第二种选择:你创建30个分身线程,每个分身线程检查一个学生的答案是否正确。 这种类似于为每一个用户创建一个进程或者线程处理连接。
第三种选择,你站在讲台上等,谁解答完谁举手。这时C、D举手,表示他们解答问题完毕,你下去依次检查C、D的答案,然后继续回到讲台上等。此时E、A又举手,然后去处理E和A。。。这种就是IO复用模型。Linux下的select、poll和epoll就是干这个的。
将用户socket对应的fd注册进epoll,然后epoll帮你监听哪些socket上有消息到达,这样就避免了大量的无用操作。此时的socket应该采用非阻塞模式。这样,整个过程只在调用select、poll、epoll这些调用的时候才会阻塞,收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,这就是事件驱动,所谓的reactor反应模式。
11.2.6.4.3 能干嘛
Redis单线程如何处理那么多并发客户端连接,为什么单线程,为什么快
Redis的IO多路复用
Redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,依次放到事件分派器,事件分派器将事件分发给事件处理器。
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Redis 服务采用 Reactor 的方式来实现文件事件处理器(每一个网络连接其实都对应一个文件描述符) 所谓 I/O 多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。这种机制的使用需要 select 、 poll 、 epoll 来配合。多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。 |
| 所谓 I/O 多路复用机制,就是说通过一种考试监考机制,一个老师可以监视多个考生,一旦某个考生举手想要交卷了,能够通知监考老师进行相应的收卷子或批改检查操作。所以这种机制需要调用班主任(select/poll/epoll)来配合。多个考生被同一个班主任监考,收完一个考试的卷子再处理其它人,无需等待所有考生,谁先举手就先响应谁,当又有考生举手要交卷,监考老师看到后从讲台走到考生位置,开始进行收卷处理。 |
Reactor设计模式
是什么
基于 I/O 复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。
Reactor 模式,是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor 模式也叫 Dispatcher 模式。即 I/O 多了复用统一监听事件,收到事件后分发(Dispatch 给某进程),是编写高性能网络服务器的必备技术。===笔记最下方还有!
Reactor 模式中有 2 个关键组成:
1)Reactor:Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应。 它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人;
2)Handlers:处理程序执行 I/O 事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际办理人。Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作。
每一个网络连接其实都对应一个文件描述符
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Redis 服务采用 Reactor 的方式来实现文件事件处理器(每一个网络连接其实都对应一个文件描述符) Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。 它的组成结构为4部分: 多个套接字、 IO多路复用程序、 文件事件分派器、 事件处理器。 因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型 |
11.2.6.4.4 select, poll, epoll 都是I/O多路复用的具体的实现
C语言struct结构体语法简介
select方法
Linux官网或者man
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| select 函数监视的文件描述符分3类,分别是readfds、writefds和exceptfds,将用户传入的数组拷贝到内核空间 调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except)或超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。 当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。 |
select(2) - Linux manual pagehttps://man7.org/linux/man-pages/man2/select.2.html
select是第一个实现 (1983 左右在BSD里面实现)
用户态我们自己写的java代码思想
C语言代码
优点
select 其实就是把NIO中用户态要遍历的fd数组(我们的每一个socket链接,安装进ArrayList里面的那个)拷贝到了内核态,让内核态来遍历,因为用户态判断socket是否有数据还是要调用内核态的,所有拷贝到内核态后,这样遍历判断的时候就不用一直用户态和内核态频繁切换了
从代码中可以看出,select系统调用后,返回了一个置位后的&rset,这样用户态只需进行很简单的二进制比较,就能很快知道哪些socket需要read数据,有效提高了效率
缺点
1、bitmap最大1024位,一个进程最多只能处理1024个客户端
2、&rset不可重用,每次socket有数据就相应的位会被置位
3、文件描述符数组拷贝到了内核态(只不过无系统调用切换上下文的开销。(内核层可优化为异步事件通知)),仍然有开销。select 调用需要传入 fd 数组,需要拷贝一份到内核,高并发场景下这样的拷贝消耗的资源是惊人的。(可优化为不复制)
4、select并没有通知用户态哪一个socket有数据,仍然需要O(n)的遍历。select 仅仅返回可读文件描述符的个数,具体哪个可读还是要用户自己遍历。(可优化为只返回给用户就绪的文件描述符,无需用户做无效的遍历)
我们自己模拟写的是,RedisServerNIO.java,只不过将它内核化了。
select小结论
select方式,既做到了一个线程处理多个客户端连接(文件描述符),又减少了系统调用的开销(多个文件描述符只有一次 select 的系统调用 + N次就绪状态的文件描述符的 read 系统调用
poll方法
Linux官网或者man
poll(2) - Linux manual pagehttps://man7.org/linux/man-pages/man2/poll.2.html
1997年实现了poll
C语言代码
优点
1、poll使用pollfd数组来代替select中的bitmap,数组没有1024的限制,可以一次管理更多的client。它和 select 的主要区别就是,去掉了 select 只能监听 1024 个文件描述符的限制。
2、当pollfds数组中有事件发生,相应的revents置位为1,遍历的时候又置位回零,实现了pollfd数组的重用
问题
poll 解决了select缺点中的前两条,其本质原理还是select的方法,还存在select中原来的问题
1、pollfds数组拷贝到了内核态,仍然有开销
2、poll并没有通知用户态哪一个socket有数据,仍然需要O(n)的遍历
epoll方法
Linux官网或者man
|------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| int epoll_create(int size) | 参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议 |
| int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) | 见上图 |
| int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout) | 等待epfd上的io事件,最多返回maxevents个事件。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大。 |
epoll(7) - Linux manual pagehttps://man7.org/linux/man-pages/man7/epoll.7.html
在2002年被大神 Davide Libenzi (戴维德·利本兹)发明出来了
三步调用
epoll_create
创建一个 epoll 句柄
epoll_ctl
向内核添加、修改或删除要监控的文件描述符
epoll_wait
类似发起了select() 调用
C语言代码
结论
多路复用快的原因在于,操作系统提供了这样的系统调用,使得原来的 while 循环里多次系统调用,
变成了一次系统调用 + 内核层遍历这些文件描述符。
epoll是现在最先进的IO多路复用器,Redis、Nginx,linux中的Java NIO都使用的是epoll。
这里"多路"指的是多个网络连接,"复用"指的是复用同一个线程。
1、一个socket的生命周期中只有一次从用户态拷贝到内核态的过程,开销小
2、使用event事件通知机制,每次socket中有数据会主动通知内核,并加入到就绪链表中,不需要遍历所有的socket
在多路复用IO模型中,会有一个内核线程不断地去轮询多个 socket 的状态,只有当真正读写事件发送时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有真正有读写事件进行时,才会使用IO资源,所以它大大减少来资源占用。多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。 采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈
三个方法对比
11.2.6.4.5 五种 I/O 模型总结
多路复用快的原因在于,操作系统提供了这样的系统调用,使得原来的 while 循环里多次系统调用,
变成了一次系统调用 + 内核层遍历这些文件描述符。
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 所谓 I/O 多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。这种机制的使用需要 select 、 poll 、 epoll 来配合。多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理; |
11.2.6.4.6 为什么3个都保有
12 终章の总结
写进简历的靓点,腾讯面试题------如何做个迷你版的微信抢红包
12.1 案例实战:微信抢红包
12.1.1 业务描述
12.1.2 需求分析
1 各种节假日,发红包+抢红包,不说了,100%高并发业务要求,不能用mysql来做
2 一个总的大红包,会有可能拆分成多个小红包,总金额= 分金额1+分金额2+分金额3......分金额N
3 每个人只能抢一次,你需要有记录,比如100块钱,被拆分成10个红包发出去,
总计有10个红包,抢一个少一个,总数显示(10/6)直到完,需要记录那些人抢到了红包,重复抢作弊不可以。
4 有可能还需要你计时,完整抢完,从发出到全部over,耗时多少?
5 红包过期,或者群主人品差,没人抢红包,原封不动退回。
6 红包过期,剩余金额可能需要回退到发红包主账户下。
由于是高并发不能用mysql来做,只能用redis,那需要要redis的什么数据类型?
12.1.3 架构设计
难点:
1 拆分算法如何
红包其实就是金额,拆分算法如何 ?给你100块,分成10个小红包(金额有可能小概率相同,有2个红包都是2.58),
如何拆分随机金额设定每个红包里面安装多少钱?
2 次数限制
每个人只能抢一次,次数限制
3 原子性
每抢走一个红包就减少一个(类似减库存),那这个就需要保证库存的-----------------------原子性,不加锁实现
你认为存在redis什么数据类型里面?set ?hash? list?
12.1.3.1 关键点
发红包
抢红包
抢,不加锁且原子性,还能支持高并发
每人一次且有抢红包记录
记红包
记录每个人抢了多少
拆红包
拆红包算法:
1.所有人抢到金额之和等于红包金额,不能超过,也不能少于。
2.每个人至少抢到一分钱。
3.要保证所有人抢到金额的几率相等。
12.1.3.2 结论
抢红包业务通用算法
二倍均值法
剩余红包金额为M,剩余人数为N,那么有如下公式:
每次抢到的金额 = 随机区间 (0, (剩余红包金额M ÷ 剩余人数N ) X 2)
这个公式,保证了每次随机金额的平均值是相等的,不会因为抢红包的先后顺序而造成不公平。
举个例子:
假设有10个人,红包总额100元。
第1次:
100÷10 X2 = 20, 所以第一个人的随机范围是(0,20 ),平均可以抢到10元。假设第一个人随机到10元,那么剩余金额是100-10 = 90 元。
第2次:
90÷9 X2 = 20, 所以第二个人的随机范围同样是(0,20 ),平均可以抢到10元。假设第二个人随机到10元,那么剩余金额是90-10 = 80 元。
第3次:
80÷8 X2 = 20, 所以第三个人的随机范围同样是(0,20 ),平均可以抢到10元。 以此类推,每一次随机范围的均值是相等的。
12.1.4 编码实现
省略其它不重要的,只写Controller表示即可
coding
java
package com.atguigu.redis7.controller;
import cn.hutool.core.util.IdUtil;
import com.google.common.primitives.Ints;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2023-02-10 14:58
*/
@RestController
public class RedPackageController
{
public static final String RED_PACKAGE_KEY = "redpackage:";
public static final String RED_PACKAGE_CONSUME_KEY = "redpackage:consume:";
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping(value = "/send")
public String sendRedPackage(int totalMoney,int redPackageNumber)
{
//1 拆红包,将总金额totalMoney拆分为redPackageNumber个子红包
Integer[] splitRedPackages = splitRedPackageAlgorithm(totalMoney,redPackageNumber);//拆分红包算法通过后获得的多个子红包数组
//2 发红包并保存进list结构里面且设置过期时间
String key = RED_PACKAGE_KEY+IdUtil.simpleUUID();
redisTemplate.opsForList().leftPushAll(key,splitRedPackages);
redisTemplate.expire(key,1, TimeUnit.DAYS);
//3 发红包OK,返回前台显示
return key+"\t"+ Ints.asList(Arrays.stream(splitRedPackages).mapToInt(Integer::valueOf).toArray());
}
@RequestMapping(value = "/rob")
public String robRedPackage(String redPackageKey,String userId)
{
//1 验证某个用户是否抢过红包,不可以多抢
Object redPackage = redisTemplate.opsForHash().get(RED_PACKAGE_CONSUME_KEY + redPackageKey, userId);
//2 没有抢过可以去抢红包,否则返回-2表示该用户抢过红包了
if(null == redPackage)
{
//2.1 从大红包(list)里面出队一个作为该客户抢的红包,抢到了一个红包
Object partRedPackage = redisTemplate.opsForList().leftPop(RED_PACKAGE_KEY + redPackageKey);
if(partRedPackage != null)
{
//2.2 抢到红包后需要记录进入hash结构,表示谁抢到了多少钱的某个子红包
redisTemplate.opsForHash().put(RED_PACKAGE_CONSUME_KEY+redPackageKey,userId,partRedPackage);
System.out.println("用户:"+userId+"\t 抢到了多少钱的红包:"+partRedPackage);
//TODO 后续异步进mysql或者MQ进一步做统计处理,每一年你发出多少红包,抢到了多少红包,年度总结
return String.valueOf(partRedPackage);
}
// 抢完了
return "errorCode:-1,红包抢完了";
}
//3 某个用户抢过了,不可以作弊抢多次
return "errorCode:-2, message:"+userId+"\t"+"你已经抢过红包了,不能重新抢";
}
/**
* 拆红包的算法--->二倍均值算法
* @param totalMoney
* @param redPackageNumber
* @return
*/
private Integer[] splitRedPackageAlgorithm(int totalMoney,int redPackageNumber)
{
Integer[] redPackageNumbers = new Integer[redPackageNumber];
//已经被抢夺的红包金额,已经被拆分塞进子红包的金额
int useMoney = 0;
for (int i = 0; i < redPackageNumber; i++)
{
if(i == redPackageNumber - 1)
{
redPackageNumbers[i] = totalMoney - useMoney;
}else{
//二倍均值算法,每次拆分后塞进子红包的金额 = 随机区间(0,(剩余红包金额M ÷ 未被抢的剩余红包个数N) * 2)
int avgMoney = ((totalMoney - useMoney) / (redPackageNumber - i)) * 2;
redPackageNumbers[i] = 1 + new Random().nextInt(avgMoney - 1);
}
useMoney = useMoney + redPackageNumbers[i];
}
return redPackageNumbers;
}
}
12.1.5 多学一手
上述案例有许多红包记录了,如何批量删除