黑马点评 Redis 实战篇 实战基于session登录校验登录状态用户在请求的时候会从cookie中携带JsessionId到后台后台通过JsessionId从session中拿到用户信息如果没有session信息则进行拦截如果有session信息则将用户信息保存到threadLocal中并放行校验登陆状态 用拦截器为什么ThreadLoacl变量需要回收ThreadLocal是用来隔离线程数据的。不回收的风险内存泄漏 数据污染。redis实现共享sessionUserDto隐藏用户敏感信息拦截器实现token更新cache缓存的作用 1. 降低后端负载 2. 提高读写效率降低响应时间 缓存的成本 3.数据一致性成本4. 代码维护成本 5. 运维成本一般采用服务器集群需要多加机器机器就是钱缓存更新策略如何保证缓存和数据库的数据⼀致性主动更新的方案采用Cache Aside Pattern(旁路缓存模式/双写方案读操作先读缓存未命中则读DB并回填缓存写操作先更新DB再删除缓存 删除缓存而非更新缓存 一种懒加载 (Lazy Loading)思想,即使删除操作重复其性能开销很小而且操作具有幂等性 如何保证缓存与数据库的操作同时成功/同时失败?单体系统将缓存与数据库操作放在同一个事务分布式系统利用TCC等分布式事务方案 如果第二步“删除缓存”失败了怎么办缓存穿透是什么查询一个一定不存在的数据。由于缓存中没有缓存未命中请求将直接转向后端存储如数据库。当大量此类请求同时发生时就会给后端存储带来巨大的压力查询的数据在缓存和数据库中都确定不存在。高并发的恶意攻击或非预期的业务流量缓存空对象 查询一个 key 返回为空时仍然将这个“空结果”缓存起来但为其设置一个较短的TTL 优点 简单效果显著 缺点额外的内存消耗大量不同key一致性问题布隆过滤 请求进入redis先判断存在不存在 不存在直接拒绝 缺点 误判的风险缓存雪崩同一时间大量缓存key失效或者redis服务宕机给不同的Key的TTL添加随机值让其在不同时间段分批失效利用Redis集群提高服务的可用性使用一个或者多个哨兵(Sentinel)实例组成的系统对redis节点进行监控在主节点出现故障的情况下能将从节点中的一个升级为主节点进行故障转义保证系统的可用性。 给缓存业务添加降级限流策略给业务添加多级缓存浏览器访问静态资源时优先读取浏览器本地缓存访问非静态资源ajax查询数据时访问服务端请求到达Nginx后优先读取Nginx本地缓存如果Nginx本地缓存未命中则去直接查询Redis不经过Tomcat如果Redis查询未命中则查询Tomcat请求进入Tomcat后优先查询JVM进程缓存如果JVM进程缓存未命中则查询数据库缓存击穿 hotkey问题高并发访问且缓存重建业务复杂的key突然失效了大量请求访问瞬间给数据库带来巨大冲击逻辑过期针对突然失效 直接不设置ttl了 手动设置expire 配合合适的内存淘汰策略 保证了可用性 互斥锁 保证了强一致性CAP 定理 一个 分布式系统 最多只能同时满足一致性Consistency、**可用性Availability和分区容错性Partition tolerance**这三项中的两项一致性(Consistency) C可用性(Availability) A分区容错性(Partition Tolerance) P互斥锁的实现 利用redis命令 set NX原理缓存查询线程访问数据首先查询缓存。锁获取若缓存未命中线程尝试获取互斥锁。数据库操作成功获取锁的线程查询数据库将数据回填到缓存中然后释放锁。未能获取锁的线程进入阻塞或自旋状态短暂休眠后再次尝试查询缓存而非直接尝试获取锁。缓存命中当持有锁的线程完成缓存回填后其他线程在重试时将直接命中缓存避免了对数据库的冲击。根本原因在于从“线程第一次检查缓存发现为空”到“该线程成功获取到锁”这两个操作之间存在一个时间差。在这个时间窗口内缓存的状态可能已经被其他线程改变了逻辑过期hot key的缓存直接不设置TTL 而是通过逻辑过期时间判断是否需要重建 重建缓存也通过互斥锁保证单线程执行 但是利用独立线程异步执行 其他线程直接返回旧数据无侵入增加逻辑过期字段 用组合或者继承 组合更优封装redis工具类泛型方法 泛型类型参数列表须位于方法的修饰符如public,static等之后且在方法返回类型之前。 函数式接口秒杀场景多人抢券全局ID生成器在分布式系统下 要求唯一性 高可用 高性能 递增性利于索引建立安全性不能让用户看出规律啊 基于redis自增策略 为了ID的安全性我们不直接使用Redis自增的数值而是拼接一些其他信息超卖问题-乐观锁UPDATEproductSETstockstock-1WHEREid10ANDstock0;-- 使用stock来充当版本号“乐观锁”CAS思想其操作的原子性确实是依赖并借助了 MySQL特指 InnoDB 存储引擎底层的行级排他锁 (Exclusive Row Lock)来保证、初级乐观锁用stock来充当版本号能解决超卖 但是成功率低因为大家一起去进行扣减只有一个能成功其他的人在处理时他们在扣减时库存已经被修改过了所以此时其他线程都会失败 简单修改一下即可只要数据库中的库存大于0都能顺利完成扣减库存操作一人一单在判断库存是否充足之后根据我们保存的订单数据判断用户订单是否已存在为了线程安全 加悲观锁一人一单所以这个锁应该只加在单个用户上锁对象应该是userId注意对userID的字符串调用intern()方法如果创建的字符串对象在常量池中有那么就不会在堆中创建新的String对象锁可能会先于事务提交而释放如果当前方法被Spring的事务控制你在内部加锁可能会导致当前方法事务还没有提交但是锁已经释放了所以我们选择将当前方法整体包裹起来确保事务不会出现问题内部调用导致事务失效AopContext.currentProxy()来获取当前对象的代理对象再用代理对象调用方法集群环境下的并发问题单机模拟集群 将springboot服务启动两份端口分别为8081和8082命令行参数设置 修改nginx的config目录下的nginx.conf文件配置反向代理和负载均衡默认轮询就行可能用户两次请求被均衡到两个tomcat上那线程 13都能抢的到哇synchronized 是基于单个 JVM 实例的锁机制无法跨越多个 JVM 实例分布式锁所有希望访问同一个共享资源的客户端必须竞争同一个名字的锁同一个Key。//这里“共享资源”并不是优惠券的库存而是指定用户的下单资格可见性多个线程都能看到相同的结果。 注意这里说的可见性并不是并发编程中指的内存可见性只是说多个进程之间都能感知到变化的意思基于SETNX实现的分布式锁死锁问题 利用ex参数 设置锁的ttl - 保证故障时依然能释放锁避免死锁提高安全性锁误删问题利用线程标识锁用UUID标识在一个JVM中ThreadId地址但是集群模式有多个JVM可能会出现ThreadId重复的情况 释放锁需要先判断标识一致 再删除分布式锁的原子性问题释放锁需要先判断标识一致 再删除 不是原子操作 极端情况依旧可能导致锁误删 Redis提供了Lua脚本功能在一个脚本中编写多条Redis命令确保多条命令执行时的原子性-- 这里的KEYS[1]就是传入锁的key-- 这里的ARGV[1]就是线程标识-- 比较锁中的线程标识与线程标识是否一致if(redis.call(get,KEYS[1])ARGV[1])then-- 一致则释放锁returnredis.call(del,KEYS[1])endreturn0-- javaprivate static final DefaultRedisScriptLongUNLOCK_SCRIPT;static{UNLOCK_SCRIPTnewDefaultRedisScript();UNLOCK_SCRIPT.setLocation(newClassPathResource(unlock.lua));UNLOCK_SCRIPT.setResultType(Long.class);}在RedisTemplate中可以利用execute方法去执行lua脚本当项目编译打包后src/main/resources/中的所有文件会被复制到target/classes/目录下。故ClassPathResource默认就去resources根目录找利用Redisson实现分布式锁基于SETNX实现的分布式锁存在以下问题:不可重入 不可重试超时释放 主从不一致风险 引入Redisson 依赖 新建配置类即可Redisson可重入锁原理利用 Redis 的 Hash 数据结构存储锁信息并结合 Lua 脚本保证操作的原子性Fields:UUID:threadId: 锁的持有者标识。UUID是 Redisson 客户端实例的唯一IDthreadId是持有锁的线程ID。这个组合唯一标识了一个线程。Value: 重入次数。每次一个线程重入这个值就加 1释放减1如何解决“业务执行时间超过 TTL”的问题因为锁的有效期timeout难以预估 锁续期 原理Watchdog看门狗机制的核心就是通过异步的后台线程来完成锁的自动续期 显式地提供leaseTime参数。一旦提供了leaseTime看门狗将不会被启用黑马redis p67 原理视频讲解分布式锁主从一致问题Redisson锁的MutiLock原理 不用主从了 加锁的逻辑写入到每一个redis节点不使用redis实现一个进程级别的伪分布式锁用一个ReentrantLock作为一个类的私有变量实例获取到了就执行逻辑秒杀优化在redis里完成秒杀资格的判断基于Lua脚本redis.call(sismember, orderKey, userId) 1判断用户是否下单) 根据lua返回结果然后打包订单信息扔到阻塞队列里面去 这里的业务就完成了线程池需要PostConstruct 其任务就是从阻塞队列不断取出订单秒杀业务需要在类初始化之后就立即执行所以这里需要用到PostConstruct注解关键先利用Redis完成抢单业务将下单业务同步变异步为了解决阻塞队列的不足redis消息队列消息队列也被称为消息代理Message Broker解耦不受jvm内存限制数据安全 持久化 消息至少被消费一次基于Pub/Sub实现消息队列基于List实现消息队列需要注意的是当队列中没有消息时RPOP和LPOP操作会返回NULL而不像JVM阻塞队列那样会阻塞并等待消息所以我们这里应该使用BRPOP或者BLPOP来实现阻塞效果缺点是remove and get无法避免消息丢失基于Stream的消息队列$可能会出现漏读STREAM类型消息队列的XREAD命令特点 1. 消息可回溯 2. 一个消息可以被多个消费者读取 3. 可以阻塞读取 4. 有漏读消息的风险消费者组多个消费者划分到一个组中监听同一个队列消息分流消息标识 记录最后一个被处理的消息消息确认 - 消费者获取消息后消息处于pending状态并存入一个pending-list当处理完成后需要通过XACK来确认消息标记消息为已处理才会从pending-list中移除 创建删除消费者组XGROUP CREATE/DESTORY/CREATECONSUMER/DELCONSUMER从消费者组/pending list中读取消息正常情况下 不要noackid用即可总结 STREAM类型消息队列的XREADGROUP命令的特点 1. 消息可回溯 2. 可以多消费者争抢消息加快消费速度 3. 可以阻塞读取 4. 没有消息漏读风险 5. 有消息确认机制保证消息至少被消费一次Stream消息队列实现异步秒杀下单项目启动时开启一个线程任务尝试获取stream.orders中的消息完成下单 stringRedisTemplate.opsForStream().read SACK stream.orders g1 idSortedSet——博客和点赞TableField(exist false)用来解决实体类中有的属性但是数据表中没有的字段Redis Sortedset 集合来判断是否点赞过按点赞顺序展示点赞用户 没有isMember方法通过查询score来判断集合中是否有该元素博主与粉丝的关系数据库中有一张tb_follow表来标示共同关注 Redis集合求交集SINTERFeed流推荐算法 Timeline 模式拉模式/读扩散推模式/写扩散推拉结合/读写混合 用户少这里采用写扩散博文发布成功后立即将其ID主动推送Push到所有粉丝的收件箱Redis ZSet通过增加写操作的复杂度来换取用户读取Feed时极低的时间复杂度和极高的性能如何优化—— 引入消息队列 (Message Queue)高效滚动分页动态的信息流Feed Stream场景下传统的“分页”模式通常指基于LIMIT和OFFSET的分页是不可行的会有内容重复或遗漏问题解决方法基于游标的分页Redis 的 List不支持游标 故使用SortedSetZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]参数解释key: Sorted Set 的键名。max: 查询范围的最大分数值score。min: 查询范围的最小分数值score默认0即可[LIMIT offset count]: 在查询出的结果内部再进行偏移和计数解决SQL的in查询结果顺序是未定义的 使用ORDER BY FIELD()来自定义排序顺序GEO数据结构——附件商户底层数据结构确实是 Sorted Set 元素添加是O(log(N)) N是sorted set的元素数量Geohash 算法的核心是生成一个代表地理位置的二进制位序列。Geohash 字符串是这个二进制序列的一种Base32 编码便于人类和通用系统使用。Redis Geo 的score是这个二进制序列的整数表示是为了适配 Sorted Set 的数据结构并实现极致的查询性能GEOSEARCH key[FROMMEMBER member][FROMLONLAT longitude latitude][BYRADIUS radius m|km|ft|mi][BYBOX width height m|km|ft|mi][ASC|DESC][COUNT count[ANY]][WITHCOORD][WITHDIST][WITHHASH]BitMap——签到Redis中是利用String类型数据结构实现BitMap因此最大上限是512M转换为bit则是2^32个bit位SETBIT向指定位置offset存入一个0或1BITCOUNT统计BitMap中值为1的bit位的数量BITFIELD操作查询、修改、自增BitMap中bit数组中的指定位置offset的值这里签到业务就用 用户和data 拼接做key bitmap标记哪一天签到了签到统计对于redis返回的结果 从后往前遍历每个bit位统计连续的1 获取本月所有签到BITFIELD key GET u[dayOfMonth] 0UV 统计UV全称Unique Visitor也叫独立访客量是指通过互联网访问、浏览这个网页的自然人。1天内同一个用户多次访问该网站只记录1次。PV全称Page View也叫页面访问量或点击量用户每访问网站的一个页面记录1次PV用户多次打开页面则记录多次PV。往往用来衡量网站的流量。HyperLogLogHyperLogLog(HLL)是从Loglog算法派生的概率算法用户确定非常大的集合基数而不需要存储其所有值Redis中的HLL是基于string结构实现的单个HLL的内存永远小于16kb作为代价其测量结果是概率性的有小于0.81的误差。不过对于UV统计来说这完全可以忽略。原文地址Redis 实战篇本文持续更新最新内容及相关资料请访问原文。