Redis缓存实战:从数据类型到分布式锁,看完这篇就够了
一、Redis数据类型1.1 五种基本数据类型类型命令使用场景StringSET/GET简单缓存、计数器、分布式锁HashHSET/HGET对象缓存、购物车ListLPUSH/LPOP队列、消息队列、粉丝列表SetSADD/SMEMBERS标签、抽奖、去重Sorted SetZADD/ZRANGE排行榜、延迟队列1.2 String详解// 基本操作redis.set(key,value);Stringvalueredis.get(key);// 计数器redis.incr(view:article:1);// 原子递增redis.decr(stock:product:100);// 原子递减// 设置过期时间redis.setex(captcha:13800138000,300,1234);// 5分钟过期// 分布式锁redis.setnx(lock:order:orderId,1);redis.expire(lock:order:orderId,30);// 30秒超时1.3 Hash使用场景// 缓存用户对象redis.hset(user:1001,name,张三);redis.hset(user:1001,age,25);redis.hset(user:1001,city,北京);// 获取全部字段MapString,Stringuserredis.hgetAll(user:1001);// 购物车实现redis.hincrby(cart:1001:product:888,1);// 商品数量1redis.hincrby(cart:1001:product:888,-1);// 商品数量-1二、Redis缓存策略2.1 缓存模式Cache Aside旁路缓存最常用// 读操作publicUsergetUserById(Longid){StringcacheKeyuser:id;Useruser(User)redis.get(cacheKey);if(user!null){returnuser;// 缓存命中}// 缓存未命中查数据库useruserDAO.findById(id);// 写入缓存设置过期时间redis.setex(cacheKey,3600,user);// 1小时过期returnuser;}// 写操作publicvoidupdateUser(Useruser){// 1. 先更新数据库userDAO.update(user);// 2. 删除缓存不是更新redis.del(user:user.getId());}为什么删除而不是更新线程A更新数据 → 线程B查询缓存 → 缓存未命中查数据库 → 数据库返回旧值 → 写入缓存 线程A更新数据 → 删除缓存 结果缓存中是旧数据 线程A更新数据 → 删除缓存 线程B查询缓存 → 缓存未命中 → 查数据库 → 数据库已是新值 → 写入缓存 结果缓存中是正确数据2.2 缓存常见问题问题解决方案缓存穿透布隆过滤器 / 空值缓存缓存击穿互斥锁 / 热点数据永不过期缓存雪崩随机过期时间 / 多级缓存缓存穿透布隆过滤器// 使用布隆过滤器判断数据是否存在BloomFilterStringbloomFilterBloomFilter.create(...);// 查询前先判断publicUsergetUserById(Longid){if(!bloomFilter.mightContain(user:id)){returnnull;// 一定不存在}// 继续查缓存和数据库...}缓存击穿互斥锁publicUsergetUserById(Longid){StringcacheKeyuser:id;Useruserredis.get(cacheKey);if(usernull){StringlockKeylock:user:id;StringlockValueredis.setnx(lockKey,1);if(lockValue){useruserDAO.findById(id);redis.setex(cacheKey,3600,user);redis.del(lockKey);}else{Thread.sleep(50);returngetUserById(id);}}returnuser;}三、Redis持久化3.1 RDB vs AOF特性RDBAOF原理定时快照记录所有写命令文件大小小大恢复速度快慢数据完整性可能丢数据可配置完整性性能影响fork子进程每次操作3.2 RDB配置# 多久触发一次快照save9001# 900秒内1个key变化save30010# 300秒内10个key变化save6010000# 60秒内10000个key变化# 文件名dbfilename dump.rdb# 存放目录dir./3.3 AOF配置# 开启AOFappendonlyyes# 同步策略appendfsync always# 每次写都同步最安全最慢appendfsync everysec# 每秒同步推荐appendfsync no# 操作系统决定# AOF文件重写auto-aof-rewrite-percentage100auto-aof-rewrite-min-size 64mb四、Redis集群4.1 主从复制# 从节点配置replicaof192.168.1.1006379replica-read-onlyyes4.2 哨兵模式Sentinel# 启动哨兵redis-sentinel sentinel.conf作用监控主从节点健康自动故障转移通知客户端// Java客户端连接哨兵JedisSentinelPoolpoolnewJedisSentinelPool(mymaster,Set.of(192.168.1.101:26379,192.168.1.102:26379),newJedisPoolConfig());4.3 Redis Cluster# 6个节点集群redis-server --cluster-enabledyes--cluster-config-file nodes.conf# 创建集群redis-cli--clustercreate192.168.1.1:6379...192.168.1.6:6379 --cluster-replicas1架构┌─────────┐ │ Slot 0-5460 │ ──→ 节点1 └─────────┘ ┌─────────┐ │Slot 5461-10922│ ──→ 节点2 └─────────┘ ┌─────────┐ │Slot 10923-16383│ ──→ 节点3 └─────────┘数据分片16384个slot根据CRC16(key) % 16384 决定key落在哪个slot每个节点负责一部分slot五、Redis分布式锁5.1 基础实现publicclassRedisLock{privateJedisjedis;privateStringlockKey;privateStringlockValue;privateintexpireTime;publicRedisLock(Jedisjedis,StringlockKey,intexpireTime){this.jedisjedis;this.lockKeylockKey;this.lockValueUUID.randomUUID().toString();this.expireTimeexpireTime;}// 加锁publicbooleantryLock(){Longresultjedis.setnx(lockKey,lockValue);if(result1){jedis.expire(lockKey,expireTime);returntrue;}returnfalse;}// 释放锁只能释放自己的锁publicvoidunlock(){Stringscriptif redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end;jedis.eval(script,1,lockKey,lockValue);}}5.2 Redisson推荐// Redisson分布式锁ConfigconfignewConfig();config.addServerAddress(redis://192.168.1.100:6379);RedissonClientredissonRedisson.create(config);// 获取锁RLocklockredisson.getLock(order:lock:1001);try{booleanlockedlock.tryLock(10,30,TimeUnit.SECONDS);if(locked){processOrder();}}finally{lock.unlock();}5.3 分布式锁注意事项注意事项说明锁必须有过期时间防止死锁锁值要唯一防止误删别人的锁加锁/解锁要原子用Lua脚本保证集群环境用Redisson等成熟框架六、Redis实战案例6.1 抢购限流publicclassSeckillService{privateJedisjedis;// 库存预减publicbooleanseckill(LonguserId,LongproductId){StringstockKeyseckill:stock:productId;StringuserKeyseckill:user:productId;// 1. 检查是否已抢购if(jedis.sismember(userKey,userId.toString())){thrownewRuntimeException(已抢购过);}// 2. 原子性扣库存Longstockjedis.decr(stockKey);if(stock0){jedis.incr(stockKey);thrownewRuntimeException(库存不足);}// 3. 记录用户jedis.sadd(userKey,userId.toString());// 4. 异步下单MQorderMQ.send(userId,productId);returntrue;}}6.2 排行榜实现publicclassRankService{privateJedisjedis;// 更新用户分数publicvoidupdateScore(LonguserId,intscore){jedis.zadd(rank:scores,score,userId.toString());}// 获取TOP NpublicListMapString,ObjectgetTopN(intn){SetZSetOperations.TypedTupleStringtopSetjedis.zrevrangeWithScores(rank:scores,0,n-1);ListMapString,ObjectresultnewArrayList();intrank1;for(ZSetOperations.TypedTupleStringitem:topSet){MapString,ObjectmapnewHashMap();map.put(rank,rank);map.put(userId,item.getValue());map.put(score,item.getScore().intValue());result.add(map);}returnresult;}// 获取用户排名publicLonggetUserRank(LonguserId){returnjedis.zrevrank(rank:scores,userId.toString());}}6.3 延迟队列publicclassDelayQueueService{privateJedisjedis;// 添加延迟任务publicvoidaddTask(LongorderId,intdelaySeconds){longexecuteTimeSystem.currentTimeMillis()delaySeconds*1000;jedis.zadd(delay:queue,executeTime,orderId.toString());}// 轮询获取过期任务publicListLonggetExpiredTasks(){longnowSystem.currentTimeMillis();SetStringtasksjedis.zrangeByScore(delay:queue,0,now);ListLongexpiredTasksnewArrayList();for(StringtaskId:tasks){Longremovedjedis.zrem(delay:queue,taskId);if(removed1){expiredTasks.add(Long.parseLong(taskId));}}returnexpiredTasks;}}总结模块核心知识点数据类型String/Hash/List/Set/ZSet缓存策略Cache Aside/穿透/击穿/雪崩持久化RDB/AOF/混合持久化集群主从/哨兵/Cluster分布式锁setnx/Lua/Redisson实战抢购/排行榜/延迟队列核心口诀缓存三剑客穿透击穿雪崩分布式锁setnx加过期集群主从加哨兵Redis在手缓存无忧。