当前位置: 首页 > news >正文

浙江响应式网站建设公司博客集成wordpress

浙江响应式网站建设公司,博客集成wordpress,网络设计报告范例,百度法务部联系方式1 Redis单线程 VS 多线程(入门篇) 1.1 面试题 redis到底是单线程还是多线程#xff1f; IO多路复用听说过吗#xff1f; redis为什么快#xff1f; 1.2 Redis为什么选择单线程#xff1f; 1.2.1 是什么 这种问法其实并不严谨#xff0c;为啥这么说呢? Redis的版本…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/ORedis使用 I/O多路复用功能来监听多个 socket连接客户端这样就可以使用一个线程连接来处理多个请求减少线程切换带来的开销同时也避免了 I/O 阻塞操作 d避免上下文切换因为是单线程模型因此就避免了不必要的上下文切换和多线程竞争这就省去了多线程切换带来的时间和性能上的消耗而且单线程不会导致死锁问题的发生 作者原话官网证据 Docs 他的大体意思是说 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流. 一个服务端进程可以同时处理多个套接字描述符。 目的是尽量多的提高服务器的吞吐能力。 大家都用过nginxnginx使用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(▽) 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的语句(keykn,valuevn)写入到/tmp目录下的redisTest.txt文件中 for((i1;i100*10000;i)); do echo set k$i v$i /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        英文 Redis SCAN 命令 递增地遍历key空间        中文 一句话类似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.1sops 就不会剧烈抬升但是扫描的时间会变长 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 官网 SCAN | Docs 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你需要注意什么双检加锁策略你了解过吗如何尽量避免缓存击穿 fredis和mysql双写100%会出纰漏做不到强一致性你如何保证最终一致性 3.2 缓存双写一致性谈谈你的理解 如果redis中有数据需要和数据库中的值相同 如果redis中无数据数据库中的值要是最新值且准备回写redis 缓存按照操作来分细分2种只读缓存只作查询不需要回写读写缓存大部分需要回写机制 3.2.1 读写缓存 同步直写策略 写数据库后也同步写redis缓存缓存和数据库中的数据⼀致 对于读写缓存来说要想保证缓存和数据库中的数据⼀致就要采⽤同步直写策略 异步缓写策略 a正常业务运行中mysql数据变动了但是可以在业务上容许出现一定时间后才作用于redis比如仓库、物流系统 b异常情况出现了不得不将失败的动作重新修补有可能需要借助kafka或者RabbitMQ等消息中间件实现重试重写 3.2.2 一图代码如何写 问题上面业务逻辑你用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:;Resourceprivate UserMapper userMapper;Resourceprivate RedisTemplate redisTemplate;/*** 业务逻辑没有写错对于小厂中厂(QPS《1000)可以使用但是大厂不行* param id* return*/public User findUserById(Integer id){User user null;String key CACHE_KEY_USERid;//1 先从redis里面查询如果有直接返回结果如果没有再去查询mysqluser (User) redisTemplate.opsForValue().get(key);if(user null){//2 redis里面无继续查询mysqluser userMapper.selectByPrimaryKey(id);if(user null){//3.1 redismysql 都无数据//你具体细化防止多次穿透我们业务规定记录下导致穿透的这个key回写redisreturn 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_USERid;//1 先从redis里面查询如果有直接返回结果如果没有再去查询mysql// 第1次查询redis加锁前user (User) redisTemplate.opsForValue().get(key);if(user null) {//2 大厂用对于高QPS的优化进来就先加锁保证一个请求操作让外面的redis等待一下避免击穿mysqlsynchronized (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 java案例来源出处ClientExample · alibaba/canal Wiki · GitHub阿里巴巴 MySQL binlog 增量订阅消费组件 . Contribute to alibaba/canal development by creating an account on GitHub.https://github.com/alibaba/canal/wiki/ClientExample 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-binmysql-bin #开启 binlog binlog-formatROW #选择 ROW 模式 server_id1    #配置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 下载 Release v1.1.6 · alibaba/canal · GitHub阿里巴巴 MySQL binlog 增量订阅消费组件 . Contribute to alibaba/canal development by creating an account on GitHub.https://github.com/alibaba/canal/releases/tag/canal-1.1.6 下载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按照下面建表 CREATE TABLE t_user (id bigint(20) NOT NULL AUTO_INCREMENT,userName varchar(100) NOT NULL,PRIMARY KEY (id)) ENGINEInnoDB AUTO_INCREMENT10 DEFAULT CHARSETutf8mb4 4.4.3.2 建module canal_demo02 4.4.3.3 改POM ?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersiongroupIdcom.atguigu.canal/groupIdartifactIdcanal_demo02/artifactIdversion1.0-SNAPSHOT/versionparentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion2.5.14/versionrelativePath//parentpropertiesproject.build.sourceEncodingUTF-8/project.build.sourceEncodingmaven.compiler.source1.8/maven.compiler.sourcemaven.compiler.target1.8/maven.compiler.targetjunit.version4.12/junit.versionlog4j.version1.2.17/log4j.versionlombok.version1.16.18/lombok.versionmysql.version5.1.47/mysql.versiondruid.version1.1.16/druid.versionmapper.version4.1.5/mapper.versionmybatis.spring.boot.version1.3.0/mybatis.spring.boot.version/propertiesdependencies!--canal--dependencygroupIdcom.alibaba.otter/groupIdartifactIdcanal.client/artifactIdversion1.1.0/version/dependency!--SpringBoot通用依赖模块--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-actuator/artifactId/dependency!--swagger2--dependencygroupIdio.springfox/groupIdartifactIdspringfox-swagger2/artifactIdversion2.9.2/version/dependencydependencygroupIdio.springfox/groupIdartifactIdspringfox-swagger-ui/artifactIdversion2.9.2/version/dependency!--SpringBoot与Redis整合依赖--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependencydependencygroupIdorg.apache.commons/groupIdartifactIdcommons-pool2/artifactId/dependency!--SpringBoot与AOP--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-aop/artifactId/dependencydependencygroupIdorg.aspectj/groupIdartifactIdaspectjweaver/artifactId/dependency!--Mysql数据库驱动--dependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactIdversion5.1.47/version/dependency!--SpringBoot集成druid连接池--dependencygroupIdcom.alibaba/groupIdartifactIddruid-spring-boot-starter/artifactIdversion1.1.10/version/dependencydependencygroupIdcom.alibaba/groupIdartifactIddruid/artifactIdversion${druid.version}/version/dependency!--mybatis和springboot整合--dependencygroupIdorg.mybatis.spring.boot/groupIdartifactIdmybatis-spring-boot-starter/artifactIdversion${mybatis.spring.boot.version}/version/dependency!--通用基础配置junit/devtools/test/log4j/lombok/hutool--!--hutool--dependencygroupIdcn.hutool/groupIdartifactIdhutool-all/artifactIdversion5.2.3/version/dependencydependencygroupIdjunit/groupIdartifactIdjunit/artifactIdversion${junit.version}/version/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependencydependencygroupIdlog4j/groupIdartifactIdlog4j/artifactIdversion${log4j.version}/version/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdversion${lombok.version}/versionoptionaltrue/optional/dependency!--persistence--dependencygroupIdjavax.persistence/groupIdartifactIdpersistence-api/artifactIdversion1.0.2/version/dependency!--通用Mapper--dependencygroupIdtk.mybatis/groupIdartifactIdmapper/artifactIdversion${mapper.version}/version/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-autoconfigure/artifactId/dependencydependencygroupIdredis.clients/groupIdartifactIdjedis/artifactIdversion3.8.0/version/dependency/dependenciesbuildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactId/plugin/plugins/build/project 4.4.3.4 写YML server.port5555# alibaba.druid spring.datasource.typecom.alibaba.druid.pool.DruidDataSource spring.datasource.driver-class-namecom.mysql.jdbc.Driver spring.datasource.urljdbc:mysql://localhost:3306/bigdata?useUnicodetruecharacterEncodingutf-8useSSLfalse spring.datasource.usernameroot spring.datasource.password123456 spring.datasource.druid.test-while-idlefalse 4.4.3.5 主启动 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 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 jedisPoolConfignew JedisPoolConfig();jedisPoolConfig.setMaxTotal(20);jedisPoolConfig.setMaxIdle(10);jedisPoolnew 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 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(ListColumn 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(ListColumn 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(ListColumn 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(ListEntry 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(gt; 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.UPDATEredisUpdate(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 VisitorUV量。 。。。。。。 痛点 类似今日头条、抖音、淘宝这样的额用户访问级别都是亿级的请问如何处理 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 傻Xo(╥﹏╥)o不解释 用redis的hash结构存储 说明 redis——hash keyDay,ip,1 按照ipv4的结构来说明每个ipv4的地址最多是15个字节(ip 192.168.111.1最多xxx.xxx.xxx.xxx) 某一天的1.5亿 * 15个字节 2G一个月60Gredis死定了。o(╥﹏╥)o hyperloglog 为什么是只需要花费12Kb  5.3.5.3 HyperLogLogService 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 {Resourceprivate RedisTemplate redisTemplate;/*** 模拟后台有用户点击首页每个用户来自不同ip地址*/PostConstructpublic 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 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 {Resourceprivate RedisTemplate redisTemplate;ApiOperation(获得IP去重后的首页访问量)RequestMapping(value /uv,method RequestMethod.GET)public long uv(){//pfcountreturn 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 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 {Resourceprivate 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 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;Autowiredprivate RedisTemplate redisTemplate;public String geoAdd(){MapString, 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) {//获取经纬度坐标ListPoint list this.redisTemplate.opsForGeo().position(CITY,member);return list.get(0);}public String hash(String member) {//geohash算法生成的base32编码值ListString 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.914402Circle circle new Circle(116.418017, 39.914402, Metrics.KILOMETERS.getMultiplier());//返回50条RedisGeoCommands.GeoRadiusCommandArgs args RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(50);GeoResultsRedisGeoCommands.GeoLocationString 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 distancenew Distance(10, Metrics.KILOMETERS);GeoResultsRedisGeoCommands.GeoLocationString 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 TABLE user_sign (   keyid BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,   user_key VARCHAR(200),#京东用户ID   sign_date DATETIME,#签到日期(20210618)   sign_count INT #连续签到天数 ) INSERT INTO user_sign(user_key,sign_date,sign_count)VALUES (20210618-xxxx-xxxx-xxxx-xxxxxxxxxxxx,2020-06-18 15:11:12,1); SELECT     sign_countFROM     user_signWHERE     user_key  20210618-xxxx-xxxx-xxxx-xxxxxxxxxxxx     AND sign_date BETWEEN 2020-06-17 00:00:00 AND 2020-06-18 23:59:59ORDER BY     sign_date DESC     LIMIT 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 Byte1000W 用户量一年也只需要 44 MB 就足够了。 假如是亿级的系统 每天使用1个1亿位的Bitmap约占12MB的内存10^8/8/1024/102410天的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号位对应用户iduid-092iok-lkj 比如1号位对应用户iduid-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案例 /*** 模拟演示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();SetInteger 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 springbootredismybatis案例基础与一键编码环境整合 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 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)) ENGINEInnoDB AUTO_INCREMENT10 DEFAULT CHARSETutf8mb4 建springboot的Module mybatis_generator 改POM ?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersiongroupIdcom.atguigu.redis7/groupIdartifactIdmybatis_generator/artifactIdversion1.0-SNAPSHOT/versionparentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion2.6.10/versionrelativePath//parentproperties!-- 依赖版本号 --project.build.sourceEncodingUTF-8/project.build.sourceEncodingmaven.compiler.source1.8/maven.compiler.sourcemaven.compiler.target1.8/maven.compiler.targetjava.version1.8/java.versionhutool.version5.5.8/hutool.versiondruid.version1.1.18/druid.versionmapper.version4.1.5/mapper.versionpagehelper.version5.1.4/pagehelper.versionmysql.version5.1.39/mysql.versionswagger2.version2.9.2/swagger2.versionswagger-ui.version2.9.2/swagger-ui.versionmybatis.spring.version2.1.3/mybatis.spring.version/propertiesdependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency!--Mybatis 通用mapper tk单独使用自己带着版本号--dependencygroupIdorg.mybatis/groupIdartifactIdmybatis/artifactIdversion3.4.6/version/dependency!--mybatis-spring--dependencygroupIdorg.mybatis.spring.boot/groupIdartifactIdmybatis-spring-boot-starter/artifactIdversion${mybatis.spring.version}/version/dependency!-- Mybatis Generator --dependencygroupIdorg.mybatis.generator/groupIdartifactIdmybatis-generator-core/artifactIdversion1.4.0/versionscopecompile/scopeoptionaltrue/optional/dependency!--通用Mapper--dependencygroupIdtk.mybatis/groupIdartifactIdmapper/artifactIdversion${mapper.version}/version/dependency!--persistence--dependencygroupIdjavax.persistence/groupIdartifactIdpersistence-api/artifactIdversion1.0.2/version/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdoptionaltrue/optional/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scopeexclusionsexclusiongroupIdorg.junit.vintage/groupIdartifactIdjunit-vintage-engine/artifactId/exclusion/exclusions/dependency/dependenciesbuildresourcesresourcedirectory${basedir}/src/main/java/directoryincludesinclude**/*.xml/include/includes/resourceresourcedirectory${basedir}/src/main/resources/directory/resource/resourcespluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactIdconfigurationexcludesexcludegroupIdorg.projectlombok/groupIdartifactIdlombok/artifactId/exclude/excludes/configuration/pluginplugingroupIdorg.mybatis.generator/groupIdartifactIdmybatis-generator-maven-plugin/artifactIdversion1.3.6/versionconfigurationconfigurationFile${basedir}/src/main/resources/generatorConfig.xml/configurationFileoverwritetrue/overwriteverbosetrue/verbose/configurationdependenciesdependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactIdversion${mysql.version}/version/dependencydependencygroupIdtk.mybatis/groupIdartifactIdmapper/artifactIdversion${mapper.version}/version/dependency/dependencies/plugin/plugins/build /project 写YML 无 mgb配置相关src\main\resources路径下新建 config.properties #t_customer表包名 package.namecom.atguigu.redis7jdbc.driverClass com.mysql.jdbc.Driver jdbc.url jdbc:mysql://localhost:3306/bigdata jdbc.user root jdbc.password 123456 generatorConfig.xml ?xml version1.0 encodingUTF-8? !DOCTYPE generatorConfigurationPUBLIC -//mybatis.org//DTD MyBatis Generator Configuration 1.0//ENhttp://mybatis.org/dtd/mybatis-generator-config_1_0.dtdgeneratorConfigurationproperties resourceconfig.properties/context idMysql targetRuntimeMyBatis3Simple defaultModelTypeflatproperty namebeginningDelimiter value/property nameendingDelimiter value/plugin typetk.mybatis.mapper.generator.MapperPluginproperty namemappers valuetk.mybatis.mapper.common.Mapper/property namecaseSensitive valuetrue//pluginjdbcConnection driverClass${jdbc.driverClass}connectionURL${jdbc.url}userId${jdbc.user}password${jdbc.password}/jdbcConnectionjavaModelGenerator targetPackage${package.name}.entities targetProjectsrc/main/java/sqlMapGenerator targetPackage${package.name}.mapper targetProjectsrc/main/java/javaClientGenerator targetPackage${package.name}.mapper targetProjectsrc/main/java typeXMLMAPPER/table tableNamet_customer domainObjectNameCustomergeneratedKey columnid sqlStatementJDBC//table/context /generatorConfiguration 一键生成 双击插件mybatis-generator:gererate 一键生成 生成entitymapper接口xml实现SQL 6.6.2.2 SpringBoot  Mybatis  Redis缓存实战编码 建Module 改造我们的redis7_study工程 POM ?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersiongroupIdcom.atguigu.redis7/groupIdartifactIdredis7_study/artifactIdversion1.0-SNAPSHOT/versionparentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion2.6.10/versionrelativePath//parentpropertiesproject.build.sourceEncodingUTF-8/project.build.sourceEncodingmaven.compiler.source1.8/maven.compiler.sourcemaven.compiler.target1.8/maven.compiler.targetjunit.version4.12/junit.versionlog4j.version1.2.17/log4j.versionlombok.version1.16.18/lombok.version/propertiesdependencies!--SpringBoot通用依赖模块--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency!--jedis--dependencygroupIdredis.clients/groupIdartifactIdjedis/artifactIdversion4.3.1/version/dependency!--lettuce--!--dependencygroupIdio.lettuce/groupIdartifactIdlettuce-core/artifactIdversion6.2.1.RELEASE/version/dependency--!--SpringBoot与Redis整合依赖--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependencydependencygroupIdorg.apache.commons/groupIdartifactIdcommons-pool2/artifactId/dependency!--swagger2--dependencygroupIdio.springfox/groupIdartifactIdspringfox-swagger2/artifactIdversion2.9.2/version/dependencydependencygroupIdio.springfox/groupIdartifactIdspringfox-swagger-ui/artifactIdversion2.9.2/version/dependency!--Mysql数据库驱动--dependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactIdversion5.1.47/version/dependency!--SpringBoot集成druid连接池--dependencygroupIdcom.alibaba/groupIdartifactIddruid-spring-boot-starter/artifactIdversion1.1.10/version/dependencydependencygroupIdcom.alibaba/groupIdartifactIddruid/artifactIdversion1.1.16/version/dependency!--mybatis和springboot整合--dependencygroupIdorg.mybatis.spring.boot/groupIdartifactIdmybatis-spring-boot-starter/artifactIdversion1.3.0/version/dependency!--hutool--dependencygroupIdcn.hutool/groupIdartifactIdhutool-all/artifactIdversion5.2.3/version/dependency!--persistence--dependencygroupIdjavax.persistence/groupIdartifactIdpersistence-api/artifactIdversion1.0.2/version/dependency!--通用Mapper--dependencygroupIdtk.mybatis/groupIdartifactIdmapper/artifactIdversion4.1.5/version/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-autoconfigure/artifactId/dependency!--通用基础配置junit/devtools/test/log4j/lombok/--dependencygroupIdjunit/groupIdartifactIdjunit/artifactIdversion${junit.version}/version/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependencydependencygroupIdlog4j/groupIdartifactIdlog4j/artifactIdversion${log4j.version}/version/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdversion${lombok.version}/versionoptionaltrue/optional/dependency/dependenciesbuildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactId/plugin/plugins/build/project YML server.port7777spring.application.nameredis7_study# logging logging.level.rootinfo logging.level.com.atguigu.redis7info logging.pattern.console%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n logging.file.nameD:/mylogs2023/redis7_study.log logging.pattern.file%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n# swagger spring.swagger2.enabledtrue #在springboot2.6.X结合swagger2.9.X会提示documentationPluginsBootstrapper空指针异常 #原因是在springboot2.6.X中将SpringMVC默认路径匹配策略从AntPathMatcher更改为PathPatternParser # 导致出错解决办法是matching-strategy切换回之前ant_path_matcher spring.mvc.pathmatch.matching-strategyant_path_matcher# redis单机 spring.redis.database0 # 修改为自己真实IP spring.redis.host192.168.111.185 spring.redis.port6379 spring.redis.password111111 spring.redis.lettuce.pool.max-active8 spring.redis.lettuce.pool.max-wait-1ms spring.redis.lettuce.pool.max-idle8 spring.redis.lettuce.pool.min-idle0# alibaba.druid spring.datasource.typecom.alibaba.druid.pool.DruidDataSource spring.datasource.driver-class-namecom.mysql.jdbc.Driver spring.datasource.urljdbc:mysql://localhost:3306/bigdata?useUnicodetruecharacterEncodingutf-8useSSLfalse spring.datasource.usernameroot spring.datasource.password123456 spring.datasource.druid.test-while-idlefalse# mybatis mybatis.mapper-locationsclasspath:mapper/*.xml mybatis.type-aliases-packagecom.atguigu.redis7.entities# redis集群 #spring.redis.password111111 ## 获取失败 最大重定向次数 #spring.redis.cluster.max-redirects3 #spring.redis.lettuce.pool.max-active8 #spring.redis.lettuce.pool.max-wait-1ms #spring.redis.lettuce.pool.max-idle8 #spring.redis.lettuce.pool.min-idle0 ##支持集群拓扑动态感应刷新,自适应拓扑刷新是否使用所有可用的更新默认false关闭 #spring.redis.lettuce.cluster.refresh.adaptivetrue ##定时刷新 #spring.redis.lettuce.cluster.refresh.period2000 #spring.redis.cluster.nodes192.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  主启动 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 上一步自动生成的拷贝过来 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 {IdGeneratedValue(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;}Overridepublic String toString(){return Customer{ id id , cname cname \ , age age , phone phone \ , sex sex , birth birth };} } mapper接口 package com.atguigu.redis7.mapper;import com.atguigu.redis7.entities.Customer; import tk.mybatis.mapper.common.Mapper;public interface CustomerMapper extends MapperCustomer { } mapperSQL文件 ?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.atguigu.redis7.mapper.CustomerMapperresultMap idBaseResultMap typecom.atguigu.redis7.entities.Customer!--WARNING - mbg.generated--id columnid jdbcTypeINTEGER propertyid /result columncname jdbcTypeVARCHAR propertycname /result columnage jdbcTypeINTEGER propertyage /result columnphone jdbcTypeVARCHAR propertyphone /result columnsex jdbcTypeTINYINT propertysex /result columnbirth jdbcTypeTIMESTAMP propertybirth //resultMap /mapper service类 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:;Resourceprivate CustomerMapper customerMapper;Resourceprivate RedisTemplate redisTemplate;public void addCustomer(Customer customer){int i customerMapper.insertSelective(customer);if(i 0){//到数据库里面重新捞出新数据出来做缓存customercustomerMapper.selectByPrimaryKey(customer.getId());//缓存keyString keyCACHE_KEY_CUSTOMERcustomer.getId();//往mysql里面插入成功随后再从mysql查询出来再插入redisredisTemplate.opsForValue().set(key,customer);}}public Customer findCustomerById(Integer customerId){Customer customer null;//缓存key的名称String keyCACHE_KEY_CUSTOMERcustomerId;//1 查询rediscustomer (Customer) redisTemplate.opsForValue().get(key);//redis无进一步查询mysqlif(customernull){//2 从mysql查出来customercustomercustomerMapper.selectByPrimaryKey(customerId);// mysql有redis无if (customer ! null) {//3 把mysql捞到的数据写入redis方便下次查询能redis命中。redisTemplate.opsForValue().set(key,customer);}}return customer;}} controller 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(customeri);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(白名单) 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 {Resourceprivate 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的坑位将该值设置为1redisTemplate.opsForValue().setBit(whitelistCustomer,index,true);} } PostConstruct初始化白名单数据故意差异化数据演示效果...... CheckUtils 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 {Resourceprivate 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 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:;Resourceprivate CustomerMapper customerMapper;Resourceprivate RedisTemplate redisTemplate;Resourceprivate CheckUtils checkUtils;public void addCustomer(Customer customer){int i customerMapper.insertSelective(customer);if(i 0){//到数据库里面重新捞出新数据出来做缓存customercustomerMapper.selectByPrimaryKey(customer.getId());//缓存keyString keyCACHE_KEY_CUSTOMERcustomer.getId();//往mysql里面插入成功随后再从mysql查询出来再插入redisredisTemplate.opsForValue().set(key,customer);}}public Customer findCustomerById(Integer customerId){Customer customer null;//缓存key的名称String keyCACHE_KEY_CUSTOMERcustomerId;//1 查询rediscustomer (Customer) redisTemplate.opsForValue().get(key);//redis无进一步查询mysqlif(customernull){//2 从mysql查出来customercustomercustomerMapper.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*/Resourceprivate 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 查询rediscustomer (Customer) redisTemplate.opsForValue().get(key);//redis无进一步查询mysqlif (customer null) {//2 从mysql查出来customercustomer customerMapper.selectByPrimaryKey(customerId);// mysql有redis无if (customer ! null) {//3 把mysql捞到的数据写入redis方便下次查询能redis命中。redisTemplate.opsForValue().set(key, customer);}}return customer;} } CustomerController 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(customeri);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 FilterBetter Than Bloom》 https://www.cs.cmu.edu/~binfan/papers/conext14_cuckoofilter.pdf#:~:textCuckoo%20%EF%AC%81lters%20support%20adding%20and%20removing%20items%20dynamically,have%20lower%20space%20overhead%20than%20space-optimized%20Bloom%20%EF%AC%81lters. 作者将布谷鸟过滤器和布隆过滤器进行了深入的对比有兴趣的同学可以自己看看。不过按照阳哥企业调研目前用的比较多比较成熟的就是布隆过滤器企业暂时没有升级换代的需求考虑到上课时间有限在此不再展开。 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版 云数据库 Redis_缓存数据库_高并发_读写分离-阿里云云数据库 Redis 版是阿里云推出的一种全托管、兼容Redis协议的缓存数据库服务具备低延迟、高并发、弹性扩缩容的特点支持读写分离适合大规模高性能需求的在线数据业务。https://www.aliyun.com/product/kvstore?spm5176.54432.J_3207526240.15.2a3818a5iG19IE 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:abcdxxxredis和mysql都没有返回null给调用者但是增强回写后第二次来查uid:abcdxxx此时redis就有值了。 可以直接从Redis中读取default缺省值返回给业务应用程序避免了把大量请求发送给mysql处理打爆mysql。 但是此方法架不住黑客的恶意攻击有缺陷......只能解决key相同的情况 黑客或者恶意攻击 黑客会对你的系统进行攻击拿一个不存在的id​去查询数据会产生大量的请求到数据库去查询。可能会导致你的数据库由于压力过大而宕掉 key相同打你系统 第一次打到mysql空对象缓存后第二次就返回defaultNull缺省值避免mysql被攻击不用再到数据库中去走一圈了 key不同打你系统 由于存在空对象缓存和缓存回写(看自己业务不限死)redis中的无关紧要的key也会越写越多(记得设置redis过期时间) 7.4.3 方案2Google布隆过滤器Guava解决缓存穿透 Guava 中布隆过滤器的实现算是比较权威的所以实际项目中我们可以直接使用Guava布隆过滤器 Guava’s BloomFilter源码出处 guava/guava/src/com/google/common/hash/BloomFilter.java at master · google/guava · GitHubGoogle core libraries for Java. Contribute to google/guava development by creating an account on GitHub.https://github.com/google/guava/blob/master/guava/src/com/google/common/hash/BloomFilter.java 7.4.3.1 案例白名单过滤器 白名单架构说明 误判问题但是概率小可以接受不能从布隆过滤器删除 全部合法的key都需要放入Guava版布隆过滤器redis里面不然数据就是返回null Coding实战 建Module 修改redis7_study 改POM ?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersiongroupIdcom.atguigu.redis7/groupIdartifactIdredis7_study/artifactIdversion1.0-SNAPSHOT/versionparentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion2.6.10/versionrelativePath//parentpropertiesproject.build.sourceEncodingUTF-8/project.build.sourceEncodingmaven.compiler.source1.8/maven.compiler.sourcemaven.compiler.target1.8/maven.compiler.targetjunit.version4.12/junit.versionlog4j.version1.2.17/log4j.versionlombok.version1.16.18/lombok.version/propertiesdependencies!--guava Google 开源的 Guava 中自带的布隆过滤器--dependencygroupIdcom.google.guava/groupIdartifactIdguava/artifactIdversion23.0/version/dependency!--SpringBoot通用依赖模块--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency!--jedis--dependencygroupIdredis.clients/groupIdartifactIdjedis/artifactIdversion4.3.1/version/dependency!--lettuce--!--dependencygroupIdio.lettuce/groupIdartifactIdlettuce-core/artifactIdversion6.2.1.RELEASE/version/dependency--!--SpringBoot与Redis整合依赖--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependencydependencygroupIdorg.apache.commons/groupIdartifactIdcommons-pool2/artifactId/dependency!--swagger2--dependencygroupIdio.springfox/groupIdartifactIdspringfox-swagger2/artifactIdversion2.9.2/version/dependencydependencygroupIdio.springfox/groupIdartifactIdspringfox-swagger-ui/artifactIdversion2.9.2/version/dependency!--Mysql数据库驱动--dependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactIdversion5.1.47/version/dependency!--SpringBoot集成druid连接池--dependencygroupIdcom.alibaba/groupIdartifactIddruid-spring-boot-starter/artifactIdversion1.1.10/version/dependencydependencygroupIdcom.alibaba/groupIdartifactIddruid/artifactIdversion1.1.16/version/dependency!--mybatis和springboot整合--dependencygroupIdorg.mybatis.spring.boot/groupIdartifactIdmybatis-spring-boot-starter/artifactIdversion1.3.0/version/dependency!--hutool--dependencygroupIdcn.hutool/groupIdartifactIdhutool-all/artifactIdversion5.2.3/version/dependency!--persistence--dependencygroupIdjavax.persistence/groupIdartifactIdpersistence-api/artifactIdversion1.0.2/version/dependency!--通用Mapper--dependencygroupIdtk.mybatis/groupIdartifactIdmapper/artifactIdversion4.1.5/version/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-autoconfigure/artifactId/dependency!--通用基础配置junit/devtools/test/log4j/lombok/--dependencygroupIdjunit/groupIdartifactIdjunit/artifactIdversion${junit.version}/version/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependencydependencygroupIdlog4j/groupIdartifactIdlog4j/artifactIdversion${log4j.version}/version/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdversion${lombok.version}/versionoptionaltrue/optional/dependency/dependenciesbuildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactId/plugin/plugins/build/project 写YML server.port7777spring.application.nameredis7_study# logging logging.level.rootinfo logging.level.com.atguigu.redis7info logging.pattern.console%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n logging.file.nameD:/mylogs2023/redis7_study.log logging.pattern.file%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n# swagger spring.swagger2.enabledtrue #在springboot2.6.X结合swagger2.9.X会提示documentationPluginsBootstrapper空指针异常 #原因是在springboot2.6.X中将SpringMVC默认路径匹配策略从AntPathMatcher更改为PathPatternParser # 导致出错解决办法是matching-strategy切换回之前ant_path_matcher spring.mvc.pathmatch.matching-strategyant_path_matcher# redis单机 spring.redis.database0 # 修改为自己真实IP spring.redis.host192.168.111.185 spring.redis.port6379 spring.redis.password111111 spring.redis.lettuce.pool.max-active8 spring.redis.lettuce.pool.max-wait-1ms spring.redis.lettuce.pool.max-idle8 spring.redis.lettuce.pool.min-idle0# alibaba.druid spring.datasource.typecom.alibaba.druid.pool.DruidDataSource spring.datasource.driver-class-namecom.mysql.jdbc.Driver spring.datasource.urljdbc:mysql://localhost:3306/bigdata?useUnicodetruecharacterEncodingutf-8useSSLfalse spring.datasource.usernameroot spring.datasource.password123456 spring.datasource.druid.test-while-idlefalse# mybatis mybatis.mapper-locationsclasspath:mapper/*.xml mybatis.type-aliases-packagecom.atguigu.redis7.entities# redis集群 #spring.redis.password111111 ## 获取失败 最大重定向次数 #spring.redis.cluster.max-redirects3 #spring.redis.lettuce.pool.max-active8 #spring.redis.lettuce.pool.max-wait-1ms #spring.redis.lettuce.pool.max-idle8 #spring.redis.lettuce.pool.min-idle0 ##支持集群拓扑动态感应刷新,自适应拓扑刷新是否使用所有可用的更新默认false关闭 #spring.redis.lettuce.cluster.refresh.adaptivetrue ##定时刷新 #spring.redis.lettuce.cluster.refresh.period2000 #spring.redis.cluster.nodes192.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 主启动 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入门 /*** 创建guava版布隆过滤器,helloworld入门级演示*/ Test public void testGuavaWithBloomFilter() {//1 创建guava版布隆过滤器BloomFilterInteger bloomFilter BloomFilter.create(Funnels.integerFunnel(), 100);//2 判断指定的元素是否存在System.out.println(bloomFilter.mightContain(1));System.out.println(bloomFilter.mightContain(2));System.out.println();//3 讲元素新增进入bloomfilterbloomFilter.put(1);bloomFilter.put(2);System.out.println(bloomFilter.mightContain(1));System.out.println(bloomFilter.mightContain(2));} Case02 GuavaBloomFilterController 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 {Resourceprivate GuavaBloomFilterService guavaBloomFilterService;ApiOperation(guava布隆过滤器插入100万样本数据并额外10W测试是否存在)RequestMapping(value /guavafilter,method RequestMethod.GET)public void guavaBloomFilter(){guavaBloomFilterService.guavaBloomFilter();} } GuavaBloomFilterService 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 BloomFilterInteger 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个不在合法范围内的数据,来进行误判率的演示ArrayListInteger list new ArrayList(10 * _1W);//3 验证for (int i SIZE1; 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 springbootredis实现高并发的聚划算业务V2 建Module 修改redis7_study 改POM 无 写YML 无 主启动 无 业务类 entity 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 {//产品IDprivate Long id;//产品名称private String name;//产品价格private Integer price;//产品详情private String detail; } JHSTaskService 采用定时器将参与聚划算活动的特价商品新增进入redis中 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_KEYjhs;public static final String JHS_KEY_Ajhs:a;public static final String JHS_KEY_Bjhs:b;Autowiredprivate RedisTemplate redisTemplate;/*** 偷个懒不加mybatis了模拟从数据库读取100件特价商品用于加载到聚划算的页面中* return*/private ListProduct getProductsFromMysql() {ListProduct listnew ArrayList();for (int i 1; i 20; i) {Random rand new Random();int id rand.nextInt(10000);Product objnew Product((long) id,producti,i,detail);list.add(obj);}return list;}//PostConstructpublic void initJHS(){log.info(启动定时器天猫聚划算功能模拟开始......,O(∩_∩)O哈哈~);//1 用线程模拟定时任务后台任务定时将mysql里面的参加活动的商品刷新到redis里new Thread(() - {while (true){//2 模拟从mysql查出数据用于加载到redis并给聚划算页面显示ListProduct 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 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_KEYjhs;public static final String JHS_KEY_Ajhs:a;public static final String JHS_KEY_Bjhs:b;Autowiredprivate RedisTemplate redisTemplate;/*** 分页查询在高并发的情况下只能走redis查询走db的话必定会把db打垮* param page* param size* return*/RequestMapping(value /pruduct/find,method RequestMethod.GET)ApiOperation(聚划算案例每次1页每页5条显示)public ListProduct find(int page, int size) {ListProduct listnull;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网络抖动导致timeoutlog.error(jhs exception:{},e);e.printStackTrace();// ....再次查询mysql}return list;}} 备注 至此步骤上述聚划算的功能算是完成请思考在高并发下有什么经典生产问题 Bug和隐患说明 热点key突然失效导致可怕的缓存击穿 delete命令执行的一瞬间有空隙其它请求线程继续找Redis为null  打到了mysql暴击...... 复习again 最终目的 2条命令原子性还是其次主要是防止热key突然失效暴击mysql打爆系统 进一步升级加固案例 复习互斥跟新采用双检加锁策略 多个线程同时去查询数据库的这条数据那么我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它。 其他的线程走到这一步拿不到锁就等着等第一个线程查询到了数据然后做缓存。后面的线程进来发现已经有缓存了就直接走缓存。 差异失效时间 JHSTaskService 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_KEYjhs;public static final String JHS_KEY_Ajhs:a;public static final String JHS_KEY_Bjhs:b;Autowiredprivate RedisTemplate redisTemplate;/*** 偷个懒不加mybatis了模拟从数据库读取20件特价商品用于加载到聚划算的页面中* return*/private ListProduct getProductsFromMysql() {ListProduct listnew ArrayList();for (int i 1; i 20; i) {Random rand new Random();int id rand.nextInt(10000);Product objnew Product((long) id,producti,i,detail);list.add(obj);}return list;}//PostConstructpublic void initJHS(){log.info(启动定时器天猫聚划算功能模拟开始......,O(∩_∩)O哈哈~);//1 用线程模拟定时任务后台任务定时将mysql里面的参加活动的商品刷新到redis里new Thread(() - {while (true){//2 模拟从mysql查出数据用于加载到redis并给聚划算页面显示ListProduct 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();}PostConstructpublic void initJHSAB(){log.info(启动AB定时器计划任务天猫聚划算功能模拟..........DateUtil.now());//1 用线程模拟定时任务后台任务定时将mysql里面的参加活动的商品刷新到redis里new Thread(() - {while (true){//2 模拟从mysql查出数据用于加载到redis并给聚划算页面显示ListProduct 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 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_KEYjhs;public static final String JHS_KEY_Ajhs:a;public static final String JHS_KEY_Bjhs:b;Autowiredprivate RedisTemplate redisTemplate;/*** 分页查询在高并发的情况下只能走redis查询走db的话必定会把db打垮* param page* param size* return*/RequestMapping(value /pruduct/find,method RequestMethod.GET)ApiOperation(聚划算案例每次1页每页5条显示)public ListProduct find(int page, int size) {ListProduct listnull;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网络抖动导致timeoutlog.error(jhs exception:{},e);e.printStackTrace();// ....再次查询mysql}return list;}RequestMapping(value /pruduct/findab,method RequestMethod.GET)ApiOperation(AB双缓存架构防止热点key突然失效)public ListProduct findAB(int page, int size) {ListProduct listnull;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网络抖动导致timeoutlog.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  差评setnxexpire不安全两条命令非原子性的  set key value [EX seconds] [PX milliseconds] [NX|XX] 8.5 重点 JUC中AQS锁的规范落地参考可重入锁考虑Lua脚本Redis命令一步步实现分布式锁 8.6 Base案例(bootredis) 使用场景 多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击) 建Module redis_distributed_lock2 redis_distributed_lock3 改POM ?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersiongroupIdcom.atguigu.redislock/groupIdartifactIdredis_distributed_lock2/artifactIdversion1.0-SNAPSHOT/versionparentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion2.6.12/versionrelativePath/ !-- lookup parent from repository --/parentpropertiesproject.build.sourceEncodingUTF-8/project.build.sourceEncodingmaven.compiler.source8/maven.compiler.sourcemaven.compiler.target8/maven.compiler.targetlombok.version1.16.18/lombok.version/propertiesdependencies!--SpringBoot通用依赖模块--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency!--SpringBoot与Redis整合依赖--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependencydependencygroupIdorg.apache.commons/groupIdartifactIdcommons-pool2/artifactId/dependency!--swagger2--dependencygroupIdio.springfox/groupIdartifactIdspringfox-swagger2/artifactIdversion2.9.2/version/dependencydependencygroupIdio.springfox/groupIdartifactIdspringfox-swagger-ui/artifactIdversion2.9.2/version/dependency!--通用基础配置boottest/lombok/hutool--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdversion${lombok.version}/versionoptionaltrue/optional/dependencydependencygroupIdcn.hutool/groupIdartifactIdhutool-all/artifactIdversion5.8.8/version/dependency/dependenciesbuildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactId/plugin/plugins/build/project 写YML server.port7777spring.application.nameredis_distributed_lock # swagger2 # http://localhost:7777/swagger-ui.html swagger2.enabledtrue spring.mvc.pathmatch.matching-strategyant_path_matcher# redis单机 spring.redis.database0 spring.redis.host192.168.111.185 spring.redis.port6379 spring.redis.password111111 spring.redis.lettuce.pool.max-active8 spring.redis.lettuce.pool.max-wait-1ms spring.redis.lettuce.pool.max-idle8 spring.redis.lettuce.pool.min-idle0 主启动 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 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;Beanpublic 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(springbootredis整合).version(1.0).termsOfServiceUrl(https://www.baidu.com/).build();}} RedisConfig 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 {Beanpublic RedisTemplateString, Object redisTemplate(LettuceConnectionFactory lettuceConnectionFactory){RedisTemplateString,Object redisTemplate new RedisTemplate();redisTemplate.setConnectionFactory(lettuceConnectionFactory);//设置key序列化方式stringredisTemplate.setKeySerializer(new StringRedisSerializer());//设置value的序列化方式jsonredisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());redisTemplate.setHashKeySerializer(new StringRedisSerializer());redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());redisTemplate.afterPropertiesSet();return redisTemplate;} } InventoryService 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 {Autowiredprivate 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 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 {Autowiredprivate 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 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 {Autowiredprivate 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 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 {Autowiredprivate 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 官网        SET | DocsSets the string value of a key, ignoring its type. The key is created if it doesnt exist.https://redis.io/commands/set 8.7.3 redis分布式锁 8.7.3.1 修改为3.1版 通过递归重试的方式 问题 测试手工OK测试Jmeter压测5000 OK 递归是一种思想没错但是容易导致StackOverflowError不太推荐进一步完善 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 {Autowiredprivate StringRedisTemplate stringRedisTemplate;Value(${server.port})private String port;private Lock lock new ReentrantLock();// V3.1递归重试容易导致stackoverflowerror所以不太推荐另外高并发唤醒后推荐用while判断而不是ifpublic String sale(){String retMessage ;String key zzyyRedisLock;String uuidValue IdUtil.simpleUUID():Thread.currentThread().getId();Boolean flag stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue);//flagfalse,抢不到的线程要继续重试。。。。。。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 用自旋替代递归重试 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 {Autowiredprivate 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版增加过期时间 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版 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 {Autowiredprivate 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版 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 {Autowiredprivate 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版接上一步 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 {Autowiredprivate 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\tuuidValue;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 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 {Autowiredprivate 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\tuuidValue;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( new DefaultRedisScript(script), Arrays. asList(key),value); stringRedisTemplate.execute( new DefaultRedisScript(script, Long.class), Arrays. asList(key),value); //使用该构造方法不然报错 8.7.7 可重入锁设计模式 8.7.7.1 当前代码为6.0版接上一步 while判断并自旋重试获取锁setnx含自然过期时间Lua脚本官网删除锁命令 //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修饰的方法或代码块时是永远可以得到锁的 与可重入锁相反不可重入锁不可递归调用递归调用就发生死锁。 同步块 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();} } 同步方法 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这样的可重入锁。 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中那个数据类型可以代替 KKV hset  zzyyRedisLock 29f0ee01ac77414fb8b0861271902a94:1  MapString,MapObject,Object 案例命令 hset key field value hset redis锁名字(zzyyRedisLock)  某个请求线程的UUIDThreadID  加锁的次数 小总结 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方法 if redis.call(exists,key) 0 thenredis.call(hset,key,uuid:threadid,1)redis.call(expire,key,30)return 1elseif redis.call(hexists,key,uuid:threadid) 1 thenredis.call(hincrby,key,uuid:threadid,1)redis.call(expire,key,30)return 1elsereturn 0end 相同部分是否可以替换处理 hincrby命令可否替代hset命令 V2        //合并相同的代码用hincrby替代hset精简代码 if redis.call(exists,key) 0 or redis.call(hexists,key,uuid:threadid) 1 thenredis.call(hincrby,key,uuid:threadid,1)redis.call(expire,key,30)return 1elsereturn 0end V3        //脚本OK了换上参数来替代 key KEYS[1] zzyyRedisLock value ARGV[1] 2f586ae740a94736894ab9d51880ed9d:1 过期时间值 ARGV[2] 30   秒 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 elsereturn 0end 测试 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 负一 表示每次减个一解锁一次。直到它变为零表示可以删除该锁Keydel 锁key 全套流程 上述设计修改为Lua脚本 V1        //解锁的Lua脚本对标我们的unlock方法 if redis.call(HEXISTS,lock,uuid:threadID) 0 thenreturn nilelseif redis.call(HINCRBY,lock,uuid:threadID,-1) 0 thenreturn redis.call(del,lock)else return 0end V2        //脚本OK了换上参数来替代 if redis.call(HEXISTS,KEYS[1],ARGV[1]) 0 thenreturn nilelseif redis.call(HINCRBY,KEYS[1],ARGV[1],-1) 0 thenreturn redis.call(del,KEYS[1])elsereturn 0end 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程序 复原程序为初始无锁版 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 {Autowiredprivate 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 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 elsereturn 0end 解锁unlock 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 0end 工厂设计模式引入 通过实现JUC里面的Lock接口实现Redis分布式锁RedisDistributedLock 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:ThreadIDthis.expireTime 30L;}Overridepublic void lock(){tryLock();}Overridepublic boolean tryLock(){try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}return false;}/*** 干活的实现加锁功能实现这一个干活的就OK全盘通用* param time* param unit* return* throws InterruptedException*/Overridepublic 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;}/***干活的实现解锁功能*/Overridepublic 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 falseSystem.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 doesnt EXIST);}}//下面的redis分布式锁暂时用不到//下面的redis分布式锁暂时用不到//下面的redis分布式锁暂时用不到Overridepublic void lockInterruptibly() throws InterruptedException{}Overridepublic Condition newCondition(){return null;} } InventoryService直接使用上面的代码设计有什么问题 考虑扩展本次是redis实现分布式锁以后zookeeper、mysql实现那 引入工厂模式改造7.1版code DistributedLockFactory 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 {Autowiredprivate 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 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:ThreadIDthis.expireTime 30L;}Overridepublic void lock(){tryLock();}Overridepublic boolean tryLock(){try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}return false;}/*** 干活的实现加锁功能实现这一个干活的就OK全盘通用* param time* param unit* return* throws InterruptedException*/Overridepublic 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;}/***干活的实现解锁功能*/Overridepublic 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 falseSystem.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 doesnt EXIST);}}//下面的redis分布式锁暂时用不到//下面的redis分布式锁暂时用不到//下面的redis分布式锁暂时用不到Overridepublic void lockInterruptibly() throws InterruptedException{}Overridepublic Condition newCondition(){return null;} } InventoryService使用工厂模式版 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 {Autowiredprivate StringRedisTemplate stringRedisTemplate;Value(${server.port})private String port;Autowiredprivate 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类新增可重入测试方法 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 {Autowiredprivate StringRedisTemplate stringRedisTemplate;Value(${server.port})private String port;Autowiredprivate 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不OKo(╥﹏╥)o  引入工厂模式改造7.2版code DistributedLockFactory 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 {Autowiredprivate 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 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;}Overridepublic void lock(){this.tryLock();}Overridepublic boolean tryLock(){try{return this.tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}return false;}Overridepublic 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\tuuidValue: 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;}Overridepublic 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\tuuidValue: uuidValue);Long flag stringRedisTemplate.execute(new DefaultRedisScript(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));if(flag null){throw new RuntimeException(没有这个锁HEXISTS查询无);}}//Overridepublic void lockInterruptibly() throws InterruptedException{}Overridepublic Condition newCondition(){return null;} } InventoryService类新增可重入测试方法 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 {Autowiredprivate StringRedisTemplate stringRedisTemplate;Value(${server.port})private String port;Autowiredprivate 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 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;}Overridepublic void lock(){tryLock();}Overridepublic boolean tryLock(){try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}return false;}/*** 干活的实现加锁功能实现这一个干活的就OK全盘通用* param time* param unit* return* throws InterruptedException*/Overridepublic 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;}/***干活的实现解锁功能*/Overridepublic 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 falseSystem.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 doesnt 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(){Overridepublic 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分布式锁暂时用不到Overridepublic void lockInterruptibly() throws InterruptedException{}Overridepublic Condition newCondition(){return null;} } InventoryService 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 {Autowiredprivate StringRedisTemplate stringRedisTemplate;Value(${server.port})private String port;Autowiredprivate 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替代setnxlock变为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 官网说明 Distributed Locks with Redis | DocsA distributed lock pattern with Redishttps://redis.io/docs/manual/patterns/distributed-locks/主页说明 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/21这里是 3 个节点的 Redis 节点都取到锁并且获取锁使用的时间小于锁失效时间时锁才算获取成功 4 如果取到了锁其真正有效时间等于初始有效时间减去获取锁所使用的时间步骤 3 计算的结果。 5 如果由于某些原因未能获得锁无法在至少 N/2 1 个 Redis 实例获取锁、或获取锁的时间超过了有效时间客户端应该在所有的 Redis 实例上进行解锁即便某些Redis实例根本就没有加锁成功防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁。 该方案为了解决数据不一致的问题直接舍弃了异步复制只使用 master 节点同时由于舍弃了 slave为了保证可用性引入了 N 个节点官方建议是 5。阳哥本次教学演示用3台实例来做说明。 客户端只有在满足下面的这两个条件时才能认为是加锁成功。 条件1客户端从超过半数大于等于N/21的Redis实例上成功获取到了锁 条件2客户端获取锁的总耗时没有超过锁的有效时间。 9.2.3.3 解决方案 为什么是奇数  N 2X 1   (N是最终部署机器数X是容错机器数) 1 先知道什么是容错 失败了多少个机器实例后我还是可以容忍的所谓的容忍就是数据一致性还是可以Ok的CP数据一致性还是可以满足 加入在集群环境中redis失败1台可接受。2X1 2 * 11 3部署3台死了1个剩下2个可以正常工作那就部署3台。 加入在集群环境中redis失败2台可接受。2X1 2 * 21 5部署5台死了2个剩下3个可以正常工作那就部署5台。 2 为什么是奇数 最少的机器最多的产出效果 加入在集群环境中redis失败1台可接受。2N2 2 * 12 4部署4台 加入在集群环境中redis失败2台可接受。2N2 2 * 22 6部署6台 容错公式 9.2.4 天上飞的理念(RedLock)必然有落地的实现(Redisson) redisson实现 Redisson是java的redis客户端之一提供了一些api方便操作redis redisson之官网 Redisson: Easy Redis Java client and Real-Time Data PlatformEasy Redis Java client and Real-Time Data Platform. Offers distributed Redis based Cache, Map, Lock, Queue and other objects and services for Java. Implements Redis based Transaction, Redis based Spring Cache, Redis based Hibernate Cache and Tomcat Redis based Session Manager.https://redisson.org/ redisson之Github 目录 · redisson/redisson Wiki · GitHubRedisson - Easy Redis Java client and Real-Time Data Platform. Valkey compatible. Sync/Async/RxJava/Reactive API. Over 50 Redis based Java objects and services: Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map Reduce, Bloom filter, Spring Cache, Tomcat, Scheduler, JCache API, Hibernate, RPC, local cache ... - 目录 · redisson/redisson Wikihttps://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95 redisson之解决分布式锁 8. Distributed locks and synchronizers · redisson/redisson Wiki · GitHubRedisson - Easy Redis Java client and Real-Time Data Platform. Valkey compatible. Sync/Async/RxJava/Reactive API. Over 50 Redis based Java objects and services: Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map Reduce, Bloom filter, Spring Cache, Tomcat, Scheduler, JCache API, Hibernate, RPC, local cache ... - 8. Distributed locks and synchronizers · redisson/redisson Wikihttps://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers 9.3 使用Redisson进行编码改造V9.0 9.3.1 你怎么知道该这样使用 官网说话 8. 分布式锁和同步器 · redisson/redisson Wiki · GitHub 9.3.2 V9.0版本修改 9.3.2.1 POM !--redisson-- dependencygroupIdorg.redisson/groupIdartifactIdredisson/artifactIdversion3.13.4/version /dependency 9.3.2.2 RedisConfig 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 {Beanpublic RedisTemplateString, Object redisTemplate(LettuceConnectionFactory lettuceConnectionFactory){RedisTemplateString,Object redisTemplate new RedisTemplate();redisTemplate.setConnectionFactory(lettuceConnectionFactory);//设置key序列化方式stringredisTemplate.setKeySerializer(new StringRedisSerializer());//设置value的序列化方式jsonredisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());redisTemplate.setHashKeySerializer(new StringRedisSerializer());redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());redisTemplate.afterPropertiesSet();return redisTemplate;}//单Redis节点模式Beanpublic 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 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 {Autowiredprivate 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 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 {Autowiredprivate StringRedisTemplate stringRedisTemplate;Value(${server.port})private String port;Autowiredprivate DistributedLockFactory distributedLockFactory;//V9.1,引入Redisson对应的官网推荐RedLock算法实现类Autowiredprivate 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版 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 {Autowiredprivate StringRedisTemplate stringRedisTemplate;Value(${server.port})private String port;Autowiredprivate DistributedLockFactory distributedLockFactory;Autowiredprivate 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 在获取锁成功后给锁加一个 watchdogwatchdog​会起一个定时任务在锁没有被释放且快要过期的时候会续期 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尝试获取锁并且能够成功获取锁导致互斥失效 代码参考来源 8. Distributed locks and synchronizers · redisson/redisson Wiki · GitHubRedisson - Easy Redis Java client and Real-Time Data Platform. Valkey compatible. Sync/Async/RxJava/Reactive API. Over 50 Redis based Java objects and services: Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map Reduce, Bloom filter, Spring Cache, Tomcat, Scheduler, JCache API, Hibernate, RPC, local cache ... - 8. Distributed locks and synchronizers · redisson/redisson Wikihttps://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers2022年第8章第4小节 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 version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersionparentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion2.3.10.RELEASE/versionrelativePath/ !-- lookup parent from repository --/parentgroupIdcom.atguigu.redis.redlock/groupIdartifactIdredis_redlock/artifactIdversion0.0.1-SNAPSHOT/versionpropertiesjava.version1.8/java.version/propertiesdependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependencydependencygroupIdorg.redisson/groupIdartifactIdredisson/artifactIdversion3.19.1/version/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdversion1.18.8/version/dependency!--swagger--dependencygroupIdio.springfox/groupIdartifactIdspringfox-swagger2/artifactIdversion2.9.2/version/dependency!--swagger-ui--dependencygroupIdio.springfox/groupIdartifactIdspringfox-swagger-ui/artifactIdversion2.9.2/version/dependencydependencygroupIdorg.apache.commons/groupIdartifactIdcommons-lang3/artifactIdversion3.4/versionscopecompile/scope/dependencydependencygroupIdcn.hutool/groupIdartifactIdhutool-all/artifactIdversion5.8.11/version/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-configuration-processor/artifactIdoptionaltrue/optional/dependency/dependenciesbuildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactIdconfigurationexcludesexcludegroupIdorg.springframework.boot/groupIdartifactIdspring-boot-configuration-processor/artifactId/exclude/excludes/configuration/plugin/plugins/build/project 9.5.5 写YML server.port9090 spring.application.nameredlockspring.swagger2.enabledtruespring.redis.database0 spring.redis.password spring.redis.timeout3000 spring.redis.modesinglespring.redis.pool.conn-timeout3000 spring.redis.pool.so-timeout3000 spring.redis.pool.size10spring.redis.single.address1192.168.111.185:6381 spring.redis.single.address2192.168.111.185:6382 spring.redis.single.address3192.168.111.185:6383 9.5.6 主启动 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 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 {AutowiredRedisProperties redisProperties;BeanRedissonClient 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);}BeanRedissonClient 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);}BeanRedissonClient 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*//*Beanpublic Redisson redisson(){Config config new Config();config.useSingleServer().setAddress(redis://192.168.111.147:6379).setDatabase(0);return (Redisson) Redisson.create(config);}*/} RedisPoolProperties 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 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 package com.atguigu.redis.redlock.config;import lombok.Data;Data public class RedisSingleProperties {private String address1;private String address2;private String address3; } controller 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;AutowiredRedissonClient redissonClient1;AutowiredRedissonClient redissonClient2;AutowiredRedissonClient 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-evictionyes 上面两种方案都走极端 定期删除 定期删除策略是前两种策略的折中 定期删除策略每隔一段时间执行一次删除过期键操作并通过限制删除操作执行时长和频率来减少删除操作对CPU时间的影响。 周期性轮询redis库中的时效性数据采用随机抽取的策略利用过期数据占比的方式控制删除频度  特点1CPU性能占用设置有峰值检测频度可自定义设置  特点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-evictionyes 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 /** 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^532byte) sdshdr8、(2 ^ 8256byte) sdshdr16、(2 ^ 1665536byte64KB) sdshdr32、 (2 ^ 32byte4GB) sdshdr642的64次方byte17179869184G用于存储不同的长度的字符串。 len 表示 SDS 的长度使我们在获取字符串长度的时候可以在 O(1)情况下拿到而不是像 C 那样需要遍历一遍字符串。 alloc 可以用来计算 free 就是字符串已经分配的未使用的空间有了这个值就可以引入预分配空间的算法了而不用去考虑内存分配的问题。 buf 表示字符串数组真存数据的。 官网 https://github.com/antirez/sds Redis为什么重新设计一个 SDS 数据结构 C语言没有Java里面的String类型只能是靠自己的char[]来实现字符串在 C 语言中的存储方式想要获取 「Redis」的长度需要从头开始遍历直到遇到 \0 为止。所以Redis 没有直接使用 C 语言传统的字符串标识而是自己构建了一种名为简单动态字符串 SDSsimple 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_lengthencoding长度都可以根据编码方式推算真正变化的是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 实现2object.c 明明有ziplist了为什么出来一个listpack紧凑列表?面试题 复习​​​​​​​ ziplist的连锁更新问题 压缩列表新增某个元素或修改某个元素时如果空间不不够压缩列表占用的内存空间就需要重新分配。而当新插入的元素较大时可能会导致后续元素的 prevlen 占用空间都发生变化从而引起「连锁更新」问题导致每个元素的空间都要重新分配造成访问压缩列表性能的下降。 案例说明压缩列表每个节点正因为需要保存前一个节点的长度字段就会有连锁更新的隐患 第一步现在假设一个压缩列表中有多个连续的、长度在 250253 之间的节点如下图 因为这些节点长度值小于 254 字节所以 prevlen 属性需要用 1 字节的空间来保存这个长度值一切OKO(∩_∩)O哈哈~ 第二步这时如果将一个长度大于等于 254 字节的新节点加入到压缩列表的表头节点即新节点将成为entry1的前置节点如下图 因为entry1节点的prevlen属性只有1个字节大小无法保存新节点的长度此时就需要对压缩列表的空间重分配操作并将entry1节点的prevlen 属性从原来的 1 字节大小扩展为 5 字节大小。 第三步连续更新问题出现 entry1节点原本的长度在250253之间因为刚才的扩展空间此时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 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(替换了ziplistlinkedList)而quicklist也用到了ziplist 结论quicklist就是「双向链表 压缩列表」组合因为一个 quicklist 就是一个链表而链表中的每个元素又是一个压缩列表 quicklist总纲 quicklist 实际上是 zipList 和 linkedList 的混合体它将 linkedList按段切分每一段使用 zipList 来紧凑存储多个 zipList 之间使用双向指针串接起来。 是ziplist和linkedlist的结合体  源码分析 quicklist.hhead和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/2n/4n/8…842n-2。所以跳表的空间复杂度是O(n) 。也就是说如果将包含n个结点的单链表构造成跳表我们需要额外再用接近n个结点的存储空间。 第三步思考三三取首每层索引的结点数n/3, n/9, n/27 ... , 9, 3, 1 以此类推 第一级索引需要大约n/3个结点第二级索引需要大约n/9个结点。每往上一级索引结点个数都除以3。为了方便计算我们假设最高一级的索 引结点个数是1。我们把每级索引的结点个数都写下来也是一个等比数列 通过等比数列求和公式总的索引结点大约就是n/3n/9n/27…931n/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(▽) 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 ​​​​​​​背景 一个redisServer2个Client 11.2.6.2 BIO 当用户进程调用了recvfrom这个系统调用kernel就开始了IO的第一个阶段准备数据对于网络IO来说很多时候数据在一开始还没有到达。比如还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来。这个过程需要等待也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边整个进程会被阻塞当然是进程自己选择的阻塞。当kernel一直等到数据准备好了它就会将数据从kernel中拷贝到用户内存然后kernel返回结果用户进程才解除block的状态重新运行起来。所以 BIO的特点就是在IO执行的两个阶段都被block了。 先演示accept  accept监听 code案例 RedisServer 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 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 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 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 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 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 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 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 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 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 ArrayListSocketChannel 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 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 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 MultiplexingIO多路复用 11.2.6.4.1 是什么 词牌 模型 I/O多路复用在英文中其实叫 I/O multiplexing  多个Socket复用一根网线这个功能是在内核驱动层实现的 I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态来同时管理多个I/O流. 目的是尽量多的提高服务器的吞吐能力。 大家都用过nginxnginx使用epoll接收请求ngnix会有很多链接进来 epoll会把他们都监视起来然后像拨开关一样谁有数据就拨向谁然后调用相应的代码处理。redis类似同理 FileDescriptor 文件描述符File descriptor是计算机科学中的一个术语是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上它是一个索引值指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时内核向进程返回一个文件描述符。在程序设计中一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。 IO多路复用 IO multiplexing就是我们说的selectpollepoll有些技术书籍也称这种IO方式为event driven IO事件驱动IO。就是通过一种机制一个进程可以监视多个描述符一旦某个描述符就绪一般是读就绪或者写就绪能够通知程序进行相应的读写操作。可以基于一个阻塞对象并同时在多个描述符上等待就绪而不是使用多个线程(每个文件描述符一个线程每次new一个线程)这样可以大大节省系统资源。所以I/O 多路复用的特点是 通过一种机制一个进程能同时等待多个文件描述符而这些文件描述符套接字描述符其中的任意一个进入读就绪状态selectpollepoll等 函数就可以返回。 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 个关键组成 1ReactorReactor 在一个单独的线程中运行负责监听和分发事件分发给适当的处理程序来对 IO 事件做出反应。 它就像公司的电话接线员它接听来自客户的电话并将线路转移到适当的联系人 2Handlers处理程序执行 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、Nginxlinux中的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, 所以第一个人的随机范围是020 )平均可以抢到10元。假设第一个人随机到10元那么剩余金额是100-10 90 元。 第2次 90÷9  X2 20, 所以第二个人的随机范围同样是020 )平均可以抢到10元。假设第二个人随机到10元那么剩余金额是90-10 80 元。 第3次 80÷8  X2 20, 所以第三个人的随机范围同样是020 )平均可以抢到10元。 以此类推每一次随机范围的均值是相等的。 12.1.4 编码实现 省略其它不重要的只写Controller表示即可 coding 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:;Autowiredprivate 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_KEYIdUtil.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_KEYredPackageKey,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 多学一手 上述案例有许多红包记录了如何批量删除
http://www.pierceye.com/news/407660/

相关文章:

  • 电子商务网站建设与管理实训网页设计与网站建设基础心得体会
  • 托管的服务器如何做网站花店网站模板 html
  • 南宁保洁网站建设在线 代理 输入网址
  • 微站是什么意思快站app下载
  • 网站让女友做网站模特做网站好一点的软件
  • 苏州h5网站建设wordpress 数据库配置
  • 环保网站模板下载农村电商平台怎么加入
  • 教育企业重庆网站建设建设公司网站征集信息的通知
  • 广州做网站找哪家好成都装饰设计公司排名
  • 做头发个人网站制作素材copyright wordpress
  • 高埗镇网站仿做企业网站建设市场分析
  • 网站怎么加icowordpress 程序员主题
  • 做国外产品描述的网站门户网站 建设 如何写
  • 建筑网站资料哪些购物网站做的比较简洁有品质
  • 永州做网站网站运营与管理实验报告
  • 江门专业网站建设系统黄冈公司网站建设平台
  • 男女做羞羞事的网站衡器行业网站建设模板
  • 怎么看网站空间大小wordpress悬浮代码
  • 淮安市建设监理协会网站家装网站做
  • 深圳专业营销网站建站网站插件
  • 企业网站建设专业精准丨 鸣远科技网站建设会议验收
  • 网站建设在哪里做比较好广东网站设计公司
  • 什么叫域名访问网站六安网络科技有限公司
  • 江苏省城乡与建设厅网站首页文山网站建设兼职
  • 做网站用什么系统好二手商标网
  • 福州网站建设公司哪个好做芯片代理哪个网站推广
  • 怎么制作钓鱼网站链接乐山网站营销推广哪家公司好
  • 一键网站制作定制网站型网站开发
  • 营销型网站开发流程包括辽宁建设工程信息网新网址
  • 宁德企业网站建设网站开发成本包括