1. 先把Redis装好:下载与安装全流程
Redis的使用起点,永远是先把环境跑起来。很多新手第一次接触redis时最容易卡壳的,不是命令记不住,而是“下载什么版本”“怎么安装”“装完怎么验证”。这一节我把常见的安装方式从头到尾捋一遍,覆盖Windows、Linux、macOS三种环境,照着手动操作就可以。
1.1 选择版本与下载途径
先说版本。Redis官方目前维护的稳定分支主要是7.2和7.4,6.2还在维护期但已经属于老版本了。个人项目和中小型业务,我推荐直接上7.2以上版本,7.0开始引入的ACL、Redis Function、多AOF重写等能力,对后面做权限管理和复杂逻辑都更友好。如果你只是在本地学习命令,那版本差异不大,选个最新的稳定版就行。
下载渠道方面,官方站点redis.io提供了源码包,Linux/macOS下用源码编译是标准做法。Windows环境稍微特殊一点:Redis官方并不提供Windows原生安装包,而是推荐使用WSL。如果不想开WSL,可以找Microsoft维护的Redis on Windows镜像,或者使用第三方编译好的zip包,这类包在GitHub上搜redis-windows或memurai就能找到,适合临时测试用,但不建议直接拿来做生产环境。
1.2 三种平台安装实操与验证
Linux(CentOS/Ubuntu系)
源码编译安装是我最推荐的方式,因为可以通过编译参数控制可执行文件、配置路径和内存分配器。以7.2.5版本为例:
wget https://download.redis.io/releases/redis-7.2.5.tar.gz tar xzf redis-7.2.5.tar.gz cd redis-7.2.5 make make install PREFIX=/usr/local/redis这里有个细节:make install PREFIX=...如果不指定prefix,默认装到/usr/local/bin下面,相关文件会比较分散。指定了PREFIX,所有二进制都会集中在指定目录的bin子目录里,后续维护、卸载都方便。
Ubuntu/Debian系则更简单,可以直接用apt:
sudo apt update sudo apt install redis-server安装完成后,Redis会自动注册成systemd服务,用systemctl status redis-server检查状态即可。
验证安装的核心命令只有两条:
redis-cli ping返回PONG说明服务已经起来了。再看一下版本信息:
redis-server --version redis-cli --statmacOS
macOS下有两条路:用Homebrew最省事,一条命令搞定:
brew install redis brew services start redis如果喜欢手动控制,也可以走源码编译,过程和Linux完全一样。实测下来,brew安装的好处是自动处理好路径和服务管理,但对Redis源码里的日志和配置位置没有太严格的管控,需要自己熟悉目录结构。
Windows
生产环境我不建议在Windows上直接跑Redis,但日常学命令和开发调试,用WSL2是最稳的:
wsl --install -d Ubuntu # 进入WSL后 sudo apt update sudo apt install redis-server如果你不想装WSL,退而求其次可以用GitHub上的redis-windows项目,下载zip包解压后直接运行redis-server.exe。需要注意,这类镜像通常停留在较旧的Redis版本,某些新特性(比如RESP3协议的部分功能)用不了。
装完之后别急着敲命令,先做一次“体检”:用redis-cli ping确认连通,再用redis-cli info server看进程版本、运行模式、端口等关键信息,确认配置加载正常。这步虽然简单,但能省掉很多后面排查的冤枉路。
2. 数据类型才是Redis的“内功”
Redis入门者最容易犯的一个错误,是把它当成“高级版的HashMap”,所有东西都往String里塞。等数据量上来之后才发现,查询效率、内存占用、并发控制全都不对劲。Redis真正值钱的是它的数据类型——每种类型都对应一套独有的操作命令和适用场景,选对类型,后面写业务逻辑会顺滑很多。
2.1 String类型:最基础也最容易用错
String是Redis里最基础的类型,一个key对应一个value,value最大能存512MB。常用命令包括SET、GET、MSET、MGET、INCR、DECR、SETEX、SETNX等。
我开发时用String最多的是这两个场景:一是缓存,把数据库查询结果直接缓存进Redis,key用业务前缀加ID,value用JSON;二是计数器,点赞数、PV统计、限流计数都可以用INCR轻松搞定,原子性天然保证。
# 缓存场景 > SET user:1001 '{"name":"zhangsan","age":20}' EX 300 > GET user:1001 # 计数场景 > INCR page:view:20240101 > INCRBY page:view:20240101 100这里需要特别提醒一个坑:INCR只对整数类型的value有效。如果value本身是“12abc”这种非纯数字串,执行INCR会报错。我见过不少新手在业务里把用户ID转成字符串后去做自增,结果直接抛异常。
关于String的使用,还有两个容易忽略的操作:
第一,SET命令的NX和XX参数。SET key value NX表示只有当key不存在时才设置,常用于分布式锁。SET key value XX则相反,只有当key存在时才更新。这两个参数加上EX过期时间,配合起来用能覆盖很多原子操作的场景。
第二,SETEX和SETNX的区别。SETEX同时完成设值和过期时间设置,比分开用SET和EXPIRE更安全,减少了两个命令之间时间窗口里key意外释放的问题。这在分布式环境下尤为重要。
2.2 Hash类型:对象数据的最优解
Hash类型底层是一个字符串字段和字符串值之间的映射表,非常适合存储“对象”。一个用户有name、age、email等多个字段,如果用String存,需要拼一个序列化后的JSON,改一个字段就得整存整取;用Hash,字段级增删改查都很方便。
> HSET user:1001 name zhangsan age 20 email zhangsan@example.com > HGET user:1001 name > HGETALL user:1001 > HINCRBY user:1001 age 1Hash的优点不需要我多啰嗦,关键分享两个实战经验:
一是什么时候不该用Hash。如果一个对象的字段是动态增加的(比如一个订单可能有1到100个商品条目),用Hash会导致key数量失控,而且HGETALL在字段超多时还可能阻塞Redis单线程,这种场景更推荐用List存放子项ID,再用String或Hash保存每个子项的详情。
二是Hash的节省内存效果。当字段数小于某个阈值、字段值较短时,Redis会使用listpack编码,内存占用比dict编码小很多。实测一个包含5个字段的Hash对象,在listpack编码下比String存储JSON串能省30%到40%的内存。如果一个业务里有百万级对象,这个省下来的量就很可观了。
2.3 List类型:队列与时间线场景的默认选择
List底层的结构比较有意思:元素少的时候用quicklist,元素多了之后自动转成linkedlist。它的特性是支持从两端推入(LPUSH/RPUSH)和弹出(LPOP/RPOP),所以两种最常见的用法是:栈和队列。
我做消息队列时通常用LPUSH + BRPOP组合:
# 生产者 > LPUSH task:queue "job1" > LPUSH task:queue "job2" # 消费者(阻塞弹出) > BRPOP task:queue 0BRPOP的第二个参数是超时时间,0表示永不超时,这是List做MQ的核心优势——客户端不会空转轮询,而是被Redis阻塞住,数据到达时立即唤醒,配合超时上限可以做到低延迟消费。
List还有几个很容易被忽略但很实用的命令:
LRANGE key start stop:按索引范围取元素,做分页很好用,但注意列表很长时,LRANGE 0 -1会一次性把所有元素都取出来,内存会爆,要控制分页大小。LTRIM key start stop:只保留start到stop区间的元素,其余全部删除。这是做“最新N条记录”的最佳工具,每次LPUSH后跟一条LTRIM key 0 N-1,列表永远只保留最新的N条。LINSERT key BEFORE|AFTER pivot value:在指定值的前或后插入元素,双链表操作,适用于需要动态调整顺序的场景。
2.4 Set类型:无序集合与点赞关注
Set对应数学上的“集合”,元素不重复,且天然无序。常用命令有SADD、SREM、SMEMBERS、SISMEMBER、SINTER、SUNION、SCARD等。
点赞、收藏、标签、好友关系,只要有“去重”和“集合运算”需求的场景,几乎都能用Set解决。举一个实际案例:一个UGC平台做“我关注的人的动态流”,动态ID都存到Set里,每次取动态流时用SINTER求多个关注集合的交集,就能快速过滤掉不关注的用户发布的动态。
> SADD user:1001:follow 2001 2002 2003 > SADD user:1001:follow 2004 > SCARD user:1001:follow # 关注数 > SISMEMBER user:1001:follow 2002 # 是否已关注 > SINTER user:1001:follow user:2002:follow # 共同关注这里分享我踩过的一个坑:SINTER如果集合很大,计算量会很大,Redis是单线程的,长时间运算会阻塞其他命令。我遇到过线上一个集合有百万级别成员,做交集计算时,Redis的延迟从不到1毫秒涨到了秒级别。所以Set运算只适合中小规模数据集,大集合场景要么裁剪数据,要么借助外部计算引擎。
2.5 ZSet类型:有序集合与排行榜
ZSet是在Set基础上加了score字段,元素按score从小到大排序。这个特性让它在排行榜、延时队列、滑动窗口限流等场景里几乎碾压一切方案。
> ZADD player:rank 100 player1 > ZADD player:rank 88 player2 > ZRANGE player:rank 0 -1 WITHSCORES > ZREVRANGE player:rank 0 9 # 前10名(从大到小) > ZINCRBY player:rank 12 player1 # 加分ZSet用得妙的地方很多,这几种是我实际开发中觉得性价比最高的:
- 排行榜:写入时直接用
ZADD设置分数,查询时用ZREVRANGE拿名次,单条命令完成,比MySQL舒服太多。 - 延时队列:score存任务的执行时间戳,消费者用
ZRANGEBYSCORE key -inf now LIMIT 0 1拿到期任务,配合哨兵循环就能做可靠延时队列。 - 滑动窗口限流:score存请求时间戳,每次请求前用
ZREMRANGEBYSCORE清掉窗口外的记录,再用ZCARD统计窗口内请求数,达到阈值就拒绝。
还有一个细节,Distributed锁相关场景也常借助ZSet:多个节点同时抢锁时,把节点信息写入同一个ZSet,score为获取时间,通过ZRANK判断谁最先请求,从而实现公平锁的排队效果。不过这个用法相对少见,更多是作为理解ZSet有序性的练习题。
3. 进阶数据结构与真实场景对应
五种基础类型能覆盖大部分业务,但Redis还有几个冷门却极其有用的高级类型:Bitmaps、HyperLogLog、GEO和Stream。这几个类型各自的适用场景非常明确,用对了能让系统性能提升一大截。
3.1 Bitmaps、HyperLogLog、GEO、Stream各自能干的事
Bitmaps
Bitmaps本质上还是String类型,但是按bit位操作。擅长做“布尔状态存储”,比如用户是否登录过、是否签到过、某个商品的某些属性标记。一个bit只占1位内存,一亿用户只需约12.5MB内存,非常省。
典型场景是连续签到统计:
# 假设用户ID在100001,签到天数为第30天,直接置位 > SETBIT sign:202501 100001 1 > BITCOUNT sign:202501 # 统计当月签到总人数 > GETBIT sign:202501 100001 # 查用户是否签到过HyperLogLog
基数统计类型,用来做UV统计(不重复用户数),标准误差率约0.81%。它最大优势是内存极低,无论统计多少数据,采用固定12KB内存结构。
> PFADD page:uv 1001 1002 1003 > PFADD page:uv 1001 > PFCOUNT page:uv # 返回3UV统计的精确度不是关键需求时,HyperLogLog就是最好的选择。但它是概率估算,误差范围在预算时要心里有数,绝对不能拿来做计费、限流等对准确度苛刻的功能。
GEO
地理位置类型,底层是ZSet,但精度更高。可以存经纬度坐标,支持计算两点间距离、查找某个半径内的元素。美团的“附近的人”、打车软件的“附近司机”,底层都是GEO。
> GEOADD driver:loc 116.397 39.908 "driver1" > GEOADD driver:loc 121.473 31.230 "driver2" > GEOSEARCH driver:loc FROMLONLAT 116.4 39.9 BYRADIUS 5 km ASCStream
Redis 5.0引入的消息队列类型,比List更正式,支持消费者组、消息确认、死信队列等。项目里如果不想引入Kafka这类重型MQ,又需要可靠消息投递,Stream是最合适的替代方案。
> XADD event:stream * user_id 1001 action "login" > XREAD COUNT 10 STREAMS event:stream 0-0 > XGROUP CREATE event:stream group1 0Stream的姿态是“内存里的持久化消息队列”,这是它区别于RabbitMQ等磁盘队列的关键特性。
3.2 高并发场景下Redis的典型用法串讲
把数据类型组合起来,就能拼出很多高并发场景的完整方案。我以“秒杀系统”为例串一遍:商品详情用String做读缓存,可售卖件数用DECR做原子扣减,用户购买记录用Set去重防超卖,抢购时间窗口用ZSet做限流,消息通知用Stream做异步推送。整个链路下来,MySQL只用来做最终数据落盘,大部分热路径都打在Redis里,单机QPS能轻松破万。
另一个常见组合是“缓存+异步同步”。一个登录系统里,用户token和用户信息分别用String和Hash存,热点更新时更新Redis后异步同步到MySQL。组件间的数据流转用Stream做缓冲,不会因为同步失败就直接丢数据,可以配合ACK机制重试。这套组合让我在多个项目里避免了缓存穿透和同步链路崩溃的坑。
再举一个“排行榜+定时任务”的例子:每日赛榜用ZSet写入当天数据,凌晨定时任务把前一天榜单Score加一个偏移量后合并到总榜ZSet,这样随时能查到“本周”“本月”的累计排名,而且查询都是毫秒级。
4. 代码实操:从Jedis到Spring Data Redis
工具选型上,Java生态用得最多的是Jedis和Lettuce两个客户端。Jedis是BIO模型,连接数多时资源占用大;Lettuce底层基于Netty,使用单连接多线程复用,吞吐量更高,也是Spring Boot 2.0之后的默认客户端。我自己的项目更倾向于Lettuce,不光是Spring生态便利,更因为它天然支持Redis Sentinel和Cluster,主从切换、路由分片都自动处理,省去很多代码。
4.1 Jedis基础示例
直接看代码,一段最简单可靠的Jedis连接和读写:
JedisPoolConfig config = new JedisPoolConfig(); config.setMaxTotal(20); config.setMaxIdle(10); config.setMinIdle(5); try (JedisPool pool = new JedisPool(config, "127.0.0.1", 6379, 2000, null)) { String key = "user:1001"; try (Jedis jedis = pool.getResource()) { jedis.set(key, "{\"name\":\"zhangsan\"}"); String value = jedis.get(key); System.out.println(value); } }注意几个地方:
- 一定要用
JedisPool,不要每次new Jedis,不然连接管理完全失控。 try-with-resources(或者手动finally close)是执行规范,避免连接泄漏。pool.getResource()如果连接不够,会按setMaxTotal限制等待,这个等待时间不能太长,否则高并发下线程会积压,造成业务超时。
4.2 Spring Boot集成与配置
Spring Boot项目里引入依赖后,主要的配置都在application.yml里:
spring: data: redis: host: 127.0.0.1 port: 6379 password: yourpassword timeout: 3s lettuce: pool: max-active: 16 max-idle: 8 min-idle: 2 max-wait: 2s这里多提一句:Spring Boot 2.4之后的配置前缀是spring.data.redis,老版本是spring.redis,升级框架时不要照抄旧配置,容易踩坑。
随后注入RedisTemplate做读写:
@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // 设置key和hash key的序列化器为String,避免乱码 template.setKeySerializer(new StringRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); // value使用JDK序列化,注意后面单独讨论 template.setValueSerializer(new JdkSerializationRedisSerializer()); template.setHashValueSerializer(new JdkSerializationRedisSerializer()); template.afterPropertiesSet(); return template; } }这里涉及到一个经典问题:默认的RedisTemplate使用JDK序列化,会把value转成二进制对象,往redis里看就是“乱码”。如果业务侧主要存JSON,建议直接把ValueSerializer改成GenericJackson2JsonRedisSerializer。很多新人第一次调Redis接口时看到乱码,其实不是编码坏了,而是序列化策略不对。
4.3 序列化与缓存注解实战
Spring Cache的@Cacheable、@CacheEvict注解与Redis结合,是项目中减少重复代码的强大工具:
@Service public class ProductService { @Cacheable(cacheNames = "product", key = "#productId") public Product findById(Long productId) { return productMapper.selectById(productId); } @CacheEvict(cacheNames = "product", key = "#product.id") public void update(Product product) { productMapper.updateById(product); } }@Cacheable的逻辑是先去Redis找key为product::bookId的数据,命中直接返回,不命中执行方法并把结果写入缓存,下次就可以直接命中。实测中这个方案能让热点商品详情的查询QPS从几百涨到几千,而且代码继续简化。
但缓存注解有几个坑要提醒一下:
- 默认是基于
SimpleCacheManager,不配Redis时缓存只存在内存里,服务重启即失效。一定要确保配置了RedisCacheManager。 - key的生成规则如果不指定,会使用默认的
SimpleKeyGenerator,多个方法参数时容易生成不同key,导致缓存不命中。建议始终用key属性显式指定。 @Cacheable内部用的是RedisTemplate的序列化器,如果只改了全局的String序列化,还需要单独配置RedisCacheConfiguration里的序列化器,否则value仍是JDK二进制。
再分享一个经验:缓存更新模式建议采用“双删”或“懒加载”,而不要简单使用@CacheEvict删除后依赖数据库重建。因为在高并发下,删除缓存后瞬间有大量请求涌到DB,此时如果DB读延迟稍高,前面的线程就会再次打穿缓存,形成缓存击穿。我在一个“商品详情”改造项目中采用“先更新DB,再删缓存”的顺序,并配合短暂的空值缓存(cache empty value),整个击穿问题就被压下去了。
5. 常见问题与排查实录
Redis用久了,总要面对各种疑难杂症。我把这些年遇到过的大部分高频问题梳理一遍,每条都附上我的排查思路和最终解法,希望能帮你少踩几个坑。
5.1 连接失败、乱码、内存报警
连接失败:Could not connect to Redis
先确认三件事:服务是否启动(redis-cli ping)、端口是否通(telnet 127.0.0.1 6379)、密码是否对(配置文件里的requirepass)。Windows下最常见的问题是防火墙拦截了6379端口,Linux下则可能是bind 127.0.0.1导致外部IP连不上。如果配置了Sentinel,还需要检查Sentinel配置里的master-name是否和主从一致。我把“配置了密码但客户端忘了设”和“绑定了本机IP但客户端连的是公网IP”列为本地开发两大高频失误。
缓存显示乱码
这个现象我详细说下:通过redis-cli看key和value,如果是\xAC\xED\x00\x05t...之类的怪字符,说明JDK序列化桶生效了。几个处理策略:
- 如果不需要在Redis端做命令行查看和处理,乱码不影响业务功能,只是影响运维观察。
- 如果需要在Redis端做统计、TTL操作,建议value用JSON格式字符存储,也就是使用
GenericJackson2JsonRedisSerializer或直接手动JSON.toJSONString。 - 注意换序列化器前后的数据不兼容,会出现“原来写入的数据读出来还是乱码”的情况,迁移时最好先清空相关缓存或做版本隔离。
内存报警:OOM command not allowed when used memory > maxmemory
这是Redis内存超过maxmemory限制后的保护行为。常见解决办法是:
- 检查大key和大量过期key堆积:用
redis-cli --bigkeys扫描。 - 检查持久化策略:RDB或AOF占用的内存也被计入,不是只有数据。
- 合理配置淘汰策略:
maxmemory-policy allkeys-lru适用于缓存场景,但注意它可能误杀近期刚写的数据。如果是有状态的关键数据,不要用淘汰策略,而是优先扩容或做内存分层。
5.2 缓存穿透、击穿、雪崩与数据一致性
缓存穿透
访问一个缓存和数据库中都不存在的数据,每次都打到DB,导致DB压力爆炸。对策通常是两个:一是布隆过滤器,在缓存前加一层过滤,数据库中不存在的数据直接被拦截;二是空值缓存,把查询不到的结果以空对象缓存几分钟,短期内能挡掉大部分穿透量。空值缓存注意设置比较短的过期时间,防止大量空key堆积。
缓存击穿
热点key过期的一瞬间,大量请求同时打到DB。对热点key,可以采取“互斥更新”或“逻辑过期”:互斥更新是更新缓存时加锁,避免并发重建;逻辑过期是设置一个比真实TTL长的逻辑过期时间字段,由后台任务负责在逻辑过期后重建真实缓存,查询请求如果发现逻辑过期,则返回旧值并触发后台刷新。推荐后者,用户体验更好。
缓存雪崩
大量key同一时间过期,Redis直接变成“空城”,请求全量打到DB。把缓存过期时间加一个随机抖动(比如5到10分钟的随机数),这是最经典的解法。同时还可以部署Redis主从和分片,从高可用层面避免Redis节点宕机导致雪崩。
数据一致性
写操作如果先更新了DB,后续更新缓存失败,就会导致脏数据。我目前比较稳的方案是“先更新DB,再删除缓存”,然后通过消息队列异步补偿删除失败的缓存。如果对一致性要求极高,可以在删除失败时做一个短时间TTL兜底,确保缓存最多只有几秒的脏数据窗口。
注意:任何缓存一致性方案都无法做到绝对强一致,最后都要接受“缓存短暂脏数据”的现实。做架构决策时不要追求很弱的极端一致,而是基于业务容忍度,给出明确的兜底策略。
5.3 性能排查方法论与工具
性能问题不要拍脑袋猜,用工具说话:
redis-cli --latency:看本机到Redis的网络和命令处理延迟。redis-cli --stat:实时看连接数、内存、命中率等指标。redis-cli --bigkeys:找出大key,大key是阻塞主线程的主要元凶之一。比如一个百万成员的ZSet,一次ZRANGE WITHSCORES就能让Redis卡住几秒钟。SLOWLOG GET 10:查看慢命令日志,定位哪些命令执行时间过长。redis-cli -h host -p port --hotkeys(Redis 6.x之后):找出访问频率最高的key,检查是否命中热点缓存策略。
线上排查时,有一次我觉得是内存不足导致的全链路卡顿,检查半天才发现是某个服务每秒执行几千次SMEMBERS全量取一个大Set,那条命令一执行就要花300多毫秒,直接把Redis主线程拖死了。用SLOWLOG定位后,把全量改为增量SSCAN扫描,延迟就回到了个位数毫秒。这类问题没有工具辅助根本难以定位。
最后说一个我自己的习惯:每次在Redis上做一次上线变更,都会顺手记录变更前后的key数量、内存占用和平均延迟。时间长了你会发现,大部分线上故障都是“量变到质变”的过程,有了基线数据,下一次性能突然恶化时,判断起来就快很多。