1.环境配置IDEA连接远程redis:

ubantu22下载docker,下载redis:

一、更新系统软件包索引

1
sudo apt update

二、安装docker

1
sudo apt install docker.io

三:拉取redis:

1
docker pull redis

IDEA连接远程国外服务器的docker:

1.直接连接是无法连成功的!因为我购买的vps是国外的!被墙了的!我们需要先配置IDEA的http代理!这里我使用的是clash下载的代理:
2.打开设置查看windown的系统代理:记住地址和端口:
Snipaste_2024-01-27_19-18-22.png
3.IDEA配置http代理:
image.png
4.设置IDEAdocker连接:
image.png
同理也可以根据上面ssh连接海外服务器!如果上述方法无法连接成功就是节点的问题!

IDEA中docker连接远程redis容器:

redis默认只能本地访问远程访问需要额外的配置:
1.新建远程的ssh连接:
image.png
2.创建docker中redis容器的数据卷!分别映射redis容器中的data和配置信息:

1
2
3
mkdir -p /mydata/redis/data
mkdir -p /mydata/redis/conf
touch /mydata/redis/conf/redis.conf

3.在外部的redis.conf中的配置文件中书写如下内容:

1
2
3
4
5
6
7
#下面的意思是允许所有的ip访问!也可以如下书写:
# bind #bind 127.0.0.1 #允许远程连接
bind 0.0.0.0
# #启用保护模式 no后面最后不要有注释
protected-mode no
# 是否 #持久化
appendonly yes

4.启动容器:

1
2
docker run --name redis -p 6379:6379 -v /mydata/redis/data:/data -v /mydata/redis/conf/redis.conf:/etc/redis/redis.conf -d redis redis-server /etc/redis/redis.conf

5.在IDEA中Docker中查看redis连接情况;
image.png

连接过程中遇到的异常:

发现上述无法启动容器:

错误描述:

1
2
3
4
*** FATAL CONFIG FILE ERROR (Redis 7.2.4) ***
2024-01-29T00:56:19.699109788Z Reading the configuration file, at line 3
2024-01-29T00:56:19.699113769Z >>> 'protected-mode no #1'
2024-01-29T00:56:19.699117853Z wrong number of arguments

1.删除无法启动的容器:

1
2
# docker rm <container_name_or_id>
docker rm /redis

2.原因分析:
是protected-mode no #1后面的注释也被单当成配置信息参数的一部分了!
3.解决方法:
删除后面的注释,或者后面的注释换行写!

在docker中的redis容器中使用redis:

image.png
1.查看redis的密码:

1
2
3
4
#连接远程服务器中的redis服务器
redis-cli
#查看密码的配置信息
CONFIG GET requirepass

image.png

入门:

Redis数据结构介绍:

image.png
Snipaste_2024-01-29_09-21-07.png

String:

image.png
image.png

Key结构:

image.png

Hash:

image.png
image.png

List:

image.png
image.png

Set:

image.png
image.png
image.png

SortedSet:

image.png
image.png

Jedis连接:

https://www.cnblogs.com/dxflqm/p/17012394.html

1
2
3
4
5
6
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.9.0</version>
</dependency>

测试连接:

1
2
3
4
5
6
7
8
9
10
 void test1() {
// 1.构造一个 Jedis 对象,因为这里使用的默认端口 6379,所以不用配置端口
jedis = new Jedis("8.130.50.249", 6379);
// 2.密码认证,如果没有设置密码就不要下面这行代码
// jedis.auth("");
// 3.测试是否连接成功
String ping = jedis.ping();
// 4.返回 pong 表示连接成功
System.out.println(ping);
}

简单使用:

1
2
3
4
5
6
7
@Test
void testString() {
String result = jedis.set("name", "zhangsan");
System.out.println(result);
String value = jedis.get("name");
System.out.println(value);
}

连接池:

虽然 redis 服务端是单线程操作,但是在实际项目中,使用 Jedis 对象来操作 redis 时,每次操作都需要新建/关闭 TCP 连接,连接资源开销很高,同时 Jedis 对象的个数不受限制,在极端情况下可能会造成连接泄漏,同时 Jedis 存在多线程不安全的问题。
为什么说 Jedis 线程不安全,更加详细的原因可以访问这个地址
https://www.cnblogs.com/gxyandwmm/p/13485226.html
所以我们需要将 Jedis 交给线程池来管理,使用 Jedis 对象时,从连接池获取 Jedis,使用完成之后,再还给连接池。
创建一个简单的连接池测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
void testPool() {
// 1. 构造一个 Jedis 连接池
JedisPool pool = new JedisPool("8.130.50.249", 6379);
// 2. 从连接池中获取一个 Jedis 连接
Jedis jedis = pool.getResource();
// jedis.auth("111111");
// 3. Jedis 操作
String ping = jedis.ping();
System.out.println(ping);
// 4. 归还连接
jedis.close();
}

SpringDataRedis:

image.png
image.png
例子:

1
2
3
4
5
6
7
8
9
10
<!--common-pool-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--Jackson依赖-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
spring:
redis:
host: 8.130.50.249
port: 6379
password: ""
lettuce:
pool:
max-active: 8 #最大连接
max-idle: 8 #最大空闲连接
min-idle: 0 #最小空闲连接
max-wait: 100ms #连接等待时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootTest
class RedisDemoApplicationTests {

@Autowired
private RedisTemplate<Object,Object> redisTemplate;

@Test
void testString() {
// 写入一条String数据
redisTemplate.opsForValue().set("name", "虎哥");
// 获取string数据
Object name = redisTemplate.opsForValue().get("name");
System.out.println("name = " + name);
}
}

1.上面的操作是往redis储存的数据是JDK序列化的!看起来很麻烦!(K和V都是序列化的)
image.png
2.private RedisTemplate<String,Object> redisTemplate;注意这里如果直接使用@Autowired 注入的话就是会报错的:
image.png
原因:
@Autowired默认是按照类型注入的!我们可以去查看注入上面bean的源代码:
image.png
RedisTemplate<String,Object>按照@Autowired 注入RedisTemplate<String,Object>它会按照类型去容器中查找该类型的bean!容器中只有RedisTemplate<Object,Object>这种类型的bean!
解决方法:

1
2
@Autowired
private RedisTemplate<Object,Object> redisTemplate;

或者使用Resource注解:

1
2
3
@Resource
private RedisTemplate<String,Object> redisTemplate;

为什么可以使用@Resource 注解呢?简单来说是该注解是按照名称来进行依赖注入的!具体可见:@Autowired 和@Resource注解的区别:

数据序列化器:

RedisTemplate可以接收任意Object作为值写入Redis:
image.png
只不过写入前会把Object序列化为字节形式,默认是采用JDK序列化,得到的结果是这样的:
image.png
缺点:

  • 可读性差
  • 内存占用较大

我们可以自定义RedisTemplate的序列化方式,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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序列化来代替默认的JDK序列化方式。最终结果如图:
image.png
image.png

StringRedisTemplate:

image.png

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
@SpringBootTest
class RedisStringTests {

@Autowired
private StringRedisTemplate stringRedisTemplate;

@Test
void testString() {
// 写入一条String数据
stringRedisTemplate.opsForValue().set("verify:phone:13600527634", "124143");
// 获取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("虎哥", 21);
// 手动序列化
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);
}

@Test
void testHash() {
stringRedisTemplate.opsForHash().put("user:400", "name", "虎哥");
stringRedisTemplate.opsForHash().put("user:400", "age", "21");

Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries("user:400");
System.out.println("entries = " + entries);
}
}

总结:
RedisTemplate的两种序列化实践方案:

  • 方案一:
    • 自定义RedisTemplate
    • 修改RedisTemplate的序列化器为GenericJackson2JsonRedisSerializer
  • 方案二:
    • 使用StringRedisTemplate
    • 写入Redis时,手动把对象序列化为JSON
    • 读取Redis时,手动把读取到的JSON反序列化为对象

实战:

短信登录:

主要是实现发送验证码和登录个功能!

当我们在前端点击发送验证码的时候!我们就在后端生成验证码并储存在redis中!传统方式是把验证码储存在session中!但是这里有个问题就是:一个用户的同一个请求会被分发到不同的tomcat服务器中!这样用户在A服务器中的session,当用户再次访问该请求的时候可能会访问B服务器!这样就无法获取用户在A服务器的session了!我们虽然可以通过复制session的方法解决这个问题,但是又增加了额外的操作!但是如果我们采取的是redis储存的方式的话,就不要复制session了

当注册完成后,用户去登录会去校验用户提交的手机号和验证码,是否一致,如果一致,则根据手机号查询用户信息,不存在则新建,最后将用户数据保存到redis,并且生成token作为redis的key,当我们校验用户是否登录时,会去携带着token进行访问,从redis中取出token对应的value,判断是否存在这个数据,如果没有则拦截,如果存在则将其保存到threadLocal中,并且放行。

Redis代替session的业务流程:

设计key的结构

首先我们要思考一下利用redis来存储数据,那么到底使用哪种结构呢?由于存入的数据比较简单,我们可以考虑使用String,或者是使用哈希,如下图,如果使用String,同学们注意他的value,用多占用一点空间,如果使用哈希,则他的value中只会存储他数据本身,如果不是特别在意内存,其实使用String就可以啦。

设计key的具体细节

所以我们可以使用String结构,就是一个简单的key,value键值对的方式,但是关于key的处理,session他是每个用户都有自己的session,但是redis的key是共享的,咱们就不能使用code了
在设计这个key的时候,我们之前讲过需要满足两点
1、key要具有唯一性
2、key要方便携带
如果我们采用phone:手机号这个的数据来存储当然是可以的,但是如果把这样的敏感数据存储到redis中并且从页面中带过来毕竟不太合适,所以我们在后台生成一个随机串token,然后让前端带来这个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
    @Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1.校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3.从redis获取验证码并校验,下面的LOGIN_CODE_KEY值为login:code:
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 = ?
// 下面的query()方法是我们继承ServiceImpl带的!这里我们可以使用super来调用!
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
 User user = query().eq("phone", phone).one();
这个方法是Iservice接口中的默认方法!只不过这个类实现Iservice接口,可以在该类中直接使用query这个
默认方法!
java中一个类A实现了接口B!在A中该如何调用接口B中default修饰的方法?
public class A implements B {
public void someMethod() {
// 调用接口B中的default方法
defaultMethod();
}
}

导包后我们可以直接使用常量的名称而不必在常量名称前加上特定的包!
配置拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
public class MvcConfig implements WebMvcConfigurer {

@Resource
private StringRedisTemplate stringRedisTemplate;

@Override
public void addInterceptors(InterceptorRegistry registry) {
// 登录拦截器
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
).order(1);
// token刷新的拦截器,注意下面的拦截器order的优先级要高于上面的拦截器!也就是下面的拦截器先生效
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
}
}
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
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 = 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;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
UserHolder.removeUser();
}
}

1.实现了HandlerInterceptor就是一个拦截器!
2.一个请求过来tomcat服务器就会生成一个线程来处理这个请求!每个线程有个ThreadLocal用来储存和隔离不同线程中的一些数据!
3、afterCompletion是HandlerInterceptor接口中的一个方法。它在请求处理完成之后,即在响应已发送给客户端后执行。这个方法会在每次请求处理完成后都会执行。也就是每个请求处理完后准备返回response的时候就会擦除储存在ThreandLocal中的信息!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}
// 有用户,则放行
return true;
}
}

上面的拦截器是对于要用户登录后才能处理的请求的拦截处理!

商户查询缓存:

添加redis缓存:

image.png

缓存更新策略:

image.png
image.png
由缓存调用者,在更新数据库的同时更新缓存:有两种方式:

1.先删除缓存,然后操作数据库
2.先操作数据库然后更新缓存

上面2种情况都会出现缓存和数据库不一样的情况:
image.png
只不过后面异常情况发生的可能性不是很高!因为操作数据库的操作时间比较长,且存在等待I/O的时间!一般情况下线程2中更新数据库的指令会在线程1写入缓存的后面!
image.png
image.png

缓存穿透:

缓存穿透 :缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
缓存穿透带来的问题:不怀好意的人会一直利用缓存穿透来搞坏数据库服务器!
常见的解决方案有两种:

  • 缓存空对象
    • 优点:实现简单,维护方便
    • 缺点:
      • 额外的内存消耗
      • 可能造成短期的不一致
  • 布隆过滤
    • 优点:内存占用较少,没有多余key
    • 缺点:
      • 实现复杂
      • 存在误判可能

缓存空对象:
image.png
布隆克隆:
image.png
解决方法1:
image.png
image.png

缓存雪崩:

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:

  • 给不同的Key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

image.png

缓存击穿;

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了(key的TTL过期了),无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案有两种:

  • 互斥锁
  • 逻辑过期

逻辑分析:假设线程1在查询缓存之后,本来应该去查询数据库,然后把这个数据重新加载到缓存的,此时只要线程1走完这个逻辑,其他线程就都能从缓存中加载这些数据了,但是假设在线程1没有走完的时候,后续的线程2,线程3,线程4同时过来访问当前这个方法, 那么这些线程都不能从缓存中查询到数据,那么他们就会同一时刻来访问查询缓存,都没查到,接着同一时间去访问数据库,同时的去执行数据库代码,对数据库访问压力过大
image.png
image.png
image.png
image.png

案例:

image.png
sexnx命令:
image.png
释放锁获取锁代码:
image.png

知识点1:

泛型方法、Function类的函数化编程与调用:

泛型方法:
java泛型
函数式编程:
万字详解 | Java 函数式编程_Lambda_Phoenix_InfoQ写作社区

装箱空指针异常:

Java细节,自动封箱拆箱导致的NullPointerException问题_拆箱的 ‘group_meals’ 可能产生 ’java.lang.nullpointerexcep-CSDN博客
image.png
代码:

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

/**
* 更新商铺信息
* @param shop 商铺数据
* @return
*/
@PutMapping
public Result updateShop(@RequestBody Shop shop) {
// 写入数据库
return shopService.update(shop);
}
}
1
2
3
4
5
6
public interface IShopService extends IService<Shop> {

Result queryById(Long id);

Result update(Shop shop);
}
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
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {


@Resource
private StringRedisTemplate stringRedisTemplate;

@Resource
private CacheClient cacheClient;

@Override
public Result queryById(Long id) {
// 解决缓存穿透,this::getById参数是将该类中的getByid默认接口方法作为Function<ID, R> dbFallback中的apply方法!
Shop shop = cacheClient
.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);

// 互斥锁解决缓存击穿
// Shop shop = cacheClient
// .queryWithMutex(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);

// 逻辑过期解决缓存击穿
// Shop shop = cacheClient
// .queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, 20L, TimeUnit.SECONDS);

if (shop == null) {
return Result.fail("店铺不存在!");
}
// 7.返回
return Result.ok(shop);
}

@Override
@Transactional
public Result update(Shop shop) {
Long id = shop.getId();
if (id == null) {
return Result.fail("店铺id不能为空");
}
// 1.更新数据库
updateById(shop);
// 2.删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
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
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
@Slf4j
@Component
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.判断是否存在isNotBlank当值为null,空字符串,只有换行符等情况下都返回false
if (StrUtil.isNotBlank(json)) {
// 3.存在,直接返回
return JSONUtil.toBean(json, type);
}
// 判断命中的是否是空值
if (json != null) {
// 返回一个错误信息,因为这个时候我们命中redis是我们为了防止缓存穿透而设置的“”空值
return null;
}

// 4.不存在,根据id查询数据库,下面方法实际调用的是queryById方法
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);
}
}

秒杀:

image.png
image.png
如果用redis生成全局唯一ID安全性可能难以满足!因为redis中的默认自增是按照一定的长度自增的!
image.png