redis 是高性能基于键值对的内存数据库,专门用来存数据,做缓存,做分布式锁,做消息队列,支持多种数据类型,如 String, List, Map, Set, ZSet有序集合
解决序列化问题
将对象变成二进制字节/字符串的方式叫做 "序列化"
首先准备一个类用作解决 redis 的序列化问题
java
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
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;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Configuration
public class RedisConfig {
// 定义时间格式化器(统一格式)
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 1. 配置 Jackson 序列化器(处理 LocalDateTime)
ObjectMapper objectMapper = new ObjectMapper();
// 注册 Java 8 时间模块
JavaTimeModule javaTimeModule = new JavaTimeModule();
// 自定义 LocalDateTime 序列化/反序列化格式
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER));
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER));
objectMapper.registerModule(javaTimeModule);
// 2. 序列化器实例
StringRedisSerializer stringSerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(objectMapper);
// 3. 配置序列化器
template.setKeySerializer(stringSerializer); // key 用字符串序列化
template.setValueSerializer(jsonSerializer); // value 用 JSON 序列化
template.setHashKeySerializer(stringSerializer); // Hash key 序列化
template.setHashValueSerializer(jsonSerializer); // Hash value 序列化
template.afterPropertiesSet();
return template;
}
}
然后准备一个 redis 工具类(这是一个针对于String的工具类)
java
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Redis 通用操作工具类(基于 StringRedisTemplate)
*/
@Component // 交给 Spring 容器管理
public class RedisUtils {
// 注入 StringRedisTemplate(Spring Boot 自动配置,无需手动创建)
@Resource
private StringRedisTemplate stringRedisTemplate;
// ====================== 字符串(最常用) ======================
/**
* 存储字符串(无过期时间)
*/
public void set(String key, String value) {
stringRedisTemplate.opsForValue().set(key, value);
}
/**
* 存储字符串(带过期时间)
* @param timeout 过期时间(如 5)
* @param unit 时间单位(如 TimeUnit.MINUTES 分钟)
*/
public void set(String key, String value, long timeout, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, value, timeout, unit);
}
/**
* 获取字符串
*/
public String get(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
// ====================== 哈希(适合存储对象) ======================
/**
* 存储哈希字段
*/
public void hSet(String key, String hashKey, String value) {
stringRedisTemplate.opsForHash().put(key, hashKey, value);
}
/**
* 获取哈希字段
*/
public String hGet(String key, String hashKey) {
return (String) stringRedisTemplate.opsForHash().get(key, hashKey);
}
// ====================== 通用操作 ======================
/**
* 删除指定 key
*/
public Boolean delete(String key) {
return stringRedisTemplate.delete(key);
}
/**
* 判断 key 是否存在
*/
public Boolean hasKey(String key) {
return stringRedisTemplate.hasKey(key);
}
/**
* 设置 key 过期时间
*/
public Boolean expire(String key, long timeout, TimeUnit unit) {
return stringRedisTemplate.expire(key, timeout, unit);
}
/**
* 获取所有匹配的 key(如模糊查询:user:*)
*/
public Set<String> keys(String pattern) {
return stringRedisTemplate.keys(pattern);
}
}
对于List的工具类
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Component
public class RedisListUtils {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// ==================== 基础操作 ====================
/**
* 往 List 尾部添加元素(RPUSH)
* @param key 缓存key
* @param values 要添加的元素(可多个)
* @return 添加后 List 长度
*/
public Long rPush(String key, Object... values) {
return redisTemplate.opsForList().rightPushAll(key, values);
}
/**
* 往 List 头部添加元素(LPUSH)
* @param key 缓存key
* @param values 要添加的元素(可多个)
* @return 添加后 List 长度
*/
public Long lPush(String key, Object... values) {
return redisTemplate.opsForList().leftPushAll(key, values);
}
/**
* 获取 List 全部元素
* @param key 缓存key
* @return 完整 List
*/
public List<Object> lRangeAll(String key) {
// 0 = 第一个元素,-1 = 最后一个元素
return redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 分页获取 List 元素(核心!适合大 List)
* @param key 缓存key
* @param start 起始索引(从0开始)
* @param end 结束索引(如取10条:start=0, end=9)
* @return 分页结果
*/
public List<Object> lRangePage(String key, long start, long end) {
return redisTemplate.opsForList().range(key, start, end);
}
/**
* 获取 List 长度
* @param key 缓存key
* @return List 元素个数
*/
public Long lSize(String key) {
return redisTemplate.opsForList().size(key);
}
/**
* 根据索引获取元素
* @param key 缓存key
* @param index 索引(0=第一个,-1=最后一个)
* @return 对应元素
*/
public Object lIndex(String key, long index) {
return redisTemplate.opsForList().index(key, index);
}
/**
* 删除 List 中的元素
* @param key 缓存key
* @param count 删除次数(count=0:删除所有匹配元素;count>0:从头部删count个;count<0:从尾部删)
* @param value 要删除的元素
* @return 删除的个数
*/
public Long lRemove(String key, long count, Object value) {
return redisTemplate.opsForList().remove(key, count, value);
}
/**
* 设置 List 过期时间(必须!避免内存泄漏)
* @param key 缓存key
* @param timeout 过期时间
* @param unit 时间单位
* @return 是否设置成功
*/
public boolean expire(String key, long timeout, TimeUnit unit) {
return Boolean.TRUE.equals(redisTemplate.expire(key, timeout, unit));
}
/**
* 删除整个 List
* @param key 缓存key
*/
public void delList(String key) {
redisTemplate.delete(key);
}
}
redis在springboot中的有以下几个使用场景
接口数据缓存
对于接口缓存而言,存入 redis 一般先需要一个Key
java
private static String key = "product:info";
然后使用 工具类中的方法
java
@GetMapping("/getAllArticle")
public CommonRespose getAllArticle() {
String cacheKey = article_redis_key + "allArticle";
Long s = redisListUtils.lSize(cacheKey);
if( s != null && s > 0)
{
System.out.println("缓存命中============查询到所有的文章=============");
return CommonRespose.success(redisListUtils.lRangeAll(cacheKey));
}
List<Article> res = articleService.getAllArticle();
if(res.size() > 0)
{
redisListUtils.rPush(cacheKey,res.toArray());
redisListUtils.expire(cacheKey,10, TimeUnit.HOURS);
}
return CommonRespose.success(res);
}
分布式锁
什么是分布式锁?
分布式锁就是,在同一时间内,多台服务器同时竞争同一把锁,确保在同一时间内只有一台服务器执行到关键代码,避免在分布式系统中因竞争同一资源导致数据混乱
以秒杀库存扣减为例:
- 用户点击秒杀
- 服务器尝试获取分布式锁
- 获取成功 → 执行扣库存、创建订单
- 执行完成 → 释放锁
- 获取失败 → 提示 "抢购太火爆"
案例:
先封装redis分布式锁工具类(服务器向运行redis的服务器申请锁,操纵锁)
java
@Component
public class RedisDistributedLockUtils {
@Resource
private StringRedisTemplate stringRedisTemplate;
// 锁的默认过期时间(5秒,防止死锁)
private static final long DEFAULT_EXPIRE_TIME = 5;
// 锁的重试间隔(100毫秒,避免频繁请求Redis)
private static final long RETRY_INTERVAL = 100;
// 最大重试次数(3次,避免无限等待)
private static final int MAX_RETRY = 3;
/**
* 获取分布式锁
* @param lockKey 锁的Key(如:lock:seckill:1001)
* @return 锁的唯一标识(释放锁时需要验证)
*/
public String tryLock(String lockKey) {
return tryLock(lockKey, DEFAULT_EXPIRE_TIME, TimeUnit.SECONDS);
}
/**
* 带过期时间的锁
* @param lockKey 锁Key
* @param expireTime 过期时间
* @param timeUnit 时间单位
* @return 唯一标识(UUID),null表示获取失败
*/
public String tryLock(String lockKey, long expireTime, TimeUnit timeUnit) {
// 生成唯一标识(防止误删别人的锁)
String requestId = UUID.randomUUID().toString();
int retryCount = 0;
// 重试获取锁
while (retryCount < MAX_RETRY) {
// Redis核心命令:SET key value NX EX time(原子操作)
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, expireTime, timeUnit);
if (Boolean.TRUE.equals(success)) {
return requestId; // 获取锁成功,返回唯一标识
}
// 获取失败,重试
retryCount++;
try {
Thread.sleep(RETRY_INTERVAL);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
return null; // 重试多次仍失败
}
/**
* 释放分布式锁(必须验证requestId,防止误删)
* @param lockKey 锁Key
* @param requestId 唯一标识(获取锁时返回的UUID)
* @return 是否释放成功
*/
public boolean releaseLock(String lockKey, String requestId) {
if (requestId == null) {
return false;
}
// 先查锁的当前值,再删除(保证原子性,避免并发误删)
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Long result = stringRedisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
requestId
);
return result != null && result > 0;
}
}
业务层使用分布式锁
java
@Service
public class SeckillDistributedService {
@Resource
private ProductStockService stockService;
@Resource
private RedisDistributedLockUtils lockUtils;
// 锁Key前缀(规范命名)
private static final String LOCK_PREFIX = "lock:seckill:";
public String seckill(Long productId) {
// 1. 拼接锁Key(商品维度,只锁当前商品)
String lockKey = LOCK_PREFIX + productId;
// 2. 获取分布式锁
String requestId = lockUtils.tryLock(lockKey);
if (requestId == null) {
return "商品" + productId + "秒杀太火爆,请稍后再试!";
}
// 3. 核心逻辑:扣库存(加锁成功后执行)
try {
Integer stock = stockService.getStock(productId);
if (stock <= 0) {
return "商品" + productId + "库存不足!";
}
boolean success = stockService.deductStock(productId);
if (success) {
return "商品" + productId + "秒杀成功!";
} else {
return "商品" + productId + "秒杀失败!";
}
} finally {
// 4. 释放锁(必须放finally,无论成功失败都要释放)
lockUtils.releaseLock(lockKey, requestId);
}
}
}
接口限流
限流就是指限制一个ip,用户, 或者接口 在一段时间内的访问次数
核心原理:redis 计数器 + 过期时间
一个请求发出 计数器 + 1
当计数器达到上限,就将接口锁住,不能访问
当过期时间达到在放开接口
首先封装 redis 限流工具类
java
@Component
public class RedisRateLimitUtils {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 接口限流核心方法
* @param keyPrefix 限流Key前缀(如:limit:login:)
* @param identifier 唯一标识(如:用户ID/IP)
* @param limit 时间窗口内的最大请求数(如:5次)
* @param timeout 时间窗口(如:1分钟)
* @param timeUnit 时间单位
* @return true=允许访问,false=拒绝访问
*/
public boolean rateLimit(String keyPrefix, String identifier, int limit, long timeout, TimeUnit timeUnit) {
// 1. 拼接限流Key:前缀 + 唯一标识(如:limit:login:1001)
String limitKey = keyPrefix + identifier;
// 2. Redis计数器自增(原子操作,避免并发问题)
Long count = stringRedisTemplate.opsForValue().increment(limitKey, 1);
// 3. 首次计数时,设置过期时间(只设置一次,避免重复覆盖)
if (count != null && count == 1) {
stringRedisTemplate.expire(limitKey, timeout, timeUnit);
}
// 4. 判断是否超过限流阈值
return count != null && count <= limit;
}
}
并在业务层使用
java
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private RedisRateLimitUtils rateLimitUtils;
// 限流Key前缀(规范命名)
private static final String LOGIN_LIMIT_PREFIX = "limit:login:";
/**
* 用户登录接口(限流:1分钟最多5次)
*/
@PostMapping("/login")
public CommonResponse login(@RequestParam Long userId, @RequestParam String password) {
// 1. 先执行限流判断
boolean allow = rateLimitUtils.rateLimit(
LOGIN_LIMIT_PREFIX, // 限流前缀
userId.toString(), // 唯一标识(用户ID)
5, // 1分钟最多5次
1, // 时间窗口:1分钟
TimeUnit.MINUTES // 时间单位
);
// 2. 超过限流阈值,直接拒绝
if (!allow) {
return CommonResponse.fail("请求过于频繁,请1分钟后再试!");
}
// 3. 未超过阈值,执行登录逻辑
if ("123456".equals(password)) {
return CommonResponse.success("登录成功!");
} else {
return CommonResponse.fail("密码错误!");
}
}
}
用户登录态存储(Session 共享)
Session 是客户端 存储在服务器(内存/redis) 的身份凭证,表示这个用户的登录,等等信息
把用户的登录状态(Session)从 "单台服务器的内存" 搬到 "所有服务器都能访问的 Redis" 里,让多台服务器都能查到同一个用户的登录信息。
什么是消息队列?
消息队列本质是一个异步的操作,一个任务发出,比如说一个电商订单的发布(下单),然后要经过很多步骤按顺序完成,但是消息队列的优势是,下单之后,将任务放在队列中交给其他任务去执行,然后直接返回下单成功即可
为什么 redis 比较适合实现消息队列?
因为 redis 的 List 结构是先进先出的,可重复,有序的数据结构,非常适合用作消息队列,且是阻塞读取,支持多消费者
阻塞读取(Blocking Read) :消费者去队列拿消息时,如果队列里没有消息 ,就 "原地等待"(阻塞),直到队列里有新消息进来,再立刻取走;如果队列里有消息,就直接取走。
支持多消费者 :Redis 的消息队列可以同时让多个消费者(线程 / 服务器) 监听同一个队列,队列里的消息会被均匀分配给不同的消费者处理(一个消息只会被一个消费者拿走),实现 "多人分工干活"。