为什么你需要Redis?
别被那些花里胡哨的理论吓到,换个角度看Redis就是一个跑在内存里的数据库,读写速度飞起,拿来当缓存再合适不过。你问我为什么要用它?举个例子:你家网站首页有个热门排行榜,每次用户访问都要去MySQL里查半天,并发一上来直接卡成PPT。这时候Redis往中间一插,把结果缓存起来,下次直接内存命中,响应时间从几百毫秒降到几微秒,用户体验瞬间起飞。
当然,Redis不光是缓存,还能做分布式锁、消息队列、计数器等等,但咱今天的目标就是——让你能上手就用,不翻车。
前置准备
安装Redis
别怕,安装这事我踩过坑。Windows用户直接去 [Redis官网](https://redis.io/download) 下载Windows版本或者用WSL。Mac用户 brew install redis 搞定。Linux用户 apt install redis-server 或 yum install redis。
装完后启动:
redis-server
如果看到那个经典的ASCII艺术字图,恭喜你,跑起来了。默认端口6379,别乱改。
客户端工具
推荐安装 redis-cli 和 redis-desktop-manager(跨平台可视化工具)。当然你也可以用IDE插件。
基本操作:从Hello World开始
打开终端,进入redis-cli:
redis-cli
试试最基础的四件套:
SET name "Redis实战"
GET name
DEL name
EXISTS name
输出分别会是:OK、"Redis实战"、1、0。完事。
但光会这个不够,缓存要设置过期时间,不然内存迟早炸。
SET cache_key "some_value" EX 60 # 60秒后自动删除
TTL cache_key # 查看剩余秒数,-1表示永不过期,-2表示已过期
关键点:别傻傻地不设过期时间,除非你的缓存永远不会失效。我见过生产环境里有人用SET不加任何参数,结果Redis内存涨到90%,然后OOM掉,运维背锅。
数据结构实战
Redis有五种基本数据结构,但最常用的是String和Hash。
String:最简单的缓存
适合存单个值的场景,比如用户Token、验证码。
SET token:123456 "abc123" EX 7200
GET token:123456
Hash:存对象
比如用户信息:
HSET user:1001 name "张三" age 28 city "北京"
HGET user:1001 name
HGETALL user:1001
Hash的好处是你不用序列化整个对象,修改单个字段时开销小。
List / Set / Sorted Set
这些在缓存里用得少,但排行榜得用Sorted Set:
ZADD ranking 100 "用户A" 200 "用户B"
ZREVRANGE ranking 0 2 WITHSCORES # 前两名
实战:Java + Redis 缓存用户数据
好了实操来了。我们要写一个完整的Spring Boot应用,用Redis缓存用户查询结果。
项目依赖(Maven)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
</dependencies>
application.yml 配置
spring:
redis:
host: localhost
port: 6379
password: # 没设就空着
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
核心代码:UserService
创建一个User实体:
import java.io.Serializable;
public class User implements Serializable {
private Long id;
private String name;
private Integer age;
// 无参构造、全参构造、getter/setter 略(IDE生成)
// 注意:必须实现Serializable,否则Redis序列化会报错
}
Service层:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class UserService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 模拟数据库查询
public User getUserFromDB(Long id) {
// 实际项目里这里是调用Mapper
System.out.println("查询数据库,id=" + id);
return new User(id, "张三", 28);
}
public User getUser(Long id) {
String key = "user:" + id;
// 先查缓存
User cached = (User) redisTemplate.opsForValue().get(key);
if (cached != null) {
System.out.println("命中缓存");
return cached;
}
// 缓存没命中,查数据库
User dbUser = getUserFromDB(id);
// 存入缓存,过期时间5分钟
redisTemplate.opsForValue().set(key, dbUser, 5, TimeUnit.MINUTES);
return dbUser;
}
public void updateUser(User user) {
// 更新数据库(模拟)
System.out.println("更新数据库,id=" + user.getId());
// 删除缓存,保持一致性
String key = "user:" + user.getId();
redisTemplate.delete(key);
}
}
配置Redis序列化
Spring Boot默认使用JDK序列化,存进Redis的是乱码,而且不可读。建议改成JSON:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// key用字符串序列化
template.setKeySerializer(new StringRedisSerializer());
// value用JSON序列化,支持对象
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// Hash的key也设为字符串
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
控制器调用
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUser(id);
}
@PostMapping
public String updateUser(@RequestBody User user) {
userService.updateUser(user);
return "ok";
}
}
启动应用,访问 http://localhost:8080/user/1,第一次会查数据库,第二次直接走缓存,观察控制台输出你就能看到效果。
常见错误及解决方法
错误1:`java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class xxx.User`
场景:从Redis读取缓存时,反序列化回来的Object实际上是LinkedHashMap,无法强转为User。
原因:默认的Jackson序列化在存储时丢失了类型信息。
解决:在RedisConfig中使用 GenericJackson2JsonRedisSerializer 时,需要在RedisTemplate的ValueSerializer上启用类型信息。或者更简单的方式:不要在 opsForValue().get() 后强转,而是用JSON工具手动转换。但推荐在序列化器上加上 ObjectMapper 启用默认类型:
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
// 或者在ObjectMapper中设置
ObjectMapper om = new ObjectMapper();
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(om);
错误2:`Redis connection refused` 或 `connect: Cannot assign requested address`
场景:应用启动就报连不上Redis,或者高并发下偶尔连不上。
原因:最常见的是Redis服务没启动,或者端口被防火墙拦截。另一个是连接池耗尽,Linux默认临时端口不够。
解决:
- 先
redis-cli -h localhost -p 6379 ping 看服务器通不通,不通就启动 redis-server。
- 连接池配置里
max-active 不要太大,一般8-16足够,同时调整Linux的 net.ipv4.ip_local_port_range 到 1024 65535。
- 代码里用完连接及时释放,Spring Data Redis已经帮我们做了,但如果你直接操作Jedis记得
close()。
实战经验心得
缓存穿透、击穿、雪崩
这三个词面试常考,实战也经常遇到。
- 缓存穿透:查询一个根本不存在的key,每次都会打到数据库。解决:缓存空对象(即使查不到也存一个key,过期时间短一点),或者布隆过滤器。
- 缓存击穿:热点key过期瞬间,大量请求同时打到数据库。解决:加互斥锁(分布式锁),让一个线程去查库并重建缓存,其他等待。或者设置热点key永不过期(后台异步更新)。
- 缓存雪崩:大量key同时过期,导致数据库压力暴增。解决:过期时间加随机数,比如
5分钟 + random(0,60秒)。
实战经验实例:有次我负责的活动页面,上线后Redis里所有的活动配置key都设了同一时间过期,结果半夜零点所有缓存集体失效,数据库被打挂了。从那以后我养成了给过期时间加随机抖动的习惯。
Big Key问题
千万别把一个List塞几万条数据当缓存,读写时Redis会单线程阻塞。经验之谈:我用Hash存用户购物车,有个用户商品加了2000件,结果每次获取购物车都耗时几百毫秒。后来拆成多个小key。
序列化坑
前面说了,Redis存储对象必须序列化。如果你的对象有嵌套对象或者循环引用,Jackson序列化会报错。建议:只缓存简单数据结构,或者用DTO降级。
最佳实践建议
- key命名规范:业务名:对象类型:ID,例如
user:profile:123,不要用空格、特殊字符。
- 过期时间必不可少:除非你明确知道数据永不失效(比如分布式锁),否则全都要设过期时间。
- 避免大key:单个字符串超过10KB就要警惕,Hash或List超过1000个元素考虑拆分。
- 使用连接池:生产环境一定要配连接池,并监控连接数。
- 定期清理:如果业务场景允许,给Redis配置
maxmemory-policy allkeys-lru 作为兜底淘汰策略。
- 监控与告警:用
info memory 命令查看内存使用,配合Prometheus + Grafana监控。
- 缓存粒度:不推荐整个大对象一股脑往Redis里塞。例如用户信息只需要显示昵称和头像,那就只缓存这两个字段,用Hash存。
总结(其实没有总结)
Redis入门不难,难的是面对复杂业务时做出合理的设计。很多新手一上来就想着用Redis解决所有性能问题,结果反而搞出了更复杂的缓存一致性问题。我的建议是:先跑通上面的Java例子,然后去你自己的项目里找一个简单的查询接口,加上缓存试试。遇到问题别慌,大概率是我上面列的那几个常见错误之一。 等你真正踩过一两个坑,Redis就算入门了。
好了,该说的都说完了。打开IDEA,开始写代码吧。
常见错误总结:缓存雪崩(大量key同时过期)、缓存穿透(查询不存在的数据)、缓存击穿(热点key突然失效)。这三个问题几乎涵盖80%的缓存故障,建议每个人都理解并掌握解决方案。