1、初识Redis
1.1、认识NoSQL
SQL | NOSQL | |
---|---|---|
数据结构 | 结构化(Structured) | 非结构化 |
数据关联 | 关联的(Relational) | 无关联的 |
查询方式 | SQL查询 | 非SQL |
事务特性 | ACID | BASE |
存储方式 | 磁盘 | 内存 |
扩展性 | 垂直 | 水平 |
使用场景 | (1)数据结构固定;(2)相关业务对数据安全性、一致性要求较高 | (1)数据结构不固定;(2)对一致性、安全性有要求不高;(3)对性能要求 |
1.2、认识Redis
Redis诞生于2009年全称是Remote Dictionary Server,远程词典服务器,是一个基于内存的键值型NoSQL数据库。
特征
-
键值(key-value)型,value支持多种不同数据结构,功能丰富
-
单线程,每个命令具备原子性
-
低延迟,速度快(基于内存、IO多路复用、良好的编码)
-
支持数据持久化
-
支持主从集群、分片集群
-
支持多语言客户端
1.3、安装Redis
参考其他md文件
1.4、Redis客户端
安装完成Redis,就可以操作Redis,实现数据的CRUD。这里需要用到Redis客户端,其中包括:
-
命令行客户端
-
图形化桌面客户端
-
编程客户端
1.4.1、Redis命令行客户端
Redis安装完成后就自带了命令行客户端:redis-cli,使用方式如下:
redis-cli [options] [commonds]
其中常见的options有:
-
-h 127.0.0.1
:指定要连接的redis节点的IP地址,默认是127.0.0.1 -
-p 6379
:指定要连接的redis节点的端口,默认是6379 -
-a 123321
:指定redis的访问密码
其中的commonds就是Redis的操作命令,例如:
-
ping
:与redis服务端做心跳测试,服务端正常会返回pong
不指定commond时,会进入redis-cli
的交互控制台:
1.4.2、图形化桌面客户端
。。。
2、Redis命令
2.1、Redis数据结构介绍
Redis是一个key-value的数据库,key一般是String类型,不过value的类型多种多样:
类型 | 内容 |
---|---|
String | hello world |
Hash | {name:“Jack”,age:“21”} |
List | [A -> B -> C -> C] |
Set | {A,B,C} |
SortedSet | {A:1,B:2,C:3} |
GEO | {A:(120.3,30.5)} |
BitMap | 0110110101110101011 |
HyperLog | 0110110101110101011 |
解释:前五种是基本类型,后三种是特殊类型。
2.2、Redis通用命令
通过指令是部分数据类型的,都可以使用的指令,常见的有:
-
SET:是添加一个key
-
KEYS:查看符合模板的所有key,不建议在生产环境设备上使用
-
DEL:删除一个指定的key
-
EXISTS:判断key是否存在
-
EXPIRE:给一个key设置有效期,有效期到期时该key会被自动删除
-
TTL:查看一个KEY的剩余有效期
通过help [command 可以查看一个命令的具体用法,例如
help keys
2.3、String类型
String类型,也就是字符串类型,是Redis中最简单的存储类型,其中value是字符串,不过根据字符串的格式不同,又可以分为3类:
-
String:普通字符串
-
int:整形类型,可以做自增、自减操作
-
float:浮点类型,可以做自增、自减操作
KEY | VALUE |
---|---|
msg | hello world |
num | 10 |
score | 92.5 |
String的常见命令有:
-
SET:添加或者修改已经存在的一个String类型的键值对
-
GET:根据key获取String类型的value
-
MSET:批量添加多个String类型的键值对
-
MGET:根据多个key获取多个String类型的value
-
INCR:让一个整型的key自增1
-
INCRBY:让一个整型的key自增并指定步长,例如:incrby num 2,让num值自增2
-
INCRBYFLOAT:让一个浮点类型的数字自增并指定步长
-
SETNX:添加一个String类型的键值对,前提是这个key不存在,否则不执行
-
SETEX:添加一个String类型的键值对,并且指定有效期
2.4、Key的层级格式
Redis的key允许有多个单词形成层级结构,多个单词之间用':'隔开,格式如下:
项目名:业务名:类型:id
这个格式并非固定,也可以根据自己的需求来删除或添加词条
例如:有个项目为csms,有user和product两种不同类型的数据,可以这样定义key:
-
user相关的key:csms:user:1
-
product相关的key:csms:product:1
如果value是一个Java对象,例如一个User对象,则可以将对象序列化为JSON字符串后存储:
KEY | VALUE |
---|---|
csms:user:1 | {"id": 1, "name": "Jack", "age": 21} |
csms:product:1 | {"id": 1, "name": "小米11", "price": 4399} |
总结
-
String类型的三种格式
-
字符串
-
int
-
float
-
-
Redis的key的格式
-
[项目名] 业务名:类型:id,可能标准不同
-
2.5、Hash类型
Hash类型,也叫散列,其value是一个无序字典,类似于Java中的HashMap结构。
String结构是将对象序列化为JSON字符串后存储,当需要修改对象某个字段时很不方便:
KEY | VALUE |
---|---|
csms:user:1 | {"id": 1, "name": "Jack", "age": 21} |
csms:product:1 | {"id": 1, "name": "小米11", "price": 4399} |
Hash结构可以将对象中的每个字段独立存储,可以针对单个字段做CRUD
key | value | |
---|---|---|
filed | value | |
csms:user:1 | name | Jack |
age | 21 | |
csms:user:2 | name | Rose |
age | 18 |
Hash的常见命令
-
HSET key field value:添加或修改hash类型key的field的值
-
HGET key field:获取一个hash类型key的field的值
-
HMSET:批量添加多个hash类型key的field的值
-
HMGET:批量获取多个hash类型key的field的值
-
HGETALL:获取一个hash类型的key中的所有的field和value
-
HKEYS:获取一个hash类型的key中的所有的field
-
HVALS:获取一个hash类型的key中的所有的value
-
HINCRBY:让一个hash类型key的字段值自增并指定步长
-
HSETNX:添加一个hash类型的key的field值,前提是这个field不存在,否则不执行
2.6、List类型
Redis中的List类型与Java中的LinkedList类似,可以看做是一个双向链表结构。既可以支持正向检索和也可以支持反向检索。
特征与LinkedList类似:
-
有序
-
元素可以重复
-
插入和删除快
-
查询速度一般
常用来存储一个有序数据,例如:朋友圈点赞列表,评论列表等。
List的常见命令
-
LPUSH key element ...:向列表左侧插入一个或多个元素
-
LPOP key:移除并返回列表左侧的第一个元素,没有则返回nil
-
RPUSH key element ...:向列表右侧插入一个或多个元素
-
RPOP key:移除并返回列表右侧的第一个元素
-
LRANGE key star end:返回一段角标范围内的所有元素
-
BLPOP和BRPOP:与LPOP和RPOP类似,只不过在没有元素时等待指定时间,而不是直接返回nil。
总结
-
如何利用List结构模拟一个栈?
-
入口出口同一边
-
-
如何利用List结构模拟一个队列?
-
入口出口不同边
-
-
如何利用List结构模拟一个阻塞队列?
-
入口出口不同边
-
出队时采用BLPOP或BRPOP
-
2.7、Set类型
Redis的Set结构与Java中的HashSet类似,可以看做是一个value为null的HashMap。因为也是一个hash表,因此具备与HashSet类似的特征:
-
无序
-
元素不可重复
-
查找快
-
支持交集、并集、差集等功能
Set的常见命令
-
SADD key member ...:向set中添加一个或多个元素
-
SREM key member ...:移除set中的指定元素
-
SCARD key:返回set中元素的个数
-
SISMEMBER key member:判断一个元素是否存在于set中
-
SMEMBERS:获取set中的所有元素
-
SINTER key1 key2 ...:求key1与key2的交集
-
SDIFF key1 key2 ...:求key1与key2的差集
-
SUNION key1 key2 ...:求key1和key2的并集
2.8、SortedSet类型
Redis的SortedSet是一个可排序的set集合,与Java中的TreeSet有些类似,但底层数据结构却差别很大。SortedSet中的每一个元素都带有一个score属性,可以基于score属性对元素排序,底层的实现是一个跳表(SkipList)加入hash表。
SortedSet具备下列特性:
-
可排序
-
元素不重复
-
查询速度快
因为SortedSet的可排序特性,经常被用来实现排行榜这样的功能。
SortedSet常见命令
-
ZADD key score member:添加一个或多个元素到sorted set,如果已经存在则更新其score值
-
ZREM key member:删除sorted set中的一个指定元素
-
ZSCORE key member:获取sorted set中的指定元素的score值
-
ZRANK key member:获取sorted set中的指定元素的排名
-
ZCARD key:获取sorted set中的元素个数
-
ZCOUNT key min max:统计score值在给定范围内的所有元素的个数
-
ZINCRBY key increment member:让sorted set中的指定元素自增,步长为指定的increment值
-
ZRANGE key min max:按照score排序后,获取指定排名范围内的元素
-
ZRANGEBYSCORE key min max:按照score排序后,获取指定score范围内的元素
-
ZDIFF、ZINTER、ZUNION:求差集、交集、并集
注意:所有的排名默认都是升序,如果要降序则在命令的Z后面添加REV即可。
3、Redis的Java客户端
3.1、Jedis快速入门
Jedis的官网地址:GitHub - redis/jedis: Redis Java client
-
首先,引入依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.0</version>
</dependency>
-
接着,创建Jedis对象,建立连接
private Jedis jedis;
@BeforeEach
void setUp() {
// 建立连接
jedis = new Jedis("192.168.176.132", 6379);
// 设置密码
jedis.auth("123321");
// 选择库
jedis.select(0);
}
-
然后,使用Jedis,方法名和Redis命令一致,测试string
@Test
public void testString() {
// 存入结果
String result = jedis.set("name", "哈士奇");
// 获取数据
String name = jedis.get("name");
System.out.println("name = " + name);
}
@Test
public void testHash() {
// 插入hash数据
jedis.hset("user:1", "name", "Jack");
jedis.hset("user:1", "age", "21");
Map<String, String> map = jedis.hgetAll("user:1");
System.out.println(map);
}
-
最后,释放资源
@AfterEach
void tearDown() {
if (jedis != null) {
jedis.close();
}
}
3.2、Jedis连接池
Jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,因此推荐使用Jedis连接池代替Jedis的直连方式。
package com.heima.jedis.util;
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 poolConfig = new JedisPoolConfig();
// 最大连接数
poolConfig.setMaxTotal(8);
// 最大空闲连接
poolConfig.setMaxIdle(8);
// 最小空闲连接
poolConfig.setMinIdle(0);
// 等待时间,当连接池无连接可用时
poolConfig.setMaxWaitMillis(1000);
// 创建连接池对象
jedisPool = new JedisPool(poolConfig,
"192.168.176.132", 6379, 1000, "123321");
}
public static Jedis getJedis() {
return jedisPool.getResource();
}
}
3.3、认识SpringDataRedis
SpringData是Spring中数据操作的模块,包含对各种数据库的集成,其中对Redis的集成模块就叫做SpringDataRedis,官网地址:Spring Data Redis
-
提供了对不同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 | 通用的命令 |
3.4、RedisTemplate快速入门
首先,创建SpringBoot项目,选NOSQL模块里的第一个,引入依赖
<!-- redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- common-pool -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
接着,配置文件
spring:
redis:
host: 192.168.176.132
port: 6379
password: 123321
jedis:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: 100ms
然后,注入RedisTemplate
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
@SpringBootTest
class RedisDemoApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
}
}
编写测试
@Test
void contextLoads() {
// 写入一条String数据
redisTemplate.opsForValue().set("name", "哈士奇博美");
// 获取String数据
Object name = redisTemplate.opsForValue().get("name");
System.out.println("name=" + name);
}
总结
-
SpringDataRedis的使用步骤
-
引入spring-boot-starter-data-redis依赖
-
在application.yml配置Redis信息
-
注入RedisTemplate
-
3.5、SpringDataRedis的序列化方式
RedisTemplate可以接收任意Object作为值写入Redis,只不过写入前会把Object序列化为字节形式,默认是采用JDK序列化。
缺点:
1.可读性差 2.内存占用较大
可以自定义RedisTemplate的序列化方式,代码如下:
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.RedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
// 创建RedisTemplate对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 设置连接工厂
template.setConnectionFactory(connectionFactory);
// 创建JSON序列化工具
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
// 设置key的序列化
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// 设置value的序列化
template.setValueSerializer(jsonRedisSerializer);
template.setHashValueSerializer(jsonRedisSerializer);
// 返回
return template;
}
}
如果单纯的用上面这个类,则会报错,缺少json依赖。因为该SpringBoot没有导入springMVC相关配置。
<!-- Jackson依赖 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
接下来就进行测试即可。
3.6、StringRedisTemplate
尽管JSON的序列化方式可以满足需求,但依然存在一些问题,本身占用的内存比保存的数据更大。
为了在反序列化时知道对象的类型,JSON序列化器会将类的class类型写入json结果中,存入Redis,会带来额外的内存开销。
解决方式:
为了节省内幕空间,并不会使用JSON序列化器来处理value,而是统一使用String序列化器,要求只能存储String类型的key和value。当需要存储Java对象时,手动完成对象的序列化和反序列化。
Spring默认提供一个StringRedisTemplate类,它的key和value的序列化方式默认就是String方式。省去了自定义RedisTemplate的过程:
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.heima.redis.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
@SpringBootTest
class RedisStringTests {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
void contextLoads() {
// 写入一条String数据
stringRedisTemplate.opsForValue().set("name", "哈士奇");
// 获取String数据
Object name = stringRedisTemplate.opsForValue().get("name");
System.out.println("name=" + name);
}
private static final ObjectMapper mapper = new ObjectMapper();
@Test
void testSaveUser() throws JsonProcessingException {
// 创建对象
User user = new User("哈士奇", 23);
// 手动序列化
String json = mapper.writeValueAsString(user);
// 写入数据
stringRedisTemplate.opsForValue().set("user:200", json);
// 获取数据
String jsonUser = stringRedisTemplate.opsForValue().get("user:200");
// 手动反序列化
User user1 = mapper.readValue(jsonUser, User.class);
System.out.println("user1=" + user1);
}
}
RedisTemplate的两种序列化实践方案
-
方案一
-
自定义RedisTemplate
-
修改RedisTemplate的序列化器为GenericJackson2JsonRedisSerialiizer
-
-
方案二
-
使用StringRedisTemplate
-
写入Redis时,手动把对象序列化为JSON
-
读取Redis时,手动把读到的JSON反序列化为对象
-
3.7、RedisTemplate操作Hash类型
...
4、Redis实战
先导入项目,运行后端项目,成功后,运行前端项目
start nginx.exe
4.1、短信登录【Redis的共享session应用】
4.1.1、基于Session实现登录
发送短信验证码
-开始
-提交手机号
-校验手机号
-不符合,返回
-符合,生成验证码
-保存验证码到session
-发送验证码,结束
-
Entity层【User】
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 手机号码
*/
private String phone;
/**
* 密码,加密存储
*/
private String password;
/**
* 昵称,默认是随机字符
*/
private String nickName;
/**
* 用户头像
*/
private String icon = "";
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
-
Controller层【UserController】
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.entity.UserInfo;
import com.hmdp.service.IUserInfoService;
import com.hmdp.service.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpSession;
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private IUserService userService;
@Resource
private IUserInfoService userInfoService;
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// 发送短信验证码并保存验证码
return userService.sendCode(phone, session);
}
}
-
Service层【IUserService】
import com.baomidou.mybatisplus.extension.service.IService;
import com.hmdp.dto.Result;
import com.hmdp.entity.User;
import javax.servlet.http.HttpSession;
public interface IUserService extends IService<User> {
Result sendCode(String phone, HttpSession session);
}
-
ServiceImpl层【UserServiceImpl】
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.entity.User;
import com.hmdp.mapper.UserMapper;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpSession;
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
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);
// 返回ok
return Result.ok();
}
}
-
Mapper层【UserMapper】
import com.hmdp.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface UserMapper extends BaseMapper<User> {
}
短信验证码登录、注册
-开始
-提交手机号和验证码
-校验手机号、验证码【与保存在session的验证码比较】
-不一致,返回
-一致,根据手机号查询用户信息
-判断用户是否存在
-存在,保存用户到session,结束
-不存在,创建新用户,保存用户到数据库,最后保存用户到session,结束
-
创建LoginFormDTO类
import lombok.Data;
@Data
public class LoginFormDTO {
private String phone;
private String code;
private String password;
}
-
在UserController中加入方法
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// 实现登录功能
return userService.login(loginForm, session);
}
-
在IUserService中加入方法
Result login(LoginFormDTO loginForm, HttpSession session);
-
在UserServiceImpl中实现该方法
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 获取手机号
String phone = loginForm.getPhone();
// 1.检验手机号
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中
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
return Result.ok();
}
private User createUserWithPhone(String phone) {
// 1.创建用户
User user = new User();
user.setPhone(phone);
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
// 2.保存用户
save(user);
return user;
}
public class SystemConstants {
public static final String USER_NICK_NAME_PREFIX = "user_";
}
校验登录状态
-开始
-请求并携带cookie
-从session中获取用户
-判断用户是否存在
-没有,进行拦截,结束
-有,保存用户到ThreadLocal,放行,结束
-
要使用ThreadLocal,所以在utils包下创建UserHolder类
import com.hmdp.dto.UserDTO;
public class UserHolder {
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
public static void saveUser(UserDTO user){
tl.set(user);
}
public static UserDTO getUser(){
return tl.get();
}
public static void removeUser(){
tl.remove();
}
}
-
在utils包下创建LoginInterceptor类
import com.hmdp.dto.UserDTO;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class LoginInterceptor implements HandlerInterceptor {
@Override
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;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
UserHolder.removeUser();
}
}
-
在config包下声明拦截器MvcConfig类
import com.hmdp.utils.LoginInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**"
);
}
}
-
最后,在UserController中加入此方法
@GetMapping("/me")
public Result me(){
// 获取当前登录的用户并返回
UserDTO user = UserHolder.getUser();
return Result.ok(user);
}
4.1.2、集群的Session共享问题
session共享问题:多台Tomcat并不共享session存储空间,当请求切换到不同的Tomcat服务时导致数据丢失的问题。
session的替代方案应该满足:
1、数据共享
2、内存存储
3、key、value结构
4.1.3、基于Redis实现共享session登录
发送短信验证码
-开始
-提交手机号
-校验手机号
-不符合,返回
-符合,生成验证码
-保存验证码到redis【以手机号为key存储验证码】
-发送验证码,结束
-
在UserServiceImpl中加入StringRedisTemplate,注释并改动
@Resource
private StringRedisTemplate stringRedisTemplate;
-
在UserServiceImpl的sendCode方法中注释并加入关于Redis代码
// 4.保存验证码到session
// session.setAttribute("code", code);
// 4.保存验证码到redis,设置有效期
stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
-
在utils包下创建RedisConstants类,如下
public class RedisConstants {
public static final String LOGIN_CODE_KEY = "login:code:";
public static final Long LOGIN_CODE_TTL = 2L;
}
短信验证码登录、注册
-开始
-提交手机号和验证码
-校验手机号、验证码【以手机号为key读取验证码】
-不一致,返回
-一致,根据手机号查询用户信息
-判断用户是否存在
-存在,保存用户到Redis【以随机token为key存储用户数据】,返回token给客户端,结束
-不存在,创建新用户,保存用户到数据库,最后保存用户到session,结束
-
在utils包下创建RedisConstants类,如下
public class RedisConstants {
public static final String LOGIN_USER_KEY = "login:token:";
public static final Long LOGIN_USER_TTL = 30L;
}
-
在UserServiceImpl的login方法中注释并加入关于Redis代码
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 获取手机号
String phone = loginForm.getPhone();
// 1.检验手机号
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("验证码错误");
// }
// 3.从redis中获取验证码并校验
String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.equals(code)) {
// 4.不一致,报错
return Result.fail("验证码错误");
}
// 5.一致,根据手机号查询用户
User user = query().eq("phone", phone).one();
// 6.判断用户是否存在
if (user == null) {
// 7.不存在,创建新用户并保存
user = createUserWithPhone(phone);
}
// 8.保存用户信息到session中
// session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
// 8.保存用户信息到redis中
// 8.1.随机生成token,作为登录令牌
String token = UUID.randomUUID().toString(true);
// 8.2.将User对象转为HashMap存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);
// 8.3.存储
String tokenKey = RedisConstants.LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
// 8.4.设置用户未访问时token有效期
stringRedisTemplate.expire(tokenKey, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
// 9.返回token
return Result.ok(token);
}
校验登录状态
-开始
-请求并携带token
-从Redis中获取用户
-判断用户是否存在
-没有,进行拦截,结束
-有,保存用户到ThreadLocal,放行,结束
-
重做LoginInterceptor类
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class LoginInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.获取请求头中的token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
// 不存在,拦截,返回401状态码
response.setStatus(401);
return false;
}
// 2.基于token获取redis中的用户
String key = RedisConstants.LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
.entries(key);
// 3.判断用户是否存在
if (userMap.isEmpty()) {
// 4.不存在,拦截,返回401状态码
response.setStatus(401);
return false;
}
// 5.将查询到的Hash数据转为UserDTO对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 6.存在,保存用户信息到ThreadLocal
UserHolder.saveUser(userDTO);
// 7.刷新token有效期
stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
// 6.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
UserHolder.removeUser();
}
}
-
往MvcConfig中注入StringRedisTemplate
import javax.annotation.Resource;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
.excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**"
);
}
}
如果做到这里直接进行测试,在验证码确认后点击登录会出现服务器异常,显示类型转换错误。
修改此错误方式:将UserServiceImpl中login方法的第8.2步进行转换
// Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
最后完成,但还是存在相对的bug,状态登录刷新问题。
4.1.4、解决状态登录刷新问题
-
首先,添加新的拦截器名为RefreshTokenInterceptor,用于刷新
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
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;
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
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 = RedisConstants.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, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
// 6.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
UserHolder.removeUser();
}
}
-
修改原先拦截器LoginInterceptor,用于拦截
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.判断是否需要拦截(ThreadLocal中是否有用户)
if (UserHolder.getUser() == null) {
// 没有,进行拦截,设置状态码
response.setStatus(401);
// 拦截
return false;
}
// 2.有用户,则放行
return true;
}
}
-
最后修改MvcConnfig类
import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 登录拦截器
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**"
).order(1);
// token刷新拦截器
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
}
}
进行测试。
4.1.5、总结【Redis代替session需要考虑的问题】
-
选择合适的数据结构
-
选择合适的key
-
选择合适的存储粒度
4.2、商户查询缓存
企业缓存使用技巧【缓存雪崩、穿透等问题解决】
4.2.1、什么是缓存
缓存就是数据交换的缓冲区(称作Cache),是存数据的临时地方,一般读写性能较高。
浏览器【浏览器缓存】=》tomcat【应用层缓存】=》数据库【数据库缓存】{CPU缓存、磁盘缓存}
缓存作用:
1、降低后端负载
2、提高读写效率,降低相应时间
缓存成本:
1、数据一致性成本
2、代码维护成本
3、运维成本
4.2.2、添加Redis缓存
缓存作用模型
客户端向Redis发起请求,命中则返回,不命中则访问数据库,如果存在,则将数据先给Redis缓存,再返回给客户端。
根据id查询商铺缓存的流程
-开始
-提交商铺id,从Redis查询商铺缓存,判断缓存是否命中
-命中,返回商铺信息,结束
-未命中,根据id查询数据库,判断商铺是否存在
-存在,将商铺数据写入Redis,返回商铺信息,结束
-不存在,返回404,结束
-
Entity实体类【Shop】
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_shop")
public class Shop implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 商铺名称
*/
private String name;
/**
* 商铺类型的id
*/
private Long typeId;
/**
* 商铺图片,多个图片以','隔开
*/
private String images;
/**
* 商圈,例如陆家嘴
*/
private String area;
/**
* 地址
*/
private String address;
/**
* 经度
*/
private Double x;
/**
* 维度
*/
private Double y;
/**
* 均价,取整数
*/
private Long avgPrice;
/**
* 销量
*/
private Integer sold;
/**
* 评论数量
*/
private Integer comments;
/**
* 评分,1~5分,乘10保存,避免小数
*/
private Integer score;
/**
* 营业时间,例如 10:00-22:00
*/
private String openHours;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
@TableField(exist = false)
private Double distance;
}
-
Controller控制层【ShopController】
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.service.IShopService;
import com.hmdp.utils.SystemConstants;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@RestController
@RequestMapping("/shop")
public class ShopController {
@Resource
public IShopService shopService;
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
return shopService.queryById(id);
}
}
-
IService业务层【IShopService】
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.baomidou.mybatisplus.extension.service.IService;
public interface IShopService extends IService<Shop> {
Result queryById(Long id);
}
-
ServiceImpl业务实现层【ShopServiceImpl】
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisConstants;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
String key = RedisConstants.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);
}
// 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);
}
}
public class RedisConstants {
public static final String CACHE_SHOP_KEY = "cache:shop:";
}
-
Mapper层【ShopMapper】,包扫描放到了启动器上
import com.hmdp.entity.Shop;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface ShopMapper extends BaseMapper<Shop> {
}
练习:给店铺类型查询业务添加缓存
方式一:使用Redis的List缓存
-
ShopType
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_shop_type")
public class ShopType implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 类型名称
*/
private String name;
/**
* 图标
*/
private String icon;
/**
* 顺序
*/
private Integer sort;
/**
* 创建时间
*/
@JsonIgnore
private LocalDateTime createTime;
/**
* 更新时间
*/
@JsonIgnore
private LocalDateTime updateTime;
}
-
ShopTypeController
import com.hmdp.dto.Result;
import com.hmdp.entity.ShopType;
import com.hmdp.service.IShopTypeService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("/shop-type")
public class ShopTypeController {
@Resource
private IShopTypeService typeService;
@GetMapping("list")
public Result queryTypeList() {
// List<ShopType> typeList = typeService
// .query().orderByAsc("sort").list();
// return Result.ok(typeList);
return typeService.queryShopTypeList();
}
}
-
IShopTypeService
import com.hmdp.dto.Result;
import com.hmdp.entity.ShopType;
import com.baomidou.mybatisplus.extension.service.IService;
public interface IShopTypeService extends IService<ShopType> {
Result queryShopTypeList();
}
-
ShopTypeServiceImpl
import com.hmdp.dto.Result;
import com.hmdp.entity.ShopType;
import com.hmdp.mapper.ShopTypeMapper;
import com.hmdp.service.IShopTypeService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class ShopTypeServiceImpl extends ServiceImpl<ShopTypeMapper, ShopType> implements IShopTypeService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryShopTypeList() {
// 1.从Redis中查询商铺缓存
List<String> shopTypeJsonList = stringRedisTemplate.opsForList().range(RedisConstants.CACHE_SHOP_TYPE_KEY, 0, -1);
// 2.判断Redis中是否有该缓存
if (shopTypeJsonList != null && !shopTypeJsonList.isEmpty()) {
// 2.1.若Redis中存在该缓存,则直接返回
ArrayList<ShopType> shopTypes = new ArrayList<>();
for (String str : shopTypeJsonList) {
shopTypes.add(JSONUtil.toBean(str, ShopType.class));
}
return Result.ok(shopTypes);
}
// 2.2.Redis中若不存在该数据,则从数据库中查询
List<ShopType> shopTypes = query().orderByAsc("sort").list();
// 3.判断数据库中是否存在
if (shopTypes == null || shopTypes.isEmpty()) {
// 3.1.数据库中也不存在,则返回false
return Result.fail("该分类不存在!");
}
// 3.2.数据库中存在,则将查询到的信息存入Redis
for (ShopType shopType : shopTypes) {
stringRedisTemplate.opsForList().rightPushAll(RedisConstants.CACHE_SHOP_TYPE_KEY, JSONUtil.toJsonStr(shopType));
}
// 3.3.返回
return Result.ok(shopTypes);
}
}
-
ShopTypeMapper
import com.hmdp.entity.ShopType;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface ShopTypeMapper extends BaseMapper<ShopType> {
}
-
RedisConstants
public class RedisConstants {
public static final String CACHE_SHOP_TYPE_KEY = "cache:shopType";
}
方式二:使用Redis的String缓存
@Override
public Result queryShopTypeString() {
// 1.从 Redis 中查询商铺缓存
String shopTypeJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_TYPE_KEY);
// 2.判断 Redis 中是否存在数据
if (StrUtil.isNotBlank(shopTypeJson)) {
// 2.1.存在,则返回
List<ShopType> shopTypes = JSONUtil.toList(shopTypeJson, ShopType.class);
return Result.ok(shopTypes);
}
// 2.2.Redis 中不存在,则从数据库中查询
List<ShopType> shopTypes = query().orderByAsc("sort").list();
// 3.判断数据库中是否存在
if (shopTypes == null) {
// 3.1.数据库中也不存在,则返回 false
return Result.fail("分类不存在!");
}
// 3.3.2.1.数据库中存在,则将查询到的信息存入 Redis
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_TYPE_KEY, JSONUtil.toJsonStr(shopTypes));
// 3.3.2.2.返回
return Result.ok(shopTypes);
}
4.2.3、缓存更新策略
内存淘汰 | 超时剔除 | 主动更新 | |
---|---|---|---|
说明 | 不用自己维护,利用Redis的内存淘汰机制,当内存不足时自动淘汰部分数据。下次查询时更新缓存。 | 给缓存数据添加TTL时间,到期后自动删除缓存。下次查询时更新缓存。 | 编写业务逻辑,在修改数据库的同时,更新缓存。 |
一致性 | 差 | 一般 | 好 |
维护成本 | 无 | 低 | 高 |
业务场景
-
低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存。
-
高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存。
主动更新策略
-
Cache Aside Pattern:由缓存的调用者,在更新数据库的同时更新缓存【推荐】
-
Read/Write Through Pattern:缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题
-
Write Behind Caching Pattern:调用者只操作缓存,由其它线程异步的将缓存数据持久化到数据库,保持最终一致。
Cache Aside Pattern
操作缓存和数据库时有三个问题需要考虑:
-
删除缓存还是更新缓存?
-
更新缓存:每次更新数据库都更新缓存,无效写操作较多。
-
删除缓存:更新数据库时让缓存失效,查询时再更新缓存。【推荐】
-
-
如何保证缓存与数据库的操作的同时成功或失败?【原子性】
-
单体系统,将缓存与数据库操作放在一个事务
-
分布式系统,利用TCC等分布式事务方案
-
-
先操作缓存还是先操作数据库?
-
先删除缓存,再操作数据库
-
先操作数据库,再删除缓存【推荐】
-
总结【缓存更新策略的最佳实践方案】
-
低一致性需求:使用Redis自带的内存淘汰机制
-
高一致性需求:主动更新,并以超时剔除作为兜底方案
-
读操作:
-
缓存命中则直接放回
-
缓存未命中则查询数据库,并写入缓存,设定超时时间
-
-
写操作:
-
先写数据库,然后再删除缓存
-
要确保数据库与缓存操作的原子性
-
-
4.2.3.1、给查询商铺的缓存添加超时剔除和主动更新的策略
修改ShopController中的业务逻辑,满足下面的需求:
-
根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间。【超时剔除】
在ShopServiceImpl中的queryById方法的第六步添加剔除时间
public class RedisConstants {
public static final Long CACHE_SHOP_TTL = 30L;
}
// 6.存在,写入redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
-
根据id修改店铺时,先修改数据库,再删除缓存。【主动更新】
ShopController写入方法
/**
* 更新商铺信息
* @param shop 商铺数据
* @return 无
*/
@PutMapping
public Result updateShop(@RequestBody Shop shop) {
// 写入数据库
return shopService.updateShop(shop);
}
IShopService写入方法
Result updateShop(Shop shop);
在ShopServiceImpl中实现方法
@Override
@Transactional
public Result updateShop(Shop shop) {
Long id = shop.getId();
if (id == null) {
return Result.fail("店铺id不能为空");
}
// 1.更新数据库
updateById(shop);
// 2.删除缓存
stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + shop.getId());
return Result.ok();
}
请听下回分解-茂才公