哈士奇发布Redis笔记-上

1、初识Redis

1.1、认识NoSQL

SQLNOSQL
数据结构结构化(Structured)非结构化
数据关联关联的(Relational)无关联的
查询方式SQL查询非SQL
事务特性ACIDBASE
存储方式磁盘内存
扩展性垂直水平
使用场景(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的类型多种多样:

类型内容
Stringhello 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)}
BitMap0110110101110101011
HyperLog0110110101110101011

解释:前五种是基本类型,后三种是特殊类型。

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:浮点类型,可以做自增、自减操作

KEYVALUE
msghello world
num10
score92.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字符串后存储:

KEYVALUE
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字符串后存储,当需要修改对象某个字段时很不方便:

KEYVALUE
csms:user:1{"id": 1, "name": "Jack", "age": 21}
csms:product:1{"id": 1, "name": "小米11", "price": 4399}

Hash结构可以将对象中的每个字段独立存储,可以针对单个字段做CRUD

keyvalue
filedvalue
csms:user:1nameJack
age21
csms:user:2nameRose
age18

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();
}

请听下回分解-茂才公

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值