Redis 是 Java 后端开发中最高频使用的中间件之一,缓存、分布式锁、Session 共享是三大最常用的场景。这一篇从配置到实战,一步到位。
一、基础配置
1. 引入依赖
<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>
2. application.yml
spring:
redis:
host: localhost
port: 6379
password: # 如果设置了密码
database: 0 # 默认 0,共 0-15 个库
timeout: 3000ms
lettuce:
pool:
max-active: 16 # 连接池最大连接数
max-idle: 8 # 最大空闲连接
min-idle: 4 # 最小空闲连接
max-wait: 3000ms # 获取连接最大等待时间
3. RedisTemplate 配置
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// JSON 序列化
Jackson2JsonRedisSerializer<Object> jacksonSer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LazyValidatorFactory.class, ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSer.setObjectMapper(om);
// String 序列化(key 用)
StringRedisSerializer stringSer = new StringRedisSerializer();
// key 和 hashKey 用 String 序列化
template.setKeySerializer(stringSer);
template.setHashKeySerializer(stringSer);
// value 和 hashValue 用 JSON 序列化
template.setValueSerializer(jacksonSer);
template.setHashValueSerializer(jacksonSer);
template.afterPropertiesSet();
return template;
}
/**
* 简单操作时,直接用 StringRedisTemplate
* 省去序列化配置的麻烦
*/
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
return new StringRedisTemplate(factory);
}
}
注意: RedisTemplate 默认用 JDK 序列化,存进 Redis 是乱码,必须改成 JSON 序列化。
二、场景一:缓存
1. 缓存查询——最简单的写法
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String CACHE_KEY = "cache:user:";
@Override
public User getUserById(Long id) {
// 1. 先从缓存查
String key = CACHE_KEY + id;
User user = (User) redisTemplate.opsForValue().get(key);
if (user != null) {
System.out.println("缓存命中: " + key);
return user;
}
// 2. 缓存未命中,查数据库
user = this.getById(id);
if (user != null) {
// 3. 写入缓存,设置过期时间 30 分钟
redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
}
return user;
}
@Override
public boolean updateUser(User user) {
boolean success = this.updateById(user);
if (success) {
// 更新后删除缓存,下次查询重新加载
redisTemplate.delete(CACHE_KEY + user.getId());
}
return success;
}
}
缓存策略: 更新时直接删缓存,而不是更新缓存。下次查询时再重新加载,这叫 Cache-Aside 模式。
2. Spring Cache 注解版(更省事)
@CacheConfig(cacheNames = "user")
@Service
public class UserCacheService {
@Cacheable(key = "#id", unless = "#result == null")
public User getById(Long id) {
// 方法内有缓存时不会执行
// 等价于上面手动查缓存→查数据库→写缓存
return userMapper.selectById(id);
}
@CachePut(key = "#user.id")
public User update(User user) {
userMapper.updateById(user);
return user;
// @CachePut 每次都会执行方法,并把返回值更新到缓存
}
@CacheEvict(key = "#id")
public void delete(Long id) {
userMapper.deleteById(id);
// @CacheEvict 执行后删除缓存
}
}
开启注解支持:
@SpringBootApplication
@EnableCaching // 开启缓存注解
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
三种注解总结:
| 注解 | 作用 | 使用场景 |
|---|---|---|
@Cacheable | 先查缓存,有则返回,无则执行方法并写入缓存 | 查询接口 |
@CachePut | 执行方法,并将结果更新到缓存 | 更新接口 |
@CacheEvict | 执行方法,删除缓存 | 删除接口 |
注意: @Cacheable 的 key 不要用 SpEL 拼接太复杂的表达式,出错了缓存会静默失效。
3. 缓存穿透/击穿/雪崩
// 缓存穿透:查一个不存在的 key,每次都穿透到数据库
// 解决方案:缓存空值(设置短过期时间)
User user = (User) redisTemplate.opsForValue().get(key);
if (user != null) {
return user;
}
user = this.getById(id);
if (user == null) {
// 缓存空值,防止穿透
redisTemplate.opsForValue().set(key, new User(), 60, TimeUnit.SECONDS);
} else {
redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
}
return user;
// 缓存雪崩:大量 key 同时过期
// 解决方案:过期时间加随机值
long baseExpire = 30; // 基础 30 分钟
long randomExpire = ThreadLocalRandom.current().nextLong(5, 15); // 随机加 5~15 分钟
redisTemplate.opsForValue().set(key, user, baseExpire + randomExpire, TimeUnit.MINUTES);
三、场景二:分布式锁
单体项目用 synchronized,分布式系统必须用 Redis 分布式锁。
1. 最简单的分布式锁
private static final String LOCK_KEY = "lock:order:";
public boolean createOrder(Long productId, Long userId) {
String lockKey = LOCK_KEY + userId;
// SETNX:key 不存在才设置成功,防止重复提交
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(locked)) {
return false; // 别人正在操作,请稍后
}
try {
// 业务逻辑:扣库存、生成订单
return doCreateOrder(productId, userId);
} finally {
// 释放锁(必须放在 finally 中)
// 注意:只释放自己的锁,别把别人的锁误删了
String value = (String) redisTemplate.opsForValue().get(lockKey);
if ("1".equals(value)) {
redisTemplate.delete(lockKey);
}
}
}
2. 更完善的锁工具类
@Component
public class RedisLock {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 加锁
* @param key 锁的 key
* @param value 锁的值(用于释放时校验)
* @param expire 过期时间(秒)
*/
public boolean lock(String key, String value, long expire) {
return Boolean.TRUE.equals(
redisTemplate.opsForValue().setIfAbsent(key, value, expire, TimeUnit.SECONDS)
);
}
/**
* 解锁(使用 Lua 脚本保证原子性)
*/
public boolean unlock(String key, String value) {
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(key),
value
);
return Long.valueOf(1).equals(result);
}
}
// 使用
RedisLock lock = new RedisLock();
String lockValue = UUID.randomUUID().toString();
try {
boolean ok = lock.lock("lock:pay:" + orderNo, lockValue, 30);
if (!ok) {
return "操作太频繁,请稍后重试";
}
// 执行业务...
} finally {
lock.unlock("lock:pay:" + orderNo, lockValue);
}
一定要用 Lua 脚本释放锁,检查自己的锁 + 删除是两步操作,非原子操作可能误删别人的锁。
四、场景三:Session 共享
多实例部署时,用户登录到 A 机器,下次请求被转发到 B 机器,Session 就丢了。用 Redis 存 Session 就能解决。
1. 引入依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
2. 配置
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800) // Session 30 分钟过期
public class SessionConfig {
}
就这两步,Spring Session 自动把 Session 存储从内存换成 Redis。不需要改任何业务代码。
# application.yml 加一个配置项(可选)
server:
servlet:
session:
timeout: 1800 # Session 超时时间
验证:
@RestController
public class SessionController {
@PostMapping("/login")
public String login(HttpSession session, @RequestParam String username) {
session.setAttribute("user", username); // 存到 Redis
return "登录成功";
}
@GetMapping("/currentUser")
public String currentUser(HttpSession session) {
return (String) session.getAttribute("user"); // 从 Redis 取
}
}
不管部署多少个实例,Session 数据都从 Redis 读取,用户登录状态在多台机器间共享。
五、常用数据类型操作速查
// String(字符串)
redisTemplate.opsForValue().set(key, value);
redisTemplate.opsForValue().get(key);
redisTemplate.opsForValue().increment(key); // 自增(原子操作)
redisTemplate.opsForValue().decrement(key); // 自减
// Hash(哈希)
redisTemplate.opsForHash().put(key, field, value);
redisTemplate.opsForHash().get(key, field);
redisTemplate.opsForHash().entries(key); // 获取所有 field-value
// List(列表)
redisTemplate.opsForList().leftPush(key, value); // 左入队
redisTemplate.opsForList().rightPop(key); // 右出队
redisTemplate.opsForList().range(key, 0, -1); // 获取全部
// Set(集合,无重复)
redisTemplate.opsForSet().add(key, values);
redisTemplate.opsForSet().members(key);
redisTemplate.opsForSet().isMember(key, value); // 判断是否存在
// ZSet(有序集合)
redisTemplate.opsForZSet().add(key, value, score);
redisTemplate.opsForZSet().range(key, 0, -1); // 按分数升序
// 通用操作
redisTemplate.expire(key, timeout, TimeUnit.SECONDS); // 设置过期时间
redisTemplate.delete(key); // 删除 key
redisTemplate.hasKey(key); // 判断是否存在
六、实际开发注意事项
- RedisTemplate 的 key 统一加前缀:比如
user:123、order:2024001,方便管理 - 所有 key 必须设置过期时间:除非是计数器等极少数场景,否则 Redis 内存会爆
- 大 key 问题:不要往 Redis 里存超过 10MB 的数据(比如用户头像 base64)
- Redis 不是万能的:Redis 是缓存数据库,不是主数据库,重要数据必须落 MySQL
💡 觉得有用的话,点赞 + 关注【张老师技术栈】吧!每周更新 Java/Python/爬虫 实战干货,不让你白来。

1180

被折叠的 条评论
为什么被折叠?



