
Redis快的原因纯内存操作没有磁盘IO单线程模式没有多线程的上下文切换IO多路复用一个单线程监听所有网络连接高效的数据结构如果Redis是多线程的话多线程操作共享数据必须加锁。多个线程同时写同一个key会出现并发错乱。锁会带来锁竞争、死锁、锁等待代码复杂度暴涨线程的上下文切换开销大Redis核心工作是内存读写不消耗CPU单线程就足够且天然线程安全没有上下文开销IO多路复用的解释没有多路复用的话一个客户端连接就需要一个线程去等待数据数据通过网络发过来比较慢线程就在那等着Redis把全部的socket的链接都交给操作系统数据发过来了就唤醒Redis的线程来处理Redis里面的常用数据结构和语法String 可存字符串、数字应用场景缓存、计数器、分布式锁# 设置key value set key value # 设置同时设置过期时间单位秒 set key value ex 10 # 不存在才设置分布式锁用 setnx key value # 获取 get key # 删除 del key # 数字自增 incr key # 增加指定数值 incrby key 5 # 自减 decr key # 批量设置 mset k1 v1 k2 v2 # 批量获取 mget k1 k2INCR 这个操作在 Redis 内部是读取→加1→写回三步但 Redis 单线程保证这三步不会被其他命令打断要么全部执行完要么不执行不存在两个请求同时读到 0、都加 1、结果都写成 1 的并发问题。这就是原子性是后面限流和分布式锁的基础Hash存储方法hset key field value对于hset user:100 name zhangsan age 20Redis keyuser100整个hash的大keyfieldname、agevaluezhangsan、20user:100 { name → zhangsan age → 20 }Hash两种底层编码ziplist压缩列表小hash的时候当同时满足field数量不多默认 hash‑max‑ziplist‑entries 512每个field、value字节都不大默认 hash‑max‑ziplist‑value 64字节底层用ziplist压缩列表存储。ziplist是连续内存数组按顺序存放 field1,value1,field2,value2 。内存紧凑节省内存。一旦超出上面任意阈值Redis自动转成 hashtable 编码。hashtable字典dict大hash当hash的field变多、或者字段value变长自动升级为 dict哈希表。底层就是Redis自己实现的哈希字典和Java HashMap类似数组链表发生hash冲突链表挂链。查询复杂度 O(1)内存开销会比ziplist大。不需要手动切换编码Redis自动做。总结Redis Hash外层是Redis大key内部存储多组field‑value底层有两种编码数据量小时用ziplist压缩列表连续内存节省空间当字段数量或者value大小超过阈值自动转为dict哈希表Hash只能给整个key设置过期无法单独给field设置过期时间适合存储对象支持局部字段更新操作语法# 设置字段 hset user:100 name zhangsan age 20 # 获取单个字段 hget user:100 name # 获取全部field‑value hgetall user:100 # 获取多个字段 hmget user:100 name age # 判断field是否存在 hexists user:100 name # 删除某个字段不是删整个key hdel user:100 age # 字段数值增加 hincrby user:100 age 1 # 获取所有field hkeys user:100 # 获取所有value hvals user:100Set集合无序、元素不可重复# 添加元素 sadd set1 a b c a # 获取全部元素 smembers set1 # 判断是否存在 sismember set1 a # 删除元素 srem set1 b # 获取集合大小 scard set1 # 随机弹出一个元素 spop set1 # 差集、交集、并集 sdiff setA setB sinter setA setB sunion setA setBRedis中的三个经典问题缓存穿透问题查询一个数据库里根本不存在的数据Redis 没有MySQL 也没有每次请求都打到 MySQL。攻击者用大量不存在的 id 请求压垮数据库解决方法缓存空值缓存null数据库查询为空仍然往Redis写一个keyvaluenull设置短过期时间比如3‑5分钟下次再查直接返回缓存null不会访问数据库。缺点会存很多无用key占用内存伪代码逻辑// 查询缓存Stringcacheredis.get(goods:100);if(cache!null){returncache;}// 查数据库Goodsgoodsmapper.selectById(100);if(goodsnull){redis.set(goods:100,null,3,TimeUnit.MINUTES);//缓存空短过期returnnull;}redis.set(goods:100,JSON.toJSONString(goods),30,TimeUnit.MINUTES);returngoods;布隆过滤器 BloomFilter生产常用把所有合法存在的业务id全部预加载进布隆过滤器。请求过来先过布隆过滤器布隆说不存在 → 直接返回不去查Redis、数据库布隆说存在 → 才继续查缓存、数据库特点布隆过滤器存在误判不会漏判说不存在一定不存在说存在有可能实际不存在适合海量数据拦截恶意id。缓存穿透问题查询一个数据库里根本不存在的数据Redis 没有MySQL 也没有每次请求都打到 MySQL。攻击者用大量不存在的 id 请求压垮数据库解决方案互斥锁分布式锁缓存失效的时候只放行一个线程去查数据库写缓存其他线程等待或者重试伪流程 - 缓存未命中 - 获取分布式锁拿到锁的线程去查DB回写缓存 - 没拿到锁的线程sleep一小段时间重试读缓存热点key永不过期逻辑过期Redis不设置TTL过期时间。在value里面存一个逻辑过期时间字段。所有请求直接返回缓存数据单独开一个后台异步线程定时刷新缓存。Redis 中的 key 物理上永不过期是否失效由代码中的逻辑过期时间来判断缺点数据在刷新窗口期内可能会有短暂的不一致。提前主动刷新热点key热点数据在快要过期前程序主动更新缓存不让它过期。缓存雪崩问题查询一个数据库里根本不存在的数据Redis 没有MySQL 也没有每次请求都打到 MySQL。攻击者用大量不存在的 id 请求压垮数据库。解决方案过期时间加随机值在原有TTL基础上增加随机偏移量例 30分钟 随机0~5分钟 避免大批量key同一时刻过期哨兵服务熔断和降级Lua脚本Redis 支持执行 Lua 脚本。你可以把多条 Redis 命令写进一个 Lua 脚本一次性发给 Redis 执行保证了原子性减少了网络开销一般用在限流里面如果限流不用Lua脚本Long count redisTemplate.opsForValue().increment(key); // 步骤1计数 if (count 1) { redisTemplate.expire(key, 60, TimeUnit.SECONDS); // 步骤2设置过期 }问题步骤1和步骤2之间如果服务器突然宕机INCR 执行了但 EXPIRE 没执行这个 key 就永不过期限流计数器永远不归零这个用户就被永久限流了。而且高并发下判断 count1也有并发问题先解释脚本语法-- test.lua local key KEYS[1] local val ARGV[1] redis.call(SET, key, val) return redis.call(GET, key)– test.lua– 是lua的注释后面内容不会执行。local key KEYS[1]local lua声明局部变量只在当前脚本内生效。不加local就是全局变量 KEYS Redis传给Lua脚本的专用数组专门存redis的key。KEYS[1] 取传入的第1个keylua数组下标从1开始不是0这点非常容易踩坑不要写 KEYS[0] 拿不到数据。local val ARGV[1]ARGV 同样是Redis内置数组存放普通参数value、数字不是redis的key。ARGV[1] 获取传入的第一个参数值。KEYS[] → 存Redis的key要操作的redis键ARGV[] → 普通参数value、过期时间、扣减数量等不是keyredis.call(‘SET’, key, val)redis.call() 调用Redis命令。第一个字符串是redis命令名后面是命令参数。等价执行 SET key val如果这条redis命令报错整个脚本直接终止后面代码不再运行。不会回滚的对比 redis.pcall(‘SET’,key,val)pcall遇到错误不会终止脚本会把错误信息返回脚本继续向下跑。开发大部分场景用 callreturn redis.call(‘GET’, key)把get拿到的结果作为脚本返回值交给Java/客户端怎么传KEYS和ARGVredis-cli--evaltest.lua name , zhangsantest.lua 脚本文件name → 对应 KEYS[1] 这是redis key, 分隔符逗号前后必须有空格逗号左边全部是KEYS逗号右边全部是ARGVzhangsan →对应 ARGV[1] value脚本执行逻辑 SET name zhangsan 然后 GET name 返回 zhangsan分布式锁解决并发问题时Java 单机可以用 synchronized 或 ReentrantLock 加锁保证同一时间只有一个线程执行抽奖。但这是单机锁——只能锁住当前这台服务器分布式场景如果项目部署了多台服务器负载均衡请求 A 打到服务器1请求 B 打到服务器2两台服务器各有各的 JVM synchronized 根本锁不住对方分布式锁就是把锁存在一个所有服务器都能访问的公共地方Redis谁先抢到谁执行其他服务器等待SET key value NX EX 30NX Only set if Not eXistskey 不存在时才设置成功相当于抢锁存在则失败EX 30 30 秒自动过期防止拿到锁的服务器宕机锁永远不释放 → 死锁value 存唯一标识如 UUID释放锁时验证是不是自己的锁防止误删别人的锁详解核心目标互斥同一时间只能有一个客户端拿到锁防死锁锁一定要有过期时间客户端挂了锁要能自动释放解铃还须系铃人谁加锁谁才能释放锁不能把别人的锁释放掉可重入可选同一个线程可以多次获取同一把锁高可用Redis集群环境锁依然可靠注意点setnx和expire写成一条原子命令set lock:order uuid-xxx NX EX 30value用唯一的UUID防止别人删了这个锁删锁的时候判断value是不是自己的UUID-- 释放锁 lua脚本ifredis.call(GET,KEYS[1])ARGV[1]thenreturnredis.call(DEL,KEYS[1])elsereturn0end看门狗机制解决业务时间锁过期时间看门狗不要自己写直接用Redisson封装的获取锁成功后启动一个后台守护线程每 leaseTime/3 默认10秒检查一次如果业务线程还持有锁就把锁过期时间重新刷到30秒如果业务执行完毕线程退出不再续期锁到期自动释放