Redis 的 Java 客户端
Redis 的 Java 客户端
客户端选择
Jedis: 以Redis命令作为方法名称、学习成本低,简单实用,但是 Jedus实例是线程不安全的,多线程环境下需要基于连接池来实现 Lettuce是基于 Netty实现的,支持同步,异步和响应式编程方式,并且是线程安全的。支持 Redis的哨兵模式,集群模式和管道模式
Jedis
-
引入依赖
1
2
3
4
5
6<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.2.3</version>
</dependency> -
建立连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51package org.example;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import redis.clients.jedis.Jedis;
public class AppTest {
private Jedis jedis;
public void setUp() {
// 建立连接
jedis = new Jedis("192.168.22.4", 6379);
// 设置密码
jedis.auth("redis6.4");
// 选择数据库
jedis.select(0);
}
// 测试 String 数据类型
void testString(){
// 插入数据 方法名称就是 redis 命令名称,非常简单
String result = jedis.set("name","李四");
System.out.println("result = " + result);
// 获取数据
String name = jedis.get("name");
System.out.println("name = " + name);
}
// 测试 Hash
void testHash() {
jedis.hset("srb:core:code", "17732906439", "97hf");
jedis.hset("srb:core:code", "17732906429", "5fp2");
Map<String, String> stringMap = jedis.hgetAll("srb:core:code");
System.out.println("stringMap = " + stringMap);
}
void tearDown() {
// 释放资源
if (jedis != null) {
jedis.close();
}
}
} -
基础使用
1
2
3
4
5
6
7
8
9
10/****************************************** 单元测试 **********************************************/
void testHset() {
Map<String, String> map = new HashMap<String, String>();
map.put("name","jack");
map.put("age","31");
map.put("sex","man");
jedis.hmset("aa:bb",map);
}
/************************************************************************************************/查看 
Jedis 连接池
-
Jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,因此我们推荐大家使用 Jedis连接池代替 Jedis的直连方式 -
创建工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29package org.example;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class JedisConnectionFactory {
private static final JedisPool jedisPool;
static {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 最大连接
jedisPoolConfig.setMaxTotal(8);
// 最大空闲连接
jedisPoolConfig.setMaxIdle(8);
// 最小空闲连接
jedisPoolConfig.setMinIdle(0);
// 设置最长等待时间 ms
jedisPoolConfig.setMaxWaitMillis(200);
jedisPool = new JedisPool(jedisPoolConfig, "192.168.22.4", 6379, 1000, "redis6.4");
}
// 获取 Jedis 对象
public static Jedis getJedis() {
return jedisPool.getResource();
}
} -
使用
1
2jedis = JedisConnectionFactory.getJedis();
// 后续不变
SpringDataRedis
-
是
Spring中数据操作的模块,包含对各种数据库的集成,其中对 Redis的集成模块就叫做 SpringDataRedis -
提供了对不同
Redis客户端的整合 ( Lettuce)和 Jedis -
提供了
RedisTemplate统一 API来操作 Redis -
支持
Redis的发布订阅模型 -
支持
Redis哨兵和 Redis集群 -
支持基于
Lettuce的响应式编程 -
支持基于
JDK,JSON,字符串,Spring 对象的数据序列化以及反序列化 -
支持基于
Redis的 JDKCollection实现 -
SpringDataRedis中提供了 RedisTemplate工具类,其中封装了各种对 Redis的操作。并且将不同数据类型的操作 API封装到了不同的类型中 API返回值类型 说明 redisTemplate.opsForValue()ValueOperations操作 String类型数据 redisTemplate.opsForHash()HashOperations操作 Hash类型数据 redisTemplate.opsForList()ListOperations操作 List类型数据 redisTemplate.opsForSet()SetOperations操作 Set类型数据 redisTemplate.opsForZSet()ZSetOperations操作 SortedSet类型数据 redisTemplate通用的命令 -
使用步骤
-
引入依赖
1
2
3
4
5
6
7
8
9
10<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> -
配置
1
2
3
4
5
6
7
8
9
10
11spring:
redis:
host: 192.168.22.4
port: 6379
password: redis6.4
lettuce:
pool:
max-active: 8 # 最大连接
max-idle: 8 # 最大空闲连接
min-idle: 0 # 最小空闲连接
max-wait: 100 # 连接等待时间 -
注入
redisTemplate1
2
3
private RedisTemplate redisTemplate; -
编写测试
1
2
3
4
5
6
7
8
9
void contextLoads() {
// 插入一条数据
redisTemplate.opsForValue().set("springData", "saveValue");
// 读取一条 String 类型数据
Object name = redisTemplate.opsForValue().get("springData");
System.out.println("name = " + name);
} -
序列化配置
1
2
3
4
5<!-- 没有引入 springmvc 时添加 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 简单配置
public class RedisConfig {
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
// 创建 RedisTemplate 对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 设置连接工厂
template.setConnectionFactory(connectionFactory);
// 创建 JSON 序列化工具
GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
// 设置 key 的序列化
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// 设置 value 的序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// 详细配置
public class RedisConfig {
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory redisConnFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置连接池工厂
redisTemplate.setConnectionFactory(redisConnFactory);
// 1. 解决 key 的序列化方式
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
// 2. 解决 value 的序列化方式
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
// 序列化时将类的数据类型存入 json,以便反序列化的时候转换成正确的类型
ObjectMapper objectMapper = new ObjectMapper();
// 将当前对象的数据类型也存入序列化的结果字符串中
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
// 解决jackson2 无法反序列化 LocalDateTime 的问题
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.registerModule(new JavaTimeModule());
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
}objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
-
出现时可以进行自动反序列化,
但是占用内存 -
获取对比
-
无字节码存储
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class DemoRedisBootApplicationTests {
private RedisTemplate redisTemplate;
void contextLoads() {
// redisTemplate.opsForValue().set("userinfo", new User("coder-itl2", 18, LocalDate.now()));
String jsonString = (String) redisTemplate.opsForValue().get("userinfo");
// {username=coder-itl2, age=18, birthday=2023-01-27}
System.out.println(jsonString);
}
} -
有字节码存储
1
2
3
4
5
6
7// 修改 RedisConfig 的配置
void contextLoads() {
User userinfo = (User) redisTemplate.opsForValue().get("userinfo");
// User(username=coder-itl2, age=18, birthday=2023-01-27)
System.out.println(userinfo);
}强转类型说明 
在获取结果时,
出现强转, 这个选择时是因为了解返回的结构, 所以确定了强转的类型, 但当存储时, 字节码不存在时, 反序列化时将会出现上述错误
-
-
-
-
存储一个对象
1
2
3
4
5
6
7
8
public class User implements Serializable {
private String username;
private Integer age;
}1
2
3
4
5
6
void testSaveUser() {
redisTemplate.opsForValue().set("core:user", new User("coder-itl", 19));
Object user = redisTemplate.opsForValue().get("core:user");
System.out.println("user = " + user);
}对象的序列化 
StringRedisTemplate(推荐)
-
问题
额外存储键,占用存储 
为了在反序列化时知道对象的类型,
JSON序列化器会将类的 class类型写入 json结果中,存入 Redis,会带来额外的内存开销 -
解决方案
为了节省内存空间,我们并不会使用
JSON序列化器来处理 Value,而是统一使用String序列化器,要求只能存储在 String类型的 key和 value.当我们需要存储 Java对象时,手动完成对象的序列化和反序列化 过程 
Spring默认提供了一个 StringRedisTemplate类,他的 key和 value的序列化方式默认都是 String。省去了我们自定义RedisTemplate的过程 1
2
3
4
5
6
7
8
9
10
11
private StringRedisTemplate stringRedisTemplate;
void testStringTemplate() {
// 存储
stringRedisTemplate.opsForValue().set("strKey","数据");
// 读取
String s = stringRedisTemplate.opsForValue().get("strKey");
System.out.println("s = " + s);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 序列化工具的一种
private static final ObjectMapper mapper = new ObjectMapper();
void testStringTemplate() throws JsonProcessingException {
// 准备对象
User user = new User("coder-itl", 18);
// 手动序列化
String json = mapper.writeValueAsString(user);
// 存储
stringRedisTemplate.opsForValue().set("user", json);
// 读取
String val = stringRedisTemplate.opsForValue().get("user");
// 反序列化
User valUser = mapper.readValue(val, User.class);
System.out.println("valUser = " + valUser);
}节省存储空间 
-
Hash测试 1
2
3
4
5
6
7
8
9
void testHash() {
// 存储值
stringRedisTemplate.opsForHash().put("srb:core:code","phone","jh97g");
stringRedisTemplate.opsForHash().put("srb:core:code","time","5");
// 取值
Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries("srb:core:code");
System.out.println("entries = " + entries);
}
短线验证码
-
基于
Session的短信登录 -
发送短信流程
发送短信验证码流程 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// service 实现
public Result sendCode(String phone, HttpSession session) {
// 1. 校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
// 2. 如果不符合 返回错误信息
return Result.fail("手机号格式错误!");
}
// 3. 符合 生成验证码
String code = RandomUtil.randomNumbers(6);
// 4. 保存验证码到 session
session.setAttribute("code", code);
// 5. 第三方平台发送短信
log.debug("发送短信验证码成功,验证码: {}", code);
// 返回结果
return Result.ok();
} -
注册、登录
短信验证码 注册、登录 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41// 登录、注册
(如果用户是新用户直接注册该用户)
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1. 校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
// 2. 如果不符合 返回错误信息
return Result.fail("手机号格式错误");
}
// 3. 校验验证码
Object cacheCode = session.getAttribute("code");
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.toString().equals(code)) {
// 4. 不一致 报错
return Result.fail("验证码错误");
}
// 5. 一致 根据手机号查询用户
User user = query().eq("phone", phone).one();
// 6. 判断用户是否存在
if (user == null) {
// 7. 不存在 创建新用户并保存
user = createUserWithPhone(phone);
}
// 8. 保存用户信息到 session 中 user 可以定义存储粒度(隐私信息进行忽略)
// session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
session.setAttribute("user", user);
return Result.ok();
}
private User createUserWithPhone(String phone) {
// 创建用户
User user = new User();
user.setPhone(phone);
// 生成随机名称
user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
// 保存用户
save(user);
return user;
} -
校验
校验登录状态 拦截器使用 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// 创建拦截器
public class LoginInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 获取 session
HttpSession session = request.getSession();
// 2. 获取 session 中的用户
Object user = session.getAttribute("user");
// 3. 判断用户是否存在
if (user == null) {
// 4. 不存在 拦截 返回 401 状态码
response.setStatus(401);
return false;
}
// 5. 存在 保存用户信息到 ThreadLocal
UserHolder.saveUser((UserDTO) user);
// 6. 放行
return true;
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户 避免内存泄露
UserHolder.removeUser();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 注册 使拦截器生效
public class MvcConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
// 添加登录拦截器
registry.addInterceptor(new LoginInterceptor())
// 排除不需要拦截的路径
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
);
}
}
Redis-代替 session
-
session共享出现的问题 多台
Tomcat并不共享 session存储空间,当请求切换到不同 tomcat服务时导致数据丢失的问题 -
Redis-代替 session 实现
Redis-代替 session 实现 
1
2// 修改 session 的存储为 redis 存储 LOGIN_CODE_KEY(代替魔法值)
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL,TimeUnit.MINUTES);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1.校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3.从 redis 获取验证码并校验
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.equals(code)) {
// 不一致,报错
return Result.fail("验证码错误");
}
// 4.一致,根据手机号查询用户 select * from tb_user where phone = ?
User user = query().eq("phone", phone).one();
// 5.判断用户是否存在
if (user == null) {
// 6.不存在,创建新用户并保存
user = createUserWithPhone(phone);
}
// 7.保存用户信息到 redis 中
// 7.1.随机生成 token,作为登录令牌
String token = UUID.randomUUID().toString(true);
// 7.2.将 User 对象转为 HashMap 存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
// 7.3.存储
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
// 7.4.设置 token 有效期
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8.返回 token
return Result.ok(token);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56package com.hmdp.config;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.utils.UserHolder;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.获取请求头中的 token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
return true;
}
// 2.基于 TOKEN 获取 redis 中的用户
String key = LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
// 3.判断用户是否存在
if (userMap.isEmpty()) {
return true;
}
// 5.将查询到的 hash 数据转为 UserDTO
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 6.存在,保存用户信息到 ThreadLocal
UserHolder.saveUser(userDTO);
// 7.刷新 token 有效期
stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8.放行
return true;
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
UserHolder.removeUser();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// 配置生效 拦截器顺序通过 .order(0);修改,越小越先执行
public class MvcConfig implements WebMvcConfigurer {
private StringRedisTemplate stringRedisTemplate;
public void addInterceptors(InterceptorRegistry registry) {
// 登录拦截器
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
).order(1);
// token 刷新的拦截器
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
}
}1
2
3
4
5
6
public Result me(){
// 获取当前登录的用户并返回
UserDTO user = UserHolder.getUser();
return Result.ok(user);
}
缓存
-
是什么
缓存: 就是数据交换的缓冲区
(称做 Cache),是存贮数据的临时地方,一般读写性能比较高。 -
缓存
添加之前 添加缓存后 

-
根据
id查询商铺缓存的流程 流程分析 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Result queryById(Long id) {
String key = "cache:shop:" + id;
// 1. 从 redis 中查询商铺的缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2. 判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3. 存在 直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 4. 不存在 根据 id 查询数据库
Shop shop = getById(id);
// 5. 不存在 返回错误
if (shop == null) {
return Result.fail("商铺不存在");
}
// 6. 存在 写入 redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
// 7. 返回
return Result.ok(shop);
}
缓存更新策略
-
策略
内存淘汰 超时剔除 主动更新 说明 不用自己维护,利用 Redis的内存淘汰机制, 当内存不足时自动淘汰部分数据。下次查询时更新缓存 给缓存数据添加 TTL时间,到期时自动删除缓存。下次查询时更新缓存 编写业务逻辑,在修改数据库的同时,更新缓存 一致性 差 一般 好 维护成本 无 低 高 -
业务场景
- 第一致性需求: 使用内存淘汰机制。例如店铺类型的查询缓存
- 高一致性需求: 主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存
-
主动更新策略
Cache Aside Pattern(胜出选择): 由缓存的调用者,在更新数据库的同时更新缓存Read/Write Throuhg Pattern缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题 Write Behind Caching Pattern: 调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库,保持最终一致。
-
缓存更新策略的最佳实践方案
- 低一致性需求: 使用
Redis自带的内存淘汰机制 - 高一致性需求:主动更新,并以超时剔除作为兜底方案
- 读操作
- 缓存命中则直接返回
- 缓存未命中则查询数据库,并写入缓存,设定超时时间
- 写操作
- 先写数据库,然后再删除缓存
- 要确保数据库与缓存操作的原子性
- 读操作
- 低一致性需求: 使用
给查询商铺的缓存添加超时剔除和主动更新的策略
-
需求: 修改
ShopController中的业务逻辑, 满足下面的需求 -
根据
id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Result queryById(Long id) {
String key = "cache:shop:" + id;
// 1. 从 redis 中查询商铺的缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2. 判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3. 存在 直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 4. 不存在 根据 id 查询数据库
Shop shop = getById(id);
// 5. 不存在 返回错误
if (shop == null) {
return Result.fail("商铺不存在");
}
// 6. 存在 写入 redis 设置超时时间
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), 30L, TimeUnit.MINUTES);
// 7. 返回
return Result.ok(shop);
} -
根据
id修改店铺时,先修改数据库,再删除缓存 1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Result update(Shop shop) {
Long id = shop.getId();
if (id == null) {
return Result.fail("店铺id 不能为空");
}
// 1. 更新数据库
updateById(shop);
// 2. 删除缓存
stringRedisTemplate.delete("cache:shop:" + shop.getId());
return Result.ok();
}
-
缓存穿透
-
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。缓存穿透 
-
解决方案
-
缓存空对象
- 优点: 实现简单,维护方便
- 缺点
- 额外的内存消耗
- 可能造成短期的不一致
-
布隆过滤
布隆过滤流程 
-
优点: 内存占用较少,没有多余
key -
缺点
- 实现复杂
- 存在误判可能
-
-
实现
修改解决缓存穿透:1. 空值写入 2. 判断是否是空值 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31// 右侧流程图实现
public Result queryById(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1. 从 redis 中查询商铺的缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2. 判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3. 存在 直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 判断命中的是否是空值
if (shopJson != null) {
// 返回一个错误信息
return Result.fail("店铺信息不存在!");
}
// 4. 不存在 根据 id 查询数据库
Shop shop = getById(id);
// 5. 不存在 返回错误
if (shop == null) {
// 将空值写入 redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return Result.fail("商铺不存在");
}
// 6. 存在 写入 redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 7. 返回
return Result.ok(shop);
} -
总结
-
缓存穿透产生的原因是什么?
用户请求的数据在缓存中和数据库都不存在,不断发起这样的请求,给数据库带来巨大压力
-
缓存穿透的解决方案有那些?
- 缓存
null值 - 布隆过滤
- 增强
id的复杂度,避免被猜测 id规律 - 做好数据的基础格式校验
- 加强用户权限校验
- 做好热点参数的限流
- 缓存
-
缓存雪崩
-
缓存雪崩是指在同一时段大量的缓存key同时失效或者 Redis服务宕机,导致大量请求到达数据库,带来巨大压力 缓存雪崩: 导致大量请求到达数据库,带来巨大压力 
-
解决方案
- 给不同的
key的 TTL添加随机值 - 利用
Redis集群提高服务的可用性 - 给缓存业务添加降级限流策略
- 给业务添加多级缓存
- 给不同的
缓存击穿
-
缓存击穿也叫热点key问题,就是一个被 高并发访问并且缓存重建业务比较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击 -
解决方案
-
互斥锁
互斥锁 
-
逻辑过期
时序图 
-
-
优缺点
-
互斥锁
- 优点
- 没有额外的内存消耗
- 保证一致性
- 实现简单
- 缺点
- 线程需要等待,性能受影响
- 可能有思索风险
- 优点
-
逻辑过期
- 优点
- 线程无需等待,性能比较好
- 缺点
- 不保证一致性
- 有额外内存消耗
- 实现复杂
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public Result queryById(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1. 从 redis 中查询商铺的缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2. 判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3. 存在 直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 判断命中的是否是空值
if (shopJson != null) {
// 返回一个错误信息
return Result.fail("店铺信息不存在!");
}
// 4. 不存在 根据 id 查询数据库
Shop shop = getById(id);
// 5. 不存在 返回错误
if (shop == null) {
// 将空值写入 redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return Result.fail("商铺不存在");
}
// 6. 存在 写入 redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 7. 返回
return Result.ok(shop);
} - 优点
-
基于互斥锁方式解决缓存击穿问题
-
需求: 修改根据
id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题 流程分析 
-
setnx说明 
-
互斥锁实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/**
* 获取锁
*
* @param key
* @return
*/
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
/**
* 释放锁
*
* @param key
*/
private void unlock(String key) {
stringRedisTemplate.delete(key);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53/**
* 互斥锁
*/
public Shop queryWithMutex(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1. 从 redis 中查询商铺的缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2. 判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3. 存在 直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return null;
}
// 判断命中的是否是空值
if (shopJson != null) {
// 返回一个错误信息
return null;
}
// 5. 实现缓存重建
String lockKey = "lock:shop:" + id;
Shop shop = null;
try {
// 5.1 获取互斥锁
boolean isLock = tryLock(lockKey);
// 5.2 判断是否获取成功
if (!isLock) {
// 5.3 失败 休眠重试
Thread.sleep(50);
return queryWithMutex(id);
}
// 4. 不存在 根据 id 查询数据库
shop = getById(id);
// 5. 不存在 返回错误
if (shop == null) {
// 将空值写入 redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6. 存在 写入 redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 释放互斥锁
unlock(lockKey);
}
return shop;
}1
2
3
4
5
6
7
8// 调用
public Result queryById(Long id) {
// 互斥锁解决缓存击穿
Shop shop = queryWithMutex(id);
// 7. 返回
return Result.ok(shop);
}
基于逻辑过期方式解决缓存击穿问题
-
需求: 修改根据
id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题 流程分析 
1
2
3
4
5
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/**
* 逻辑过期函数封装
*
* @param id
* @param expirSeconds
*/
private void saveShop2Redis(Long id, Long expirSeconds) {
// 1. 查询店铺数据
Shop shop = getById(id);
// 2. 封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expirSeconds));
// 3. 写入 Redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46// 创建线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
// 逻辑过期实现
public Shop queryWithLogicExpire(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1. 从 redis 中查询商铺的缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2. 判断是否存在
if (StrUtil.isBlank(shopJson)) {
return null;
}
// 命中,需要先把 json 反序列化为对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
// 判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
// 未过期 直接返回店铺信息
return shop;
}
// 已过期 需要缓存重建
// 缓存重建
// 获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
// 判断是否获取锁成功
boolean isLock = tryLock(lockKey);
if (isLock) {
// 成功 开启独立线程 实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 重建缓存
this.saveShop2Redis(id, 20L);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 释放锁
unlock(lockKey);
}
});
}
// 返回过期的商铺信息
return shop;
}1
2
3
4
5
6
7
8
9// 锁 与 释放锁
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
private void unlock(String key) {
stringRedisTemplate.delete(key);
}1
2
3
4
5
6
7
8
9
10
public Result queryById(Long id) {
// 逻辑过期实现
Shop shop = queryWithLogicExpire(id);
if (shop == null) {
return Result.fail("店铺不存在");
}
// 7. 返回
return Result.ok(shop);
}
缓存工具封装
-
基于
StringRedisTemplate封装一个缓存工具类,满足下列需求 -
方法一: 将任意
Java对象序列化为 json并存储在 string类型的 key中, 并且可以设置 TTL过期时间 -
方法二: 将任意
Java对象序列化为 json并存储在 string类型的 key中,并且可以设置逻辑过期时间,用于处理缓存击穿的问题 -
方法三: 根据指定的
key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透的问题 -
方法四: 根据指定的
key查询缓存,并反序列化为指定类型, 需要利用 逻辑过期解决缓存击穿问题1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178package com.hmdp.utils;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import static com.hmdp.utils.RedisConstants.CACHE_NULL_TTL;
import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public void set(String key, Object value, Long time, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
// 设置逻辑过期
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
// 写入Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
public <R,ID> R queryWithPassThrough(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
String key = keyPrefix + id;
// 1.从 redis 查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(json)) {
// 3.存在,直接返回
return JSONUtil.toBean(json, type);
}
// 判断命中的是否是空值
if (json != null) {
// 返回一个错误信息
return null;
}
// 4.不存在,根据 id 查询数据库
R r = dbFallback.apply(id);
// 5.不存在,返回错误
if (r == null) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入 redis
this.set(key, r, time, unit);
return r;
}
public <R, ID> R queryWithLogicalExpire(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1.从 redis 查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isBlank(json)) {
// 3.存在,直接返回
return null;
}
// 4.命中,需要先把 json 反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
// 5.判断是否过期
if(expireTime.isAfter(LocalDateTime.now())) {
// 5.1.未过期,直接返回店铺信息
return r;
}
// 5.2.已过期,需要缓存重建
// 6.缓存重建
// 6.1.获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 6.2.判断是否获取锁成功
if (isLock){
// 6.3.成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 查询数据库
R newR = dbFallback.apply(id);
// 重建缓存
this.setWithLogicalExpire(key, newR, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
// 释放锁
unlock(lockKey);
}
});
}
// 6.4.返回过期的商铺信息
return r;
}
public <R, ID> R queryWithMutex(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1.从 redis 查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.存在,直接返回
return JSONUtil.toBean(shopJson, type);
}
// 判断命中的是否是空值
if (shopJson != null) {
// 返回一个错误信息
return null;
}
// 4.实现缓存重建
// 4.1.获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
R r = null;
try {
boolean isLock = tryLock(lockKey);
// 4.2.判断是否获取成功
if (!isLock) {
// 4.3.获取锁失败,休眠并重试
Thread.sleep(50);
return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
}
// 4.4.获取锁成功,根据 id 查询数据库
r = dbFallback.apply(id);
// 5.不存在,返回错误
if (r == null) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入 redis
this.set(key, r, time, unit);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
// 7.释放锁
unlock(lockKey);
}
// 8.返回
return r;
}
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
}
-