作为 Java 后端开发的"瑞士军刀",Redis 在现代分布式系统中扮演着至关重要的角色。我第一次接触 Redis 是在一个高并发秒杀项目中,当数据库在 500QPS 时就开始出现连接池耗尽的情况,引入 Redis 后系统轻松扛住了 20000+ QPS 的冲击。这种性能提升的震撼感,让我深刻理解了为什么 Redis 会成为 Java 技术栈中的必备技能。
Redis 本质上是一个基于内存的键值存储系统,但与普通缓存不同的是,它提供了丰富的数据结构和原子操作能力。在工程实践中,我们主要利用 Redis 解决三类核心问题:
性能瓶颈突破:当 MySQL 的查询延迟达到 10ms 量级时,Redis 的读写操作通常能在 1ms 内完成。我曾优化过一个商品详情页接口,将热点数据放入 Redis 后,接口响应时间从 78ms 降至 3ms。
共享状态管理:在分布式环境下,多个服务实例需要共享计数器、配置等信息。通过 Redis 的原子操作(如 INCR),我们可以避免复杂的分布式锁实现。去年双十一大促时,我们就用 Redis 实现了全链路点击量统计。
系统解耦利器:Redis 的发布订阅和 Stream 特性,能够以极低耦合度实现服务间通信。我们有个订单状态变更通知系统,就是基于 Redis Stream 构建的,相比 Kafka 节省了 60% 的运维成本。
在典型架构中,Redis 的位置如下图所示:
code复制[客户端] → [Java 服务层] → [Redis] → [MySQL/其他持久化存储]
这个架构中,Redis 既作为缓存层吸收读压力,又作为共享存储协调分布式状态。值得注意的是,Redis 的单线程模型(6.0 前)使其具有天然的原子性保证,这在设计并发控制方案时非常有利。
关键认知:Redis 不是数据库的替代品,而是性能加速器和系统粘合剂。工程实践中要严格区分"可以丢失的数据"和"必须持久化的数据"。
很多初学者会困惑 Java 程序如何与 Redis 交互,这里存在一个常见的理解误区:认为它们之间是通过 HTTP 或 RPC 协议通信。实际上,Redis 使用自定义的 RESP(Redis Serialization Protocol)协议,这是一种基于 TCP 的二进制安全协议。
RESP 协议深度剖析:
举个例子,当执行 SET name Redis 命令时,实际传输的是:
code复制*3\r\n$3\r\nSET\r\n$4\r\nname\r\n$5\r\nRedis\r\n
这种设计使得协议解析极其高效,这也是 Redis 能达到百万级 QPS 的原因之一。
在 Java 生态中,我们通常通过客户端库来屏蔽协议细节。主流选择有:
以 Spring Boot 项目为例,典型配置如下:
java复制@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
连接池最佳实践:
我曾遇到一个线上事故:由于未配置连接超时(timeout),当 Redis 故障时导致业务线程全部阻塞。这个教训让我明白,即使使用高级封装,也需要深入理解底层通信机制。
String 是 Redis 最基础的数据类型,但其应用场景远不止简单的 KV 存储。在内存优化方面,Redis 会对不同长度的字符串采用不同的编码方式:
典型应用场景:
java复制// 存储用户对象
User user = userService.getById(1);
redisTemplate.opsForValue().set("user:1", user, 30, TimeUnit.MINUTES);
// 获取时建议使用这种模式防止缓存穿透
User cachedUser = redisTemplate.opsForValue().get("user:1");
if (cachedUser == null) {
cachedUser = userService.getById(1);
redisTemplate.opsForValue().set("user:1", cachedUser, 30, TimeUnit.MINUTES);
}
java复制// 加锁
Boolean locked = redisTemplate.opsForValue().setIfAbsent("lock:order", "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
// 业务逻辑
} finally {
redisTemplate.delete("lock:order");
}
}
// 注意:生产环境建议使用Redisson的RLock
java复制// 初始化
redisTemplate.opsForValue().set("article:100:views", "0");
// 原子递增
Long views = redisTemplate.opsForValue().increment("article:100:views");
// 统计日活
redisTemplate.opsForValue().setBit("dau:20230501", userId, true);
Long dau = redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) {
return connection.bitCount("dau:20230501".getBytes());
}
});
踩坑提醒:String 类型的 value 最大支持 512MB,但实际使用中建议控制在 10KB 以内,大 value 会阻塞 Redis 的单线程。
Hash 类型特别适合存储对象类型数据,与 String+JSON 的方案相比,它具有以下优势:
内存优化技巧:
当满足以下条件时,Redis 会使用 ziplist 编码(更省内存):
典型应用:
java复制// 存储用户信息
Map<String, String> userMap = new HashMap<>();
userMap.put("name", "张三");
userMap.put("age", "25");
redisTemplate.opsForHash().putAll("user:100", userMap);
// 部分更新
redisTemplate.opsForHash().put("user:100", "age", "26");
// 原子递增
redisTemplate.opsForHash().increment("user:100", "age", 1);
实战经验:
List 类型的 LPUSH+BRPOP 组合常被用作轻量级消息队列,但需要注意以下问题:
优势:
缺陷:
改进方案:
java复制// 生产者
redisTemplate.opsForList().leftPush("queue", message);
// 消费者(阻塞式)
while (true) {
Object message = redisTemplate.opsForList().rightPop("queue", 30, TimeUnit.SECONDS);
if (message != null) {
try {
processMessage(message);
} catch (Exception e) {
// 重新放回队列
redisTemplate.opsForList().leftPush("queue", message);
}
}
}
对于要求可靠消息的场景,建议使用 Redis Stream(5.0+版本):
java复制// 生产者
MapRecord<String, String, String> record = StreamRecords.string(Collections.singletonMap("data", "value")).withStreamKey("mystream");
redisTemplate.opsForStream().add(record);
// 消费者
StreamOffset<String> offset = StreamOffset.create("mystream", ReadOffset.lastConsumed());
Consumer consumer = Consumer.from("group1", "consumer1");
StreamReadOptions options = StreamReadOptions.empty().count(1).block(Duration.ofSeconds(30));
List<MapRecord<String, Object, Object>> records = redisTemplate.opsForStream().read(consumer, options, offset);
Set 的妙用:
java复制// 文章ID去重
redisTemplate.opsForSet().add("article:ids", "1001", "1002");
// 判断是否存在
Boolean exists = redisTemplate.opsForSet().isMember("article:ids", "1001");
java复制// 共同好友(交集)
Set<Long> commonFriends = redisTemplate.opsForSet().intersect("user:100:friends", "user:101:friends");
// 可能认识的人(差集)
Set<Long> recommendFriends = redisTemplate.opsForSet().difference("user:101:friends", "user:100:friends");
ZSet 实现排行榜:
java复制// 添加分数
redisTemplate.opsForZSet().add("leaderboard", "player1", 1000);
// 增加分数
redisTemplate.opsForZSet().incrementScore("leaderboard", "player1", 50);
// 获取TOP10
Set<ZSetOperations.TypedTuple<String>> top10 = redisTemplate.opsForZSet().reverseRangeWithScores("leaderboard", 0, 9);
// 分段统计
Long count = redisTemplate.opsForZSet().count("leaderboard", 1000, 2000);
性能优化点:
RDB(Redis Database)是 Redis 默认的持久化方式,通过生成数据快照来保证数据安全。在我的运维经历中,RDB 的配置优化对系统稳定性至关重要。
触发机制:
code复制save 900 1 # 900秒内至少1个key变化
save 300 10 # 300秒内至少10个key变化
save 60 10000 # 60秒内至少10000个key变化
RDB 优势:
RDB 风险点:
优化建议:
bash复制redis-cli info persistence
AOF(Append Only File)通过记录写命令来提供更可靠的持久化。在金融类项目中,我们通常采用 AOF 来确保数据安全。
工作流程:
AOF 重写机制:
随着时间推移,AOF 文件会膨胀。Redis 通过重写机制生成精简的新 AOF:
code复制auto-aof-rewrite-percentage 100 # 增长100%触发
auto-aof-rewrite-min-size 64mb # 最小64MB
混合持久化配置(4.0+):
code复制aof-use-rdb-preamble yes # 重写时先用RDB格式存储当前数据
性能调优经验:
bash复制redis-check-aof --fix appendonly.aof
主从复制配置:
code复制# 从节点配置
replicaof 192.168.1.100 6379
replica-read-only yes
复制流程:
哨兵高可用方案:
code复制sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
Java 客户端接入哨兵:
java复制@Bean
public RedisConnectionFactory redisConnectionFactory() {
SentinelConfiguration config = new SentinelConfiguration()
.master("mymaster")
.sentinel("sentinel1", 26379)
.sentinel("sentinel2", 26379);
return new JedisConnectionFactory(config);
}
运维经验:
内存分析工具:
bash复制redis-cli --bigkeys # 查找大key
redis-cli memory usage key # 查看key内存占用
redis-cli memory stats # 详细内存报告
优化策略:
配置建议:
code复制maxmemory 16gb # 设置为物理内存的3/4
maxmemory-policy volatile-lru # 对设置了TTL的key进行LRU淘汰
hash-max-ziplist-entries 512 # 小Hash使用ziplist
list-max-ziplist-size -2 # 列表使用quicklist
诊断命令:
bash复制redis-cli --latency # 基本延迟测试
redis-cli --latency-history # 分时段延迟
redis-cli --intrinsic-latency 50 # 服务器内在延迟
常见延迟原因:
慢查询配置:
code复制slowlog-log-slower-than 10000 # 超过10ms记录(微秒单位)
slowlog-max-len 128 # 保留128条记录
分析慢日志:
bash复制redis-cli slowlog get 10 # 获取10条慢查询
优化案例:
Pipeline 示例:
java复制List<Object> results = redisTemplate.executePipelined(
(RedisCallback<Object>) connection -> {
for (int i = 0; i < 1000; i++) {
connection.stringCommands().set(("key:" + i).getBytes(), ("value"+i).getBytes());
}
return null;
}
);
现象:大量 key 同时失效,请求直接打到数据库
解决方案:
java复制// 基础过期时间 + 随机偏移量
int expireTime = 3600 + new Random().nextInt(600);
redisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.SECONDS);
code复制[本地缓存] → [Redis集群] → [数据库]
java复制// 使用Hystrix或Resilience4j实现
@CircuitBreaker(name = "queryUser", fallbackMethod = "queryUserFallback")
public User queryUser(Long id) {
// 查询逻辑
}
现象:查询不存在的数据,导致无效请求穿透到数据库
解决方案:
java复制// 初始化布隆过滤器
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("userFilter");
bloomFilter.tryInit(1000000L, 0.01);
// 添加所有有效ID
bloomFilter.add("user:1001");
// 查询前校验
if (!bloomFilter.contains("user:1001")) {
return null;
}
java复制User user = redisTemplate.opsForValue().get(key);
if (user == null) {
user = userService.getById(id);
if (user == null) {
// 缓存空值,设置较短过期时间
redisTemplate.opsForValue().set(key, new NullValue(), 5, TimeUnit.MINUTES);
} else {
redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
}
}
现象:某个 key 访问量极大,造成单节点压力
解决方案:
java复制User user = localCache.get(key);
if (user == null) {
user = redisTemplate.opsForValue().get(key);
if (user == null) {
user = userService.getById(id);
redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
}
localCache.put(key, user, 1, TimeUnit.MINUTES);
}
java复制// 原始key
String hotKey = "product:1001";
// 分片策略
int shardId = new Random().nextInt(10);
String shardKey = hotKey + ":" + shardId;
// 写入时同步更新所有分片
for (int i = 0; i < 10; i++) {
redisTemplate.opsForValue().set(hotKey + ":" + i, value);
}
// 读取时随机选择一个分片
String value = redisTemplate.opsForValue().get(hotKey + ":" + shardId);
集群配置:
code复制cluster-enabled yes
cluster-config-file nodes-6379.conf
cluster-node-timeout 15000
数据分片原理:
Java 客户端接入:
java复制@Bean
public RedisConnectionFactory redisConnectionFactory() {
ClusterConfiguration config = new ClusterConfiguration()
.clusterNode("127.0.0.1", 6379)
.clusterNode("127.0.0.1", 6380);
return new JedisConnectionFactory(config);
}
运维经验:
bash复制redis-cli --cluster check 127.0.0.1:6379
双写方案:
java复制// 写入时同步写两个集群
public void set(String key, Object value) {
redisTemplatePrimary.opsForValue().set(key, value);
redisTemplateSecondary.opsForValue().set(key, value);
}
基于 Redis-Shake 的同步:
code复制# 配置文件
source.address = 10.0.0.1:6379
target.address = 10.1.0.1:6379
filter.db.whitelist = 0
注意事项:
必须监控的指标:
Prometheus 监控配置:
code复制scrape_configs:
- job_name: 'redis'
static_configs:
- targets: ['redis:9121']
连接池耗尽:
java复制GenericObjectPoolConfig config = new GenericObjectPoolConfig();
config.setMaxTotal(100);
config.setMaxIdle(20);
config.setMinIdle(5);
config.setMaxWaitMillis(1000);
主从切换数据丢失:
java复制redisTemplate.execute((RedisCallback<Long>) connection -> {
return connection.commands().waitReplicas(1, 1000);
});
配置参数:
code复制io-threads 4 # 启用4个I/O线程
io-threads-do-reads yes # 开启读线程
适用场景:
注意事项:
服务端配置:
code复制client-tracking on
Java 客户端使用:
java复制// 开启tracking
redisTemplate.execute((RedisCallback<String>) connection -> {
connection.commands().clientTrackingOn();
return null;
});
// 获取invalidate消息
MessageListener listener = (message, pattern) -> {
System.out.println("Key invalidated: " + message);
};
redisTemplate.getConnectionFactory().getConnection().subscribe(listener, "__redis__:invalidate");
适用场景:
密码认证配置:
code复制requirepass yourStrongPassword
ACL 权限控制(6.0+):
code复制acl setuser developer on >password ~object:* +@all -@dangerous
Java 客户端配置:
java复制@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration("localhost", 6379);
config.setPassword("yourStrongPassword");
return new JedisConnectionFactory(config);
}
绑定内网IP:
code复制bind 10.0.0.1
禁用危险命令:
code复制rename-command FLUSHDB ""
rename-command CONFIG ""
TLS 加密传输:
code复制tls-port 6379
tls-cert-file redis.crt
tls-key-file redis.key
配置示例:
java复制@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues();
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}
使用方法:
java复制@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
@CacheEvict(value = "users", key = "#user.id")
public void updateUser(User user) {
userRepository.save(user);
}
分布式锁:
java复制RLock lock = redisson.getLock("orderLock");
try {
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
// 业务逻辑
}
} finally {
lock.unlock();
}
分布式集合:
java复制RMap<String, User> userMap = redisson.getMap("users");
userMap.put("1001", new User("张三"));
RList<User> userList = redisson.getList("userList");
userList.add(new User("李四"));
String:
Hash:
List:
RDB 优缺点:
AOF 重写过程:
数据迁移过程:
脑裂问题处理:
特点:
示例:
lua复制#!js api_version=1.0 name=lib
redis.registerFunction('hello', function() {
return 'Hello World';
});
改进点:
经过多年 Redis 实战,我总结了以下黄金法则:
数据分类原则:
容量规划原则:
性能优化原则:
高可用原则:
监控告警原则:
最后记住:Redis 是工具而不是银弹,合理的设计比盲目的性能优化更重要。在我参与过的一个电商项目中,通过简单的数据结构调整(用 Hash 代替 String 存储用户信息),内存使用减少了 40%,这比任何参数调优都有效。