Redis学习笔记 | 数据结构 | 缓存 | 分布式锁 | 集群

张开发
2026/4/9 8:57:52 15 分钟阅读

分享文章

Redis学习笔记 | 数据结构 | 缓存 | 分布式锁 | 集群
引言之前都是在在线文档写学习笔记最近正在找实习于是准备重新整理一遍自己的笔记顺便写成博客记录下来。笔记参考的是黑马的八股视频以及csdn上其他同类型的文章还有根据自己实际的问题整理的ai的回答本文字数约1.4w。介绍内容顺序Redis数据结构、缓存、分布式锁、集群、Redis单线程却快的原因、秒杀系统。值得一提的是这里把八股分为了两种题型场景题和纯八股场景题会结合业务问另外视频提到的这些问题都会进行探究Redis的数据结构Redis的数据结构主要有五种这里将只会简单介绍一下五种数据结构是存什么的适合什么样的场景以及在Java里操作Redis的方法。String字符串# 基本操作SET user:1 AliceGET user:1INCR counterAPPEND message world特点二进制安全最大512MB可以存储字符串、数字、序列化的对象适用场景✅ 缓存用户信息✅ 计数器访问量、点赞数✅ 分布式锁SETNX命令✅ 限流令牌桶算法其中注意String存对象的话需要将其序列化为Json类型的所以对对象的操作是不方便的因为还要把Json转成对象类型的之前做外卖项目的时候套餐、菜品都是以String存储的因为不需要怎么操作对象。还有关于这个限流算法将在后面秒杀接口的时候总结。Hash# 存储对象属性HSET user:100 name Alice age 25 email aliceexample.comHGET user:100 nameHGETALL user:100特点类似HashMap存储字段-值对适合存储对象适用场景✅ 存储用户信息姓名、年龄、邮箱等✅ 商品详情价格、库存、描述等✅ 配置信息缓存✅ 避免键名冗余user:1:name, user:1:age → user:1[name,age]对于避免键名冗余这是String的SET user:1:name 张三SET user:1:age 20SET user:1:sex 男SET user:2:name 李四SET user:2:age 21SET user:2:sex 女这是Hash的HSET user:1 name 张三 age 20 sex 男HSET user:2 name 李四 age 21 sex 女结构是这样的user:1 - {name: 张三,age: 20,sex: 男}user:2 - {name: 李四,age: 21,sex: 女}公共前缀只需要存一次。list# 两端操作LPUSH news_list article_1RPUSH news_list article_2LRANGE news_list 0 -1LPOP news_list特点双端链表实现保持插入顺序两端操作O(1)适用场景✅ 消息队列生产者-消费者模式✅ 最新N条数据最新评论、日志✅ 任务队列✅ 粉丝列表按关注时间排序set# 无序、去重SADD tags redis database cacheSMEMBERS tagsSISMEMBER tags redisSUNION set1 set2特点无序、不重复基于哈希表实现查找快支持集合运算交并补适用场景✅ 标签系统文章标签、用户兴趣✅ 好友关系共同好友、推荐好友✅ 去重统计独立访客IP✅ 权限控制用户权限集合Sorted Set有序集合zset# 有序、去重带分数ZADD leaderboard 100 player1ZADD leaderboard 200 player2ZRANK leaderboard player1ZRANGE leaderboard 0 -1 WITHSCORES特点有序按分数排序不重复分数可变适用场景✅ 排行榜游戏积分、销售排名✅滑动窗口限流socre✅ 时效性数据带权重的任务队列✅ 社交网络按热度排序的内容✅ 时间轴按时间戳排序Redis的数据结构就是这么多我觉得记住每种适合干嘛的就行了结合自己的项目来像我自己就是菜品和套餐缓存用到了String购物车用到了Hash滑动窗口限流用到了zset。在Java中使用Redis1.pom.xml里加依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency2.配 Redisapplication.ymlspring: redis: host: 127.0.0.1 port: 6379 password: # 没有密码就空着 database: 03.序列化配置类Configuration Slf4j EnableAsync // 启用异步支持 public class RedisConfiguration { Value(${sky.redis.host}) private String redisHost; Value(${sky.redis.port}) private int redisPort; Bean public RedisTemplateString, String redisTemplate(RedisConnectionFactory redisConnectionFactory){ log.info(开始创建 redis 模板对象...); RedisTemplateString, String redisTemplate new RedisTemplate(); //设置 redis 的连接工厂对象 redisTemplate.setConnectionFactory(redisConnectionFactory); //设置 redis key 的序列化器 redisTemplate.setKeySerializer(new StringRedisSerializer()); //设置 hash 值 value 的序列化器购物车使用 JSON 格式 redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(new StringRedisSerializer()); return redisTemplate; }4.直接注入使用最关键Autowired private StringRedisTemplate stringRedisTemplate;5.使用方法// 存 stringRedisTemplate.opsForValue().set(name, 张三); // 取 String name stringRedisTemplate.opsForValue().get(name); // 带过期时间 stringRedisTemplate.opsForValue().set(code, 1234, 5, TimeUnit.MINUTES); // 自增计数器、限流、ID Long cnt stringRedisTemplate.opsForValue().increment(click);简单来说就是配置这个stringRedisTemplate注意特殊的一步就是要写一个配置类进行序列化然后就用这个opsFor..方法就行了。缓存上面的是使用Redis的基础接下来解决Redis业务中的一系列问题。缓存穿透这里可以套用我的通用模板了场景是什么缓存穿透是指查询一个在数据库中也不存在的、根本没数据的key。由于缓存和数据库里都没有这条数据请求会每次都“穿透”缓存层直接打到数据库上导致数据库压力增大。例如业务误操作删除了大量不该删除的数据更典型的是黑客的恶意攻击。解决方案主要有两种为什么包含了优缺点分析等布隆过滤器 (Bloom Filter):在访问缓存前用一个高效的数据结构快速判断这个key是否存在。如果布隆过滤器认为key肯定不存在就直接返回不会查询缓存和数据库。这是治本的方法。缓存空值/默认值:当从数据库查询到结果为空时也将这个“空结果”缓存起来如缓存一个null或特殊标记并设置一个较短的过期时间。这样下次再有相同的key查询时可以直接从缓存中拿到“空”的结果而不会打到数据库。这是最常用的方法。缓存空值会导致数据不一致比如之后数据库里有这个key了但是用户查询的时候返回的还是空值。缓存空值 必须 短期过期时间就能完美解决不一致设想一个场景用户查 id999 的店铺 → 数据库没有缓存空值null后来管理员在后台新增了 id999 的店铺但缓存还是空 → 用户查不到怎么解决给空值设置一个很短的过期时间 **比如 30s / 1min / 5min**到期自动删除下次查询就会重新查数据库。缓存击穿什么是缓存击穿缓存击穿的意思是对于设置了过期时间的key缓存在某个时间点过期的时候恰好这个时间点对这个Key有大量的并发请求过来。这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存这个时候大并发的请求可能会瞬间把 DB 压垮。怎么解决第一可以使用互斥锁也就是分布式锁当缓存失效时不立即去load db先使用如 Redis 的SETNX去设置一个互斥锁。当操作成功返回时再进行 load db的操作并回设缓存否则重试get缓存的方法。第二种方案是设置当前key逻辑过期大概思路如下1) 在设置key的时候设置一个过期时间字段一块存入缓存中不给当前key设置过期时间2) 当查询的时候从redis取出数据后判断时间是否过期3) 如果过期则开通另外一个线程进行数据同步当前线程正常返回数据这个数据可能不是最新的。当然两种方案各有利弊如果选择数据的强一致性建议使用分布式锁的方案但性能上可能没那么高且有可能产生死锁的问题。如果选择key的逻辑删除则优先考虑高可用性性能比较高但数据同步这块做不到强一致。逻辑过期不是真的过期不过期的话缓存会一直在内存中直到内存满了触发淘汰策略这不是对内存的压力很大吗标准答案对确实不会自动删除会占内存。但只用于热点 key数量极少内存压力可以忽略。真满了冷数据会被 LRU 淘汰热点数据保留。这是用极小内存代价换极高稳定性高并发系统里非常划算。缓存雪崩什么是缓存雪崩怎么解决缓存雪崩意思是设置缓存时采用了相同的过期时间导致缓存在某一时刻同时失效又或者是redis服务器宕机请求全部转发到DBDB瞬时压力过重而雪崩。与缓存击穿的区别是雪崩是很多key而击穿是某一个key缓存。解决方案主要是可以将缓存失效时间分散开。比如可以在原有的失效时间基础上增加一个随机值比如1-5分钟随机。这样每一个缓存的过期时间的重复率就会降低就很难引发集体失效的事件。最后总结一下缓存三兄弟探究的问题主要就是如何避免各种情景下大量并发请求冲击数据库双写一致性什么是双写一致性双写一致性”指的是在同时使用缓存如 Redis和数据库如 MySQL的系统中确保缓存中的数据与数据库中的数据保持逻辑上的一致。redis做为缓存mysql的数据如何与redis进行同步呢双写一致性这里的回答需要结合项目场景也是分为了是需要强一致性还是高时效性。对于读操作而言会产生的一系列缓存问题已经探讨过了当前探讨的是写操作也就是更新数据库之后数据如何同步的问题。比较常见的解决方案是先更新数据库再删除缓存。首先我们探讨一下为什么是先更新再删而不是先删再更新。如果A线程先删的话这时另一个B线程进来了然后缓存未命中去查数据库然后写入缓存之后A线程更新数据库此时就出现了脏数据的现象。如果先更新再删B线程就不会把数据库的旧数据写入缓存了。但是仍然有极小的概率出现脏数据的情况线程1查询缓存的时候缓存里的key刚好过期了于是往数据库里查询数据然后此时刚好线程2挤进来更新数据库接着把缓存删了然后线程1把旧数据写进缓存出现脏数据。这是非常极端的情况首先是数据库的查询速度远快于更新速度然后就是要在查询的时候恰好挤进来一个更新数据库的线程。比先删后改出现脏数据的概率低多了。然后对于这个延迟双删为什么要进行两次删除呢因为不管是先删还是后删都会导致脏数据所以执行写操作的时候进行两次删除。为什么要延迟呢因为数据库是主从架构读写分离主节点负责写操作从节点负责读操作。而数据库的主从节点同步是需要一定的时间的延迟删除是希望另外一个线程在读取数据库数据的时候它读取到了同步前的从节点并将其写入缓存的话延迟到了后把缓存里的这个脏数据删除保证数据一致性。但是这个延迟的时间是不确定的如果同步还没好就删了还是可能导致脏数据所以还是没办法保证强一致性一般不采用。至于这个两次删除更像是为了体现设计思想让另一个线程访问缓存的时候不能命中让其读取数据库的信息。而在实际的解决问题这方面感觉就是多余的。如果项目要保证强一致性可以采用读写锁的方式读数据的时候加一把读锁就是共享锁其它线程也可以读数据但是不能写数据不然我读到的就不知道是新数据还是旧数据了。写操作的时候加一把写锁也就是排他锁其它线程既不能读也不能写先等我更新完毕之后再说。如果允许短暂的不一致这种情况是主流的这里有两种解决方案首先明确保证数据的一致性主要是讨论发生写操作的时候就是更改MySQL数据库的时候。对于这个异步通知我是这么理解的之前我们采取的方式可能是先更新数据库再去删缓存这时就会导致其它线程可能再删除之后还会写入旧数据导致脏数据。那么我们采取异步通知的解决方案用一个中间件MQ修改数据的时候给MQ发布一条消息然后缓存服务这边会监听MQ最终再更新缓存的信息保证了最终一致性如何保证的呢主要取决于MQ的可靠性但是MQ更新之前肯定还是会导致一些时效性的问题。这种方案对于代码几乎是0侵入的。采用的阿里的Canal组件实现数据同步不需要更改业务代码只需部署一个Canal服务。Canal服务把自己伪装成mysql的一个从节点。当mysql数据更新以后Canal会读取binlog数据然后再通过Canal的客户端获取到数据并更新缓存即可。实际项目中单独部署MQ和Canal确实很麻烦我是直接使用了异步双删的方案。持久化redis做为缓存数据的持久化是怎么做的候选人在Redis中提供了两种数据持久化的方式1) RDB2) AOF。它们的意思是只要满足以下任意一个条件Redis 就会自动触发一次bgsave操作。save 900 1表示在最近900秒内如果至少有1个key被修改过则触发bgsave。save 300 10表示在最近300秒内如果至少有10个key被修改过则触发bgsave。save 60 10000表示在最近60秒内如果至少有10000个key被修改过则触发bgsave。注意这些条件是“或”的关系。只要满足其中任意一个就会触发保存。对于这个COW我的理解是对于主线程来说一个它应该是增量拷贝只拷贝了被修改的页不用全拷贝。还有一个是它是基于内存拷贝的所以这个主线程的拷贝应该是很快速的。2026/3/13也就是隔几天来复习的时候这里明显理不清了这里在试着理一下首先就是明确现在在干什么持久化就是把Redis的数据写入磁盘相当于把内存数据写到磁盘里去。明确了这一点我们就知道这个所谓的主子进程是干嘛的了主进程是用来读或操作redis里的数据的想实现持久化的时候为了不阻塞主进程上的其它命令操作会开启一个子进程用子进程来实现这个把内存数据写进磁盘的操作。剩下的操作后面就有说了。这一节内容挺多的也挺复杂的这里来理一下Redis持久化怎么做的呢两种方案RDB是Redis数据快照就是把内存的所有数据迁移到磁盘里去相当于定时给内存里的数据存一张照片。可以通过手动备份的方式来实现主要有两条命令save和bgsave其中save是主线程运行会阻碍其它的所有命令。而bgsave是开启子线程备份避免阻塞。也可以通过配置文件来配置自动执行的条件自动执行的都是创建子线程的方式。一般也都是采取的这种方式。那么RDB的原理是什么呢RDB就是要把内存中的数据的数据存入磁盘里而Linux系统里的进程是无法操控物理内存的这时候操控的就是虚拟内存并且进程里有一个页表来存储虚拟进程和物理内存的关系。主进程写RDB文件其实就是fork一个子进程fork只需要把主进程的页表拷贝进子进程就行了这个速度是非常快的纳秒级别的。这个时候子进程操控自己的虚拟内存的时候因为映射关系是一致的可以影响到物理内存里的数据这样就实现了主子进程的内存共享。但是这里有一个问题子进程在读内存写RDB文件的过程中主进程可以修改内存中的数据这样可能会出现冲突或者脏数据所以fork采取的是copy-on-write技术就是主进程要写内存中的数据的时候要先拷贝一份然后写完了在把读的映射关系更新到更新写好的数据里这样可以避免冲突和脏写的问题。RDB存的的内存里的数据而AOF则是直接存入写命令AOF是默认关闭的刷盘将命令写入磁盘频率也是可以通过配置文件来配的一般默认的是每秒刷新一次。而AOF记录命令的时候有一个重写的操作可以实现用记录最少的命令达到相同的效果。重写策略也是可以配置的。现在高版本的Redis一般是两种同时开启的了现实中的项目主要也都是两种方案都用的。数据过期策略这个过期策略挺简单的就是Redis提供了两种过期策略然后实际情况中Redis两种策略都是有使用的。数据淘汰策略这个在redis中提供了很多种默认是noeviction不删除任何数据内部不足时直接报错。这个可以在redis的配置文件中进行设置。里面有两个非常重要的概念一个是LRU另外一个是LFU。LRU的意思就是最少最近使用。它会用当前时间减去最后一次访问时间。这个值越大则淘汰优先级越高。LFU的意思是最少频率使用。它会统计每个key的访问频率。值越小淘汰优先级越高。我们在项目中设置的是allkeys-lru它会挑选最近最少使用的数据进行淘汰把一些经常访问的key留在redis中。总结默认的就是不删除内存不足直接报错前面一个allkeys或者volatile代表对全体key还是设置了TTL的keylru就是最近最少使用lfu就是最少频率使用random就是随机。总结频度优先LFU、热度优先LRU、寿命优先TTL、缘分优先random。缓存部分到此就结束了。分布式锁场景抢券的时候会发现券超额发放。现成技术加一个synchronized这个synchronized只能在本地加锁也就是在这一台JVM里加锁而对于其它的服务器上的线程就没有作用了也就是说多服务下不是同一把锁。所以应该引入分布式锁分布式锁是外部锁。其它服务器创建锁时也会失败。Redis分布式锁如何实现这里要注意这个EX要设置的避免因为服务器宕机而导致死锁问题。这个锁的控制效时长是有点麻烦的因为不知道业务具体的执行用时并且可能会因为网络等问题执行用时也是不稳定的。解决方案就是给锁续期就是另外开一个线程来监视业务是否执行完了如果没执行完就续期。Redisson已经实现了这种技术。看门狗会随着服务宕机也挂掉所以不存在服务宕机之后看门狗还在续期宕机之后前面续期的时间一到key就失效了。不设置过期时间手动设置过期时间太短业务没执行完成提前释放了导致并发问题太长造成线程阻塞以上可以很好的体现redisson的执行流程。要注意的是这个while是有一个阈值的我看弹幕里也称之为“自旋”。再顺便提一嘴这里的看门狗就是解决业务执行时间不确定的问题的。先总的讲一下三个点锁的续期是看门狗实现的获取锁失败的线程会进行while循环加锁、设置过期时间等操作是基于lua脚本完成的保证了原子性。还需要注意的是这里的第二个参数如果设置了看门狗就失效了如果不设置解锁时间默认是30s。会通过识别线程的id判断是否是同一个线程如果是同一个线程就是可以重入的。目的是避免死锁可重入锁允许同一个线程多次获取同一个锁而不会导致死锁。如果锁不可重入当同一个线程在持有锁的情况下再次请求该锁就会被自己阻塞造成死锁。Redisson实现的分布式锁能解决主从一致性的问题吗候选人这个是不能的。比如当线程1加锁成功后master节点数据会异步复制到slave节点此时如果当前持有Redis锁的master节点宕机slave节点被提升为新的master节点假如现在来了一个线程2再次加锁会在新的master节点上加锁成功这个时候就会出现两个节点同时持有一把锁的问题。我们可以利用Redisson提供的红锁来解决这个问题它的主要作用是不能只在一个Redis实例上创建锁应该是在多个Redis实例上创建锁并且要求在大多数Redis节点上都成功创建锁红锁中要求是Redis的节点数量要过半。这样就能避免线程1加锁成功后master节点宕机导致线程2成功加锁到新的master节点上的问题了。但是如果使用了红锁因为需要同时在多个节点上都添加锁性能就变得非常低并且运维维护成本也非常高所以我们一般在项目中也不会直接使用红锁并且官方也暂时废弃了这个红锁。核心流程怎么发生的一步一步看我们用Redisson 分布式锁 一人一单 / 秒杀场景演示步骤 1线程 A 向主节点加锁主节点lock:1001加锁成功主节点返回成功给 Redisson此时还没把锁数据同步给从节点异步复制有延迟步骤 2主节点突然宕机锁数据只在主节点有从节点没有。步骤 3哨兵选举从节点升级成新主节点新主节点没有这个锁数据认为锁是未加锁状态。步骤 4线程 B 尝试加锁线程 B 向新主节点加锁新主节点无锁数据 →加锁成功最终结果线程 A 和线程 B 同时持有一把锁→ 同时执行扣库存、创建订单→超卖虽然也是有办法解决的但是性能太低了违背了Redis的观念集群主从单节点Redis的并发能力是有上限的要进一步提高Redis的并发能力可以搭建主从集群实现读写分离。一般都是一主多从主节点负责写数据从节点负责读数据主节点写入数据之后需要把数据同步到从节点中。重点是主从同步的流程主从同步分为了两个阶段一个是全量同步一个是增量同步。全量同步是指从节点第一次与主节点建立连接的时候使用全量同步流程是这样的第一从节点请求主节点同步数据其中从节点会携带自己的replication id和offset偏移量。第二主节点判断是否是第一次请求主要判断的依据就是主节点与从节点是否是同一个replication id如果不是就说明是第一次同步那主节点就会把自己的replication id和offset发送给从节点让从节点与主节点的信息保持一致。第三在同时主节点会执行BGSAVE生成RDB文件后发送给从节点去执行从节点先把自己的数据清空然后执行主节点发送过来的RDB文件这样就保持了一致。当然如果在RDB生成执行期间依然有请求到了主节点而主节点会以命令的方式记录到缓冲区缓冲区是一个日志文件最后把这个日志文件发送给从节点这样就能保证主节点与从节点完全一致了后期再同步数据的时候都是依赖于这个日志文件这个就是全量同步。增量同步指的是当从节点服务重启之后数据就不一致了所以这个时候从节点会请求主节点同步数据主节点还是判断不是第一次请求不是第一次就获取从节点的offset值然后主节点从命令日志中获取offset值之后的数据发送给从节点进行数据同步。哨兵主从复制解决了高并发读的问题但是不能保证高可用性主节点宕机了就不能写数据了哨兵是怎么监控的以及怎么选择从节点转换成主节点的哨兵导致的脑裂问题有的时候由于网络等原因可能会出现脑裂的情况就是说由于Redis master节点和Redis slave节点和Sentinel处于不同的网络分区使得Sentinel没有能够心跳感知到master所以通过选举的方式提升了一个slave为master这样就存在了两个master就像大脑分裂了一样这样会导致客户端还在old master那里写入数据新节点无法同步数据当网络恢复后Sentinel会将old master降为slave这时再从新master同步数据这会导致old master中的大量数据丢失。关于解决的话我记得在Redis的配置中可以设置第一可以设置最少的slave节点个数比如设置至少要有一个从节点才能同步数据第二个可以设置主从数据复制和同步的延迟时间达不到要求就拒绝请求就可以避免大量的数据丢失。你们使用Redis是单点还是集群哪种集群候选人嗯我们当时使用的是主从1主1从加哨兵。一般单节点不超过10G内存如果Redis内存不足则可以给不同服务分配独立的Redis主从节点。尽量不做分片集群。因为集群维护起来比较麻烦并且集群之间的心跳检测和数据通信会消耗大量的网络带宽也没有办法使用Lua脚本和事务。主节点并发大概8万从节点大概10万分片这里的分片集群就是讲了是怎么解决那两个问题的其实就是加服务器呗加多几个master然后多个master之间可以互相监测心跳就不需要哨兵了。另外就是说明了一个具体的细节分片集群是怎么存储和获取数据的也就是一个路由问题分片集群是引入了哈希槽来实现的。总结高并发读主从复制高可用哨兵高并发写分片集群Redis单线程快速的原因Redis是单线程的但是为什么还那么快候选人完全基于内存的C语言编写。采用单线程避免不必要的上下文切换和竞争条件。没有线程安全的问题。使用多路I/O复用模型非阻塞IO。这里2、3点都可以深入解释一下特别是第三点能解释一下I/O多路复用模型候选人嗯~~I/O多路复用是指利用单个线程来同时监听多个Socket并且在某个Socket可读、可写时得到通知从而避免无效的等待充分利用CPU资源。目前的I/O多路复用都是采用的epoll模式实现它会在通知用户进程Socket就绪的同时把已就绪的Socket写入用户空间不需要挨个遍历Socket来判断是否就绪提升了性能。其中Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求比如提供了连接应答处理器、命令回复处理器命令请求处理器在Redis6.0之后为了提升更好的性能在命令回复处理器使用了多线程来处理回复事件在命令请求处理器中将命令的转换使用了多线程增加命令转换速度在命令执行的时候依然是单线程。基于这个情景我们想要提升效率可以从两个方面入手1缩短用户空间等待内核空间数据就绪的时间2加快内核空间拷贝数据给用户空间的速度这里讲I/O的时候是真的有点复杂我尽量理一下首先就是阻塞IO哪里阻塞呢这里我们得从用户空间和内核空间说起用户空间执行命令是受限的是不能直接调用系统资源的得通过内核空间提供的接口来访问。而Linux系统为了提高IO效率在用户空间和内核空间都加入了缓冲区写数据时要先把数据拷贝到内核缓冲区再写入设备读数据时要先从设备里把数据读到内核缓冲区再拷贝到用户缓冲区这里如果要提高效率有两种方案1缩短用户空间等待内核空间数据就绪的时间2加快内核空间拷贝数据给用户空间的速度对于阻塞IO而言用户应用会使用recvfrom命令去内核空间获取数据当内核空间准备数据和拷贝数据的时候进程都是阻塞的而对于非阻塞IO而言用户应用发送recvfrom的时候如果内核缓冲区的数据没有准备就绪则会直接返回异常用户接受到了异常之后继续发送recvfrom命令直到数据准备就绪而拷贝数据的时候仍然是阻塞的。然而即使内核缓冲区数据为就绪的时候进程是非阻塞的此时CPU也是在空转并没有提高效率。此时真正提高效率的IO多路复用模型登场用户应用会开启一个进程调用select或其它命令监听多个socket直到有socket就绪返回readable的时候才会调用recvfrom此时内核拷贝数据的时候select会持续监听并且发送recvfrom命令直到拷贝完成。而像监听socekt的实现方式常见的有三种selectpollepoll现在最常用的是epoll因为select和poll只直到有socket准备好了并不知道是哪一个socket发送recvfrom命令的时候需要轮询而epoll可以知道是哪一个socket就绪了。对于Redis的网络模型性能瓶颈是网络IOMySQL的性能瓶颈是磁盘IO秒杀系统的实现因为到这里已经一万两千字了秒杀系统的实现就简单点说了。个人总结一下对于秒杀系统这个高并发的场景主要就四个方面应对高并发请求击穿MySQL——用Redis高并发下的数据不一致问题——加锁秒杀系统的性能优化——异步下单防刷单、刷券——限流以下内容是对AI回答的整理及补充一、应对高并发击穿 MySQL —— 用 Redis秒杀最大问题同一时间几万、几十万请求打数据库数据库直接挂。1. 为什么 MySQL 扛不住行锁、事务、IO、索引开销大单机 MySQL 抗不住高并发写2. Redis 怎么扛把库存放 Redis所有库存扣减、库存判断全部在 Redis 完成只有校验通过才去数据库真正下单3. 具体怎么做活动开始前把商品库存加载到 Redis请求进来先查 Redis 库存是否 0查秒杀是否开始Redis 中decr 扣减库存扣减成功 → 进入下单流程扣减失败 → 直接返回 “已售罄”4. 关键点Redis 扛读 扛写完全挡住流量洪峰MySQL 只处理 “真正有效订单”二、高并发下的数据不一致 —— 加锁秒杀最容易出现超卖。库存 100卖出 120这就是数据不一致。1. 为什么会超卖多线程同时执行plaintextif (库存 0) { 库存--; 下单; }并发下判断和扣减不是原子操作。2. 怎么加锁① 单机synchronized / ReentrantLock适合单机简单但分布式环境没用。② 分布式Redis 分布式锁SET lock NX PX扣库存前加锁扣完释放保证只有一个请求能执行扣库存③ 数据库乐观锁最经典sqlupdate stock set count count - 1 where goods_id ? and count 0where count 0 自带原子判断不用分布式锁也能防止超卖性能比分布式锁更好④ Lua 脚本推荐把判断库存 扣减库存写成一段 Lualuaif redis.call(get, KEYS[1]) 0 then redis.call(decr, KEYS[1]) return 1 else return 0 endRedis 执行 Lua 是原子的性能极高无超卖无锁竞争三、秒杀系统性能优化 —— 异步下单Redis 扣库存成功后不要直接同步写库否则数据库依然扛不住。1. 同步下单的问题每个请求都要写库、生成订单、扣减、日志…高并发下大量线程阻塞RT 飙升系统雪崩2. 异步下单怎么做Redis 扣减成功把下单请求扔进 MQRabbitMQ / RocketMQ / Kafka直接返回前端 “排队中”后台消费者慢慢消费异步生成订单、写库如果没学过MQ就直接用Redis Stream方案吧。3. 优点削峰洪峰变平稳流解耦前端不关心下单细节提高吞吐量秒杀接口只做 “校验 发消息”4. 关键点MQ 要做消息可靠性消费者要幂等避免重复下单四、防刷单、刷券、恶意请求 —— 限流不做限流羊毛党、机器流量直接把系统冲垮。1. 限流 4 件套① 接口限流限制每秒请求数如 1 秒 200 请求用Redis Lua或 Guava RateLimiter或 Sentinel / Hystrix② 用户维度限流1 个用户 1 秒只能点 1 次防止疯狂点击、脚本刷单③ 黑名单 白名单识别异常 IP、异常账号直接拦截④ 隐藏接口 / 验证码秒杀前不暴露真实接口前端加图形验证码、滑块验证挡住大部分机器流量对于这个限流我自己就做了一个接口限流以及用户id限流。另外总结一下四种限流算法1. 计数器法固定窗口限流思想固定时间窗口内限制总请求数。实现每秒 / 每分钟计数超过就拒绝。优点最简单好实现。缺点临界突变问题比如前 1s 末尾 后 1s 开头瞬间流量翻倍。适用简单接口、非高并发场景。2. 滑动窗口限流思想把时间切成更小的格子动态计算最近 N 个格子的总请求。优点解决临界突变比固定窗口更平滑。缺点粒度越细内存越大。适用需要比固定窗口更精准的场景。3. 漏桶算法思想请求进桶桶满则丢以固定速率流出。优点强制平滑输出保护下游。缺点无法应对突发流量流量利用率低。适用稳定输出、不允许突发的场景。4. 令牌桶算法思想以固定速率放令牌请求拿令牌才能过桶可存令牌。优点允许合理突发流量性能高最常用。缺点实现稍复杂。适用秒杀、高并发接口、网关限流Sentinel、Guava、Nginx 都用它。一句话速记版计数器简单但有临界点问题。滑动窗口更精准解决突刺。漏桶强行平滑不允许突发。令牌桶允许突发高并发首选。我项目里用的滑动窗口这里要注意实现的时候会用zset存两个时间戳一个是记录时间的还有一个socre是用来排序的。另外提一嘴这个zset就是Redis的一个数据结构哈就比较做排行榜和游戏积分什么的可以范围查询。

更多文章