黑马商城学习笔记(一)

一. 开篇导读

1. 短信登录

这一块我们会使用redis共享session来实现

2. 商户查询缓存

通过本章节,我们会理解缓存击穿,缓存穿透,缓存雪崩等问题,让小伙伴的对于这些概念的理解不仅仅是停留在概念上,更是能在代码中看到对应的内容

3. 优惠卷秒杀

通过本章节,我们可以学会Redis的计数器功能, 结合Lua完成高性能的redis操作,同时学会Redis分布式锁的原理,包括Redis的三种消息队列

4. 附近的商户

我们利用Redis的GEOHash来完成对于地理坐标的操作

5. UV统计

主要是使用Redis来完成统计功能

6. 用户签到

使用Redis的BitMap数据统计功能

7. 好友关注

基于Set集合的关注、取消关注,共同关注等等功能,这一块知识咱们之前就讲过,这次我们在项目中来使用一下

8. 打人探店

基于List来完成点赞列表的操作,同时基于SortedSet来完成点赞的排行榜功能

以上这些内容咱们统统都会给小伙伴们讲解清楚,让大家充分理解如何使用Redis

9. 课程总结

二. 短信登录

1.导入SQL hmdp.sql

a.tb.user 用户表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- auto-generated definition
create table tb_user
(
id bigint unsigned auto_increment comment '主键'
primary key,
phone varchar(11) not null comment '手机号码',
password varchar(128) default '' null comment '密码,加密存储',
nick_name varchar(32) default '' null comment '昵称,默认是用户id',
icon varchar(255) default '' null comment '人物头像',
create_time timestamp default CURRENT_TIMESTAMP not null comment '创建时间',
update_time timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
constraint uniqe_key_phone
unique (phone)
)
charset = utf8mb4
row_format = COMPACT;

b.tb_user_info 用户详情表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- auto-generated definition
create table tb_user_info
(
user_id bigint unsigned not null comment '主键,用户id'
primary key,
city varchar(64) default '' null comment '城市名称',
introduce varchar(128) null comment '个人介绍,不要超过128个字符',
fans int(8) unsigned default 0 null comment '粉丝数量',
followee int(8) unsigned default 0 null comment '关注的人的数量',
gender tinyint(1) unsigned default 0 null comment '性别,0:男,1:女',
birthday date null comment '生日',
credits int(8) unsigned default 0 null comment '积分',
level tinyint(1) unsigned default 0 null comment '会员级别,0~9级,0代表未开通会员',
create_time timestamp default CURRENT_TIMESTAMP not null comment '创建时间',
update_time timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间'
)
charset = utf8mb4
row_format = COMPACT;

c.tb_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
-- auto-generated definition
create table tb_shop
(
id bigint unsigned auto_increment comment '主键'
primary key,
name varchar(128) not null comment '商铺名称',
type_id bigint unsigned not null comment '商铺类型的id',
images varchar(1024) not null comment '商铺图片,多个图片以'',''隔开',
area varchar(128) null comment '商圈,例如陆家嘴',
address varchar(255) not null comment '地址',
x double unsigned not null comment '经度',
y double unsigned not null comment '维度',
avg_price bigint(10) unsigned null comment '均价,取整数',
sold int unsigned zerofill not null comment '销量',
comments int unsigned zerofill not null comment '评论数量',
score int(2) unsigned zerofill not null comment '评分,1~5分,乘10保存,避免小数',
open_hours varchar(32) null comment '营业时间,例如 10:00-22:00',
create_time timestamp default CURRENT_TIMESTAMP null comment '创建时间',
update_time timestamp default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间'
)
charset = utf8mb4
row_format = COMPACT;

create index foreign_key_type
on tb_shop (type_id);

d.tb_shop_type 商户类型表

1
2
3
4
5
6
7
8
9
10
11
12
13
-- auto-generated definition
create table tb_shop_type
(
id bigint unsigned auto_increment comment '主键'
primary key,
name varchar(32) null comment '类型名称',
icon varchar(255) null comment '图标',
sort int(3) unsigned null comment '顺序',
create_time timestamp default CURRENT_TIMESTAMP null comment '创建时间',
update_time timestamp default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间'
)
charset = utf8mb4
row_format = COMPACT;

e.tb_blog 用户日记表(达人探店日记)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- auto-generated definition
create table tb_blog
(
id bigint unsigned auto_increment comment '主键'
primary key,
shop_id bigint not null comment '商户id',
user_id bigint unsigned not null comment '用户id',
title varchar(255) collate utf8mb4_unicode_ci not null comment '标题',
images varchar(2048) not null comment '探店的照片,最多9张,多张以","隔开',
content varchar(2048) collate utf8mb4_unicode_ci not null comment '探店的文字描述',
liked int(8) unsigned default 0 null comment '点赞数量',
comments int(8) unsigned null comment '评论数量',
create_time timestamp default CURRENT_TIMESTAMP not null comment '创建时间',
update_time timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间'
)
charset = utf8mb4
row_format = COMPACT;

f.tb_follow 用户关注表

1
2
3
4
5
6
7
8
9
10
11
-- auto-generated definition
create table tb_follow
(
id bigint auto_increment comment '主键'
primary key,
user_id bigint unsigned not null comment '用户id',
follow_user_id bigint unsigned not null comment '关联的用户id',
create_time timestamp default CURRENT_TIMESTAMP not null comment '创建时间'
)
charset = utf8mb4
row_format = COMPACT;

g.tb_voucher 优惠卷表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- auto-generated definition
create table tb_voucher
(
id bigint unsigned auto_increment comment '主键'
primary key,
shop_id bigint unsigned null comment '商铺id',
title varchar(255) not null comment '代金券标题',
sub_title varchar(255) null comment '副标题',
rules varchar(1024) null comment '使用规则',
pay_value bigint(10) unsigned not null comment '支付金额,单位是分。例如200代表2元',
actual_value bigint(10) not null comment '抵扣金额,单位是分。例如200代表2元',
type tinyint(1) unsigned default 0 not null comment '0,普通券;1,秒杀券',
status tinyint(1) unsigned default 1 not null comment '1,上架; 2,下架; 3,过期',
create_time timestamp default CURRENT_TIMESTAMP not null comment '创建时间',
update_time timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间'
)
charset = utf8mb4
row_format = COMPACT;

h.tb_voucher_order 优惠卷的订单表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- auto-generated definition
create table tb_voucher_order
(
id bigint not null comment '主键'
primary key,
user_id bigint unsigned not null comment '下单的用户id',
voucher_id bigint unsigned not null comment '购买的代金券id',
pay_type tinyint(1) unsigned default 1 not null comment '支付方式 1:余额支付;2:支付宝;3:微信',
status tinyint(1) unsigned default 1 not null comment '订单状态,1:未支付;2:已支付;3:已核销;4:已取消;5:退款中;6:已退款',
create_time timestamp default CURRENT_TIMESTAMP not null comment '下单时间',
pay_time timestamp null comment '支付时间',
use_time timestamp null comment '核销时间',
refund_time timestamp null comment '退款时间',
update_time timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间'
)
charset = utf8mb4
row_format = COMPACT;

2.当前模型

手机或者app端发起请求,请求我们的nginx服务器,nginx基于七层模型走是HTTP协议,可以实现基于Lua直接绕开tomcat访问redis,也可以作为静态资源服务器,轻松扛下上万并发, 负载均衡到下游tomcat服务器,打散流量,我们都知道一台4核8G的tomcat,在优化和处理简单业务的加持下,大不了就处理1000左右的并发, 经过nginx的负载均衡分流后,利用集群支撑起整个项目,同时nginx在部署了前端项目后,更是可以做到动静分离,进一步降低tomcat服务的压力,这些功能都得靠nginx起作用,所以nginx是整个项目中重要的一环。

在tomcat支撑起并发流量后,我们如果让tomcat直接去访问Mysql,根据经验Mysql企业级服务器只要上点并发,一般是16或32 核心cpu,32 或64G内存,像企业级mysql加上固态硬盘能够支撑的并发,大概就是4000起~7000左右,上万并发, 瞬间就会让Mysql服务器的cpu,硬盘全部打满,容易崩溃,所以我们在高并发场景下,会选择使用mysql集群,同时为了进一步降低Mysql的压力,同时增加访问的性能,我们也会加入Redis,同时使用Redis集群使得Redis对外提供更好的服务。

nginx

3.打开前后端工程项目

后端加载依赖后访问: http://localhost:8080

4.基于Session实现登录流程

Session登录流程图

a.发送验证码:

用户在提交手机号后,会校验手机号是否合法,如果不合法,则要求用户重新输入手机号

如果手机号合法,后台此时生成对应的验证码,同时将验证码进行保存,然后再通过短信的方式将验证码发送给用户

b.短信验证码登录、注册:

用户将验证码和手机号进行输入,后台从session中拿到当前验证码,然后和用户输入的验证码进行校验,如果不一致,则无法通过校验,如果一致,则后台根据手机号查询用户,如果用户不存在,则为用户创建账号信息,保存到数据库,无论是否存在,都会将用户信息保存到session中,方便后续获得当前登录信息

c.校验登录状态:

用户在请求时候,会从cookie中携带者JsessionId到后台,后台通过JsessionId从session中拿到用户信息,如果没有session信息,则进行拦截,如果有session信息,则将用户信息保存到threadLocal中,并且放行

d.代码

  • 发送验证码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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();
}
  • 登录
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
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1.校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3.校验验证码
Object cacheCode = session.getAttribute("code");
String code = loginForm.getCode();
if(cacheCode == null || !cacheCode.toString().equals(code)){
//3.不一致,报错
return Result.fail("验证码错误");
}
//一致,根据手机号查询用户
User user = query().eq("phone", phone).one();

//5.判断用户是否存在
if(user == null){
//不存在,则创建
user = createUserWithPhone(phone);
}
//7.保存用户信息到session中
session.setAttribute("user",user);

return Result.ok();
}

tomcat的运行原理

tomcat的运行原理

当用户发起请求时,会访问我们像tomcat注册的端口,任何程序想要运行,都需要有一个线程对当前端口号进行监听,tomcat也不例外,当监听线程知道用户想要和tomcat连接连接时,那会由监听线程创建socket连接,socket都是成对出现的,用户通过socket像互相传递数据,当tomcat端的socket接受到数据后,此时监听线程会从tomcat的线程池中取出一个线程执行用户请求,在我们的服务部署到tomcat后,线程会找到用户想要访问的工程,然后用这个线程转发到工程中的controller,service,dao中,并且访问对应的DB,在用户执行完请求后,再统一返回,再找到tomcat端的socket,再将数据写回到用户端的socket,完成请求和响应

通过以上讲解,我们可以得知 每个用户其实对应都是去找tomcat线程池中的一个线程来完成工作的, 使用完成后再进行回收,既然每个请求都是独立的,所以在每个用户去访问我们的工程时,我们可以使用threadlocal来做到线程隔离,每个线程操作自己的一份数据

如果小伙伴们看过threadLocal的源码,你会发现在threadLocal中,无论是他的put方法和他的get方法, 都是先从获得当前用户的线程,然后从线程中取出线程的成员变量map,只要线程不一样,map就不一样,所以可以通过这种方式来做到线程隔离

e.拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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((User)user);
//6.放行
return true;
}
}

配置拦截器,使之生效

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刷新的拦截器
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
}
}

f.隐藏用户敏感信息

通过直接映射数据库对应表的实体类,里面可能会包含很多重要信息,或者不想要暴露一些字段,再或者我想最后要传进来一个多个表的数据,那么我们就可以添加一个新的DTO类, 用来存储这个不是敏感的数据

g.session共享问题

每个tomcat中都有一份属于自己的session,假设用户第一次访问第一台tomcat,并且把自己的信息存放到第一台服务器的session中,但是第二次这个用户访问到了第二台tomcat,那么在第二台服务器上,肯定没有第一台服务器存放的session,所以此时 整个登录拦截功能就会出现问题,我们能如何解决这个问题呢?早期的方案是session拷贝,就是说虽然每个tomcat上都有不同的session,但是每当任意一台服务器的session修改时,都会同步给其他的Tomcat服务器的session,这样的话,就可以实现session的共享了

但是这种方案具有两个大问题

  1. 每台服务器中都有完整的一份session数据,服务器压力过大。

  2. session拷贝数据时,可能会出现延迟

所以咱们后来采用的方案都是基于redis来完成,我们把session换成redis,redis数据本身就是共享的,就可以避免session共享的问题了

5.基于Redis实现登录流程

a.设计Key的结构

b.设计key的具体细节

所以我们可以使用String结构,就是一个简单的key,value键值对的方式,但是关于key的处理,session他是每个用户都有自己的session,但是redis的key是共享的,咱们就不能使用code了

在设计这个key的时候,我们之前讲过需要满足两点

  1. key要具有唯一性

  2. key要方便携带

如果我们采用phone:手机号这个的数据来存储当然是可以的,但是如果把这样的敏感数据存储到redis中并且从页面中带过来毕竟不太合适,所以我们在后台生成一个随机串token,然后让前端带来这个token就能完成我们的整体逻辑了

c.整体访问流程

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

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
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1.校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3.从redis获取验证码并校验
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.equals(code)) {
// 不一致,报错
return Result.fail("验证码错误");
}

// 4.一致,根据手机号查询用户 select * from tb_user where phone = ?
User user = query().eq("phone", phone).one();

// 5.判断用户是否存在
if (user == null) {
// 6.不存在,创建新用户并保存
user = createUserWithPhone(phone);
}

// 7.保存用户信息到 redis中
// 7.1.随机生成token,作为登录令牌
String token = UUID.randomUUID().toString(true);
// 7.2.将User对象转为HashMap存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
// 7.3.存储
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
// 7.4.设置token有效期
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);

// 8.返回token
return Result.ok(token);
}

d. 解决状态登录刷新问题

1.初始方案

在这个方案中,他确实可以使用对应路径的拦截,同时刷新登录token令牌的存活时间,但是现在这个拦截器他只是拦截需要被拦截的路径,假设当前用户访问了一些不需要拦截的路径,那么这个拦截器就不会生效,所以此时令牌刷新的动作实际上就不会执行,所以这个方案他是存在问题的

2.优化方案

既然之前的拦截器无法对不需要拦截的路径生效,那么我们可以添加一个拦截器,在第一个拦截器中拦截所有的路径,把第二个拦截器做的事情放入到第一个拦截器中,同时刷新令牌,因为第一个拦截器有了threadLocal的数据,所以此时第二个拦截器只需要判断拦截器中的user对象是否存在即可,完成整体刷新功能。

3.代码

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

三. 错误总结

在定义StringRedisTemplate的时候定义为了

1
2
@Resource
private StringRedisTemplate redisTemplate;

结果在运行的时候报初始化失败异常

1
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'mvcConfig': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'redisTemplate' is expected to be of type 'org.springframework.data.redis.core.StringRedisTemplate' but was actually of type 'org.springframework.data.redis.core.RedisTemplate'

这是因为Spring 在创建mvcConfig这个 Bean 时,尝试注入 StringRedisTemplate 类型的 redisTemplate,但实际找到的 Bean 类型却是 RedisTemplate,从而引发了类型不匹配的错误。

定义为

1
2
@Resource
private StringRedisTemplate stringRedisTemplate;

或者使用

1
2
@Autowired
private StringRedisTemplate stringRedisTemplate;

就可以正常运行了

为什么会出现这个问题呢?
这里有俩个要点:

  1. @Autowired 和 @Resource 有什么区别?
  2. StringRedisTemplate和RedisTemplate有什么区别?

1. @Autowired 和 @Resource的区别

@Autowired 和 @Resource 都是 Spring/Spring Boot 项目中,用来进行依赖注入的注解。它们都提供了将依赖对象注入到当前对象的功能,但二者却有众多不同,并且这也是常见的面试题之一,所以我们今天就来盘它。
@Autowired 和 @Resource 的区别主要体现在以下 5 点:

  • 来源不同;
  • 依赖查找的顺序不同;
  • 支持的参数不同;
  • 依赖注入的用法不同;
  • 编译器 IDEA 的提示不同。

a.来源不同

@Autowired 和 @Resource 来自不同的“父类”,其中 @Autowired 是 Spring 定义的注解,而 @Resource 是 Java 定义的注解,它来自于 JSR-250(Java 250 规范提案)。

小知识:JSR 是 Java Specification Requests 的缩写,意思是“Java 规范提案”。任何人都可以提交 JSR 给 Java 官方,但只有最终确定的 JSR,才会以 JSR-XXX 的格式发布,如 JSR-250,而被发布的 JSR 就可以看作是 Java 语言的规范或标准。

b.依赖查找顺序不同

依赖注入的功能,是通过先在 Spring IoC 容器中查找对象,再将对象注入引入到当前类中。而查找有分为两种实现:按名称(byName)查找或按类型(byType)查找,其中 @Autowired 和 @Resource 都是既使用了名称查找又使用了类型查找,但二者进行查找的顺序却截然相反。

1.@Autowired 查找顺序

@Autowired 是先根据类型(byType)查找,如果存在多个 Bean 再根据名称(byName)进行查找,它的具体查找流程如下:

关于以上流程,可以通过查看 Spring 源码中的 org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#postProcessPropertyValues 实现分析得出,源码执行流程如下图所示:

2.@Resource 查找顺序

@Resource 是先根据名称查找,如果(根据名称)查找不到,再根据类型进行查找,它的具体流程如下图所示:

关于以上流程可以在 Spring 源码的 org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#postProcessPropertyValues 中分析得出。虽然 @Resource 是 JSR-250 定义的,但是由 Spring 提供了具体实现,它的源码实现如下:

3. 查找顺序小结

由上面的分析可以得出:

  1. @Autowired 先根据类型(byType)查找,如果存在多个(Bean)再根据名称(byName)进行查找;
  2. @Resource 先根据名称(byName)查找,如果(根据名称)查找不到,再根据类型(byType)进行查找。

c. 支持的参数不同

其中 @Autowired 只支持设置一个 required 的参数,而 @Resource 支持 7 个参数

d.依赖注入的支持不同

@Autowired 支持属性注入、构造方法注入和 Setter 注入,而 @Resource 只支持属性注入和 Setter 注入

e.编译器提示不同

当使用 IDEA 专业版在编写依赖注入的代码时,如果注入的是 Mapper 对象,那么使用 @Autowired 编译器会提示报错信息

f.总结

@Autowired 和 @Resource 都是用来实现依赖注入的注解(在 Spring/Spring Boot 项目中),但二者却有着 5 点不同:

  1. 来源不同:@Autowired 来自 Spring 框架,而 @Resource 来自于(Java)JSR-250;
  2. 依赖查找的顺序不同:@Autowired 先根据类型再根据名称查询,而 @Resource 先根据名称再根据类型查询;
  3. 支持的参数不同:@Autowired 只支持设置 1 个参数,而 @Resource 支持设置 7 个参数;
  4. 依赖注入的用法支持不同:@Autowired 既支持构造方法注入,又支持属性注入和 Setter 注入,而 @Resource 只支持属性注入和 Setter 注入;
  5. 编译器 IDEA 的提示不同:当注入 Mapper 对象时,使用 @Autowired 注解编译器会提示错误,而使用 @Resource 注解则不会提示错误。

2. 回顾问题

看到这里答案也就清晰了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootTest
class HmDianPingApplicationTests {

@Autowired
private ApplicationContext applicationContext;

@Test
void contextLoads() {
String[] beanNames = applicationContext.getBeanDefinitionNames();
for (String beanName : beanNames) {
System.out.println(beanName);
}
}
}

看一下Bean容器

1
2
redisTemplate
stringRedisTemplate

里面会有俩个,如果使用@Resource,自然会按名称来优先查询了

3. StringRedisTemplate和RedisTemplate的区别

  1. 两者的关系是StringRedisTemplate继承RedisTemplate
  2. 两者的数据是不共通的;也就是说StringRedisTemplate只能管理StringRedisTemplate里面的数据,RedisTemplate只能管理RedisTemplate中的数据。
  3. RedisTemplate使用的是JdkSerializationRedisSerializer序列化类,存入数据会将数据先序列化成字节数组然后在存入Redis数据库。 StringRedisTemplate使用的是StringRedisSerializer序列化类
  4. 当redis数据库里面本来存的是字符串数据或者要存取的数据就是字符串类型数据的时候,那么使用StringRedisTemplate即可。但是如果你的数据是复杂的对象类型,而取出的时候又不想做任何的数据转换,直接从Redis里面取出一个对象,那么使用RedisTemplate是更好的选择。
  5. redisTemplate 中存取数据都是字节数组。当redis中存入的数据是可读形式而非字节数组时,使用redisTemplate取值的时候会无法获取导出数据,获得的值为null。可以使用 StringRedisTemplate 试试

a.RedisTemplate中定义了5种数据结构操作:

1
2
3
4
5
redisTemplate.opsForValue();  //操作字符串
redisTemplate.opsForHash();   //操作hash
redisTemplate.opsForList();   //操作list
redisTemplate.opsForSet();   //操作set
redisTemplate.opsForZSet();   //操作有序set

b.StringRedisTemplate常用操作

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
// 向redis里存入数据和设置缓存时间  
stringRedisTemplate.opsForValue().set("test", "100", 60*10, TimeUnit.SECONDS);

// val做-1操作
stringRedisTemplate.boundValueOps("test").increment(-1);

// 根据key获取缓存中的val
stringRedisTemplate.opsForValue().get("test")

// val + 1
stringRedisTemplate.boundValueOps("test").increment(1);

// 根据key获取过期时间
stringRedisTemplate.getExpire("test")

// 根据key获取过期时间并换算成指定单位
stringRedisTemplate.getExpire("test",TimeUnit.SECONDS);

// 根据key删除缓存
stringRedisTemplate.delete("test");

// 检查key是否存在,返回boolean值
stringRedisTemplate.hasKey("546545");

// 向指定key中存放set集合
stringRedisTemplate.opsForSet().add("red_123`在这里插入代码片`", "1", "2", "3");

// 设置过期时间
stringRedisTemplate.expire("red_123", 1000 , TimeUnit.MILLISECONDS);

// 根据key查看集合中是否存在指定数据
stringRedisTemplate.opsForSet().isMember("red_123", "1");

// 根据key获取set集合
stringRedisTemplate.opsForSet().members("red_123");

c.基于StringRedisTemplate封装一个缓存工具类,满足下列需求:

方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题

以下封装涉及到了缓存穿透与缓存击穿的解决方案

1.依赖:

1
2
3
4
5
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>

2.实体类

1
2
3
4
5
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}

3.封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
@Component
@Slf4j
public class RedisUtils {

private final RedisTemplate<String, Object> redisTemplate;

private final StringRedisTemplate stringRedisTemplate;

// 防止缓存穿透而设置的空值的过期时间
private final Long CACHE_NULL_TTL = 2L;

// 防止缓存穿透而设置的空值的过期时间的单位
private final TimeUnit CACHE_UNIT = TimeUnit.MINUTES;

// 互斥锁
private final String LOCK_APP_KEY = "lock:app:";

private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

public RedisUtils(RedisTemplate<String, Object> redisTemplate, StringRedisTemplate stringRedisTemplate) {
this.redisTemplate = redisTemplate;
this.stringRedisTemplate = stringRedisTemplate;
}

// =============================common============================

/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
* @return boolean
*/
public boolean expire(String key, long time) {
if (time <= 0) return false;
redisTemplate.expire(key, time, TimeUnit.SECONDS);
return true;
}

/**
* 根据key 获取过期时间,单位为秒
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public Long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}

/**
* 根据key 获取过期时间,并指定时间单位
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public Long getExpire(String key, TimeUnit unit) {
return redisTemplate.getExpire(key, unit);
}

/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}

/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
public boolean del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(Arrays.asList(key));
}
return true;
}
return false;
}

/**
* 根据 key 前缀批量删除
* @param prefix 前缀
*/
public boolean DelByPrefix(String prefix) {
Set<String> keys = redisTemplate.keys(prefix + "*");
if(Objects.nonNull(keys)) {
redisTemplate.delete(keys);
return true;
}
return false;
}

// ============================String=============================

/**
* 普通缓存获取
* @param key
* @return
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}

/**
* 普通缓存写入
* @param key
* @param value
* @return
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
log.error("缓存写入错误!key:{},value:{},错误信息:{}", key, value, e.getMessage());
return false;
}
}

/**
* 普通缓存写入并设置过期时间
* @param key
* @param value
* @param time 时间(秒) 如果time小于等于0,则设置不过期
* @return
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
log.error("缓存写入错误!key:{},value:{},time:{},错误信息:{}", key, value, time, e.getMessage());
return false;
}
}

/**
* 递增,默认+1
* @param key
* @return
*/
public Long incr(String key) {
return redisTemplate.opsForValue().increment(key, 1);
}

/**
* 递增,自定义递增步长
* @param key 键
* @param step 步长
* @return
*/
public Long incr(String key, long step) {
return redisTemplate.opsForValue().increment(key, step);
}

/**
* 递减,默认-1
* @param key
* @return
*/
public Long decr(String key) {
return redisTemplate.opsForValue().increment(key, -1);
}

/**
* 递减,默认-1
* @param key
* @param step 步长
* @return
*/
public Long decr(String key, long step) {
return redisTemplate.opsForValue().increment(key, -step);
}

// ================================Map=================================

/**
* HashGet
* @param key
* @param item
* @return
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}

/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}

/**
* HashSet
* @param key
* @param map
* @return
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* HashSet 并设置过期时间
* @param key
* @param map
* @param time 如果小于等于0,则不设置过期时间,单位为秒
* @return
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
log.error("HashSet写入错误!key:{},map:{},time:{},错误信息:{}", key, map, time, e.getMessage());
return false;
}
}

/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key
* @param item
* @param value
* @return
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
log.error("hset写入错误!key:{},item:{},value:{},错误信息:{}", key, item, value, e.getMessage());
return false;
}
}

/**
* 向一张hash表中放入数据并设置过期时间,如果不存在将创建
* @param key
* @param item
* @param value
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
log.error("hset写入错误!key:{},item:{},value:{},time:{},错误信息:{}", key, item, value, time, e.getMessage());
return false;
}
}

/**
* 删除hash表中的值
* @param key
* @param item
*/
public boolean hdel(String key, Object... item) {
try {
redisTemplate.opsForHash().delete(key, item);
return true;
} catch (Exception e) {
log.error("hdel删除错误!key:{},item:{},错误信息:{}", key, item, e.getMessage());
return false;
}
}

/**
* 判断hash表中是否有该项的值
* @param key
* @param item
* @return
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}

/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
* @param key
* @param item
* @param step 步长
* @return
*/
public double hincr(String key, String item, double step) {
return redisTemplate.opsForHash().increment(key, item, step);
}

/**
* hash递减
* @param key
* @param item
* @param step 步长
* @return
*/
public double hdecr(String key, String item, double step) {
return redisTemplate.opsForHash().increment(key, item, -step);
}

// ============================set=============================

/**
* 根据key获取Set中的所有值
* @param key
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
log.error("sGet获取错误!key:{},错误信息:{}", key, e.getMessage());
return null;
}
}

/**
* 查询指定set是否存在value
* @param key
* @param value
* @return
*/
public boolean sHasKey(String key, Object value) {
try {
return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(key, value));
} catch (Exception e) {
log.error("sHasKey查询错误!key:{},value:{},错误信息:{}", key, value, e.getMessage());
return false;
}
}

/**
* 将set数据写入缓存
*
* @param key
* @param values
* @return 成功个数
*/
public Long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
log.error("sSet写入错误!key:{},values:{},错误信息:{}", key, values, e.getMessage());
return null;
}
}

/**
* 将set数据写入缓存并设置过期时间
* @param key
* @param time 小于等于0则不设置过期
* @param values
* @return 成功个数
*/
public Long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) expire(key, time);
return count;
} catch (Exception e) {
log.error("sSet写入错误!key:{},time:{},values:{},错误信息:{}", key, time, values, e.getMessage());
return null;
}
}

/**
* 获取set缓存的长度
* @param key
* @return
*/
public Long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
log.error("sGetSetSize获取错误!key:{},错误信息:{}", key, e.getMessage());
return null;
}
}

/**
* 批量移除指定键值对
* @param key
* @param values
* @return 移除成功个数
*/
public Long setRemove(String key, Object... values) {
try {
return redisTemplate.opsForSet().remove(key, values);
} catch (Exception e) {
log.error("setRemove移除错误!key:{},values:{},错误信息:{}", key, values, e.getMessage());
return null;
}
}

// ===============================list=================================

/**
* 获取list缓存的内容
* @param key
* @param start
* @param end 0到-1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
log.error("lGet获取错误!key:{},start:{},end:{},错误信息:{}", key, start, end, e.getMessage());
return null;
}
}

/**
* 获取list缓存的长度
* @param key 键
* @return
*/
public Long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
log.error("lGetListSize获取错误!key:{},错误信息:{}", key, e.getMessage());
return null;
}
}

/**
* 通过索引 获取list中的值
* @param key
* @param index index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
log.error("lGetIndex获取错误!key:{},index:{},错误信息:{}", key, index, e.getMessage());
return null;
}
}

/**
* 将value写入list缓存
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
log.error("lSet写入错误!key:{},value:{},错误信息:{}", key, value, e.getMessage());
return false;
}
}

/**
* 将value写入list缓存并设置过期时间
* @param key
* @param value
* @param time 小于等于0则不设置过期
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
log.error("lSet写入错误!key:{},value:{},time:{},错误信息:{}", key, value, time, e.getMessage());
return false;
}
}

/**
* 将list写入缓存
* @param key
* @param value
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
log.error("lSet写入错误!key:{},value:{},错误信息:{}", key, value, e.getMessage());
return false;
}
}

/**
* 将list写入缓存并设置过期时间
* @param key
* @param value
* @param time 小于等于0则不设置过期
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
log.error("lSet写入错误!key:{},value:{},time:{},错误信息:{}", key, value, time, e.getMessage());
return false;
}
}

/**
* 根据索引修改list中的某条数据
* @param key
* @param index
* @param value
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
log.error("lUpdateIndex更新错误!key:{},index:{},value:{},错误信息:{}", key, index, value, e.getMessage());
return false;
}
}

/**
* 移除list中N个值为value的元素
* @param key
* @param count 移除数量
* @param value
* @return 移除成功的个数
*/
public Long lRemove(String key, long count, Object value) {
try {
return redisTemplate.opsForList().remove(key, count, value);
} catch (Exception e) {
log.error("lRemove移除错误!key:{},count:{},value:{},错误信息:{}", key, count, value, e.getMessage());
return null;
}
}

public void setString(String key, Object value, Long time, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}

/**
* 逻辑过期
* @param key
* @param value
* @param time 过期时间
* @param 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)));
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}

/**
* 取数据(缓存空值防止缓存穿透)
* @param keyPrefix key前缀
* @param id id
* @param type id类型
* @param dbFallback 缓存不存在,则通过该函数新建缓存
* @param time 写入缓存的过期时间
* @param unit 写入缓存的过期时间单位
* @param <R>
* @param <ID>
* @return
*/
public <R,ID> R getWithPassThrough(
String keyPrefix,
ID id,
Class<R> type,
Function<ID, R> dbFallback,
Long time,
TimeUnit unit
){
String key = keyPrefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(json)) {
return JSONUtil.toBean(json, type);
}
if (json != null) {
return null;
}
// id不存在,去数据库查询
R r = dbFallback.apply(id);
if (r == null) {
// 数据不存在,则存空值,防止缓存穿透
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, CACHE_UNIT);
return null;
}
// 数据存在,则写入缓存
this.setString(key, r, time, unit);
return r;
}

/**
* 取数据(逻辑过期防止缓存击穿)
* @param keyPrefix
* @param id
* @param type
* @param dbFallback
* @param time
* @param unit
* @param <R>
* @param <ID>
* @return
*/
public <R, ID> R getWithLogicalExpire(
String keyPrefix,
ID id,
Class<R> type,
Function<ID, R> dbFallback,
Long time,
TimeUnit unit
) {
String key = keyPrefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isBlank(json)) {
return null;
}
// 命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
if(expireTime.isAfter(LocalDateTime.now())) {
// 未过期,直接返回信息
return r;
}
// 缓存重建,获取互斥锁
String lockKey = LOCK_APP_KEY + id;
boolean isLock = tryLock(lockKey);
if (isLock){
// 成功获取锁,开启独立线程,实现缓存重建
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);
}
});
}
return r;
}

/**
* 取数据(互斥锁防止缓存击穿)
* @param keyPrefix
* @param id
* @param type
* @param dbFallback
* @param time
* @param unit
* @param <R>
* @param <ID>
* @return
*/
public <R, ID> R getWithMutex(
String keyPrefix,
ID id,
Class<R> type,
Function<ID, R> dbFallback,
Long time,
TimeUnit unit
) {
String key = keyPrefix + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopJson)) {
return JSONUtil.toBean(shopJson, type);
}
if (shopJson != null) {
return null;
}

// 实现缓存重建,获取互斥锁
String lockKey = LOCK_APP_KEY + id;
R r = null;
try {
boolean isLock = tryLock(lockKey);
if (!isLock) {
// 获取锁失败,休眠并重试
Thread.sleep(50);
return getWithMutex(keyPrefix, id, type, dbFallback, time, unit);
}
// 获取锁成功,根据id查询数据库
r = dbFallback.apply(id);
if (r == null) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 存在则写入redis
this.setString(key, r, time, unit);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
unlock(lockKey);
}
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);
}
}