redis

笔记来源:【黑马程序员Redis入门到实战教程,深度透析redis底层原理+redis分布式锁+企业解决方案+黑马点评实战项目】https://www.bilibili.com/video/BV1cr4y1671t?vd_source=4e5d5b47f9e9bcc068a572e3c71798a0

包含个人理解整理以及一些公众号文章内容会在后续附上链接

Redis实战 登录问题

为什么使用Redis存放 token:UserInfo ?

如果不使用Redis,我们的校验信息通过Session存放在服务器(比如Tomcat)中 但是如果两次访问被分配到了不同的服务器上 就需要session共享去保证两台服务器上都有Session

但是这样会造成服务器压力(每台上面都要存) Session拷贝还会有延迟 但是Redis集群能够实现高效的数据共享 所以把信息存在Redis是更优秀的解决方案,至于为什么能数据共享 会在后面学到

image.png

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获取验证码并校验
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);
}

继续优化

我们第一个拦截器只会拦截登陆请求,然后去查看token决定是否放行,但是 我们token里面已经存了用户信息,如果用户不存在,是不是就不用还查一次数据库,直接在redis就处理了,而且如果我们Token设定了一个比较大的时间,其实有时候用户已经不使用了,Token却迟迟不释放,如果有效时间太短,用户体验必然会很差,那我们可以在用户每一次发起请求之后都刷新一下Token的时间,为了实现这个功能,就需要加一层拦截器 拦截所有请求 完成两件事 1:判断用户是否存在 2:如果在 刷新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
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
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实战 缓存

为什么需要使用Redis作为缓存?

如果有某个数据项会频繁的被请求,如果所有数据请求都直接去访问数据库,会造成特别多的开销

那既然要提到缓存

我们就需要保证数据库与缓存的一致性

思考

  • 删除缓存还是更新缓存?
    • 更新缓存:每次更新数据库都更新缓存,无效写操作较多
    • 删除缓存:更新数据库时让缓存失效,查询时再更新缓存
  • 如何保证缓存与数据库的操作的同时成功或失败?
    • 单体系统,将缓存与数据库操作放在一个事务
    • 分布式系统,利用TCC等分布式事务方案 (这是啥 没学过

image.png

保持一致性问题上

双写问题

应该先操作数据库 再删除缓存

image.png

缓存穿透问题

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。如果同一时段大量这样的请求涌入,就会很糟了

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

布隆过滤器

这里来介绍一下布隆过滤器:

应用场景:

想要判断 某个元素 是否已经存在

第一反应可以拿一个哈希表存储元素,但是当数据量大的时候,需要很大的存储空间,而布隆过滤器可以牺牲很小的空间就实现这个功能

100亿个100byte的记录,内存占用也不超过30G

且运行在内存,访问速度快

原理

当传入一个字符串S

我们通过Hash函数将其映射为数字

比如如果有三个Hash函数

S会被映射出三个数字2 5 7 那我们就把对应下标2 5 7置为1

这样当下一次S再进来的时候,会发现对应的2 5 7 都是1 就说明S已经来过了

image.png

因为存在hash冲突 所以会存在误报的情况 也就是明明这个元素不存在,却返回存在的结果

这是因为大量数据输入后 产生Hash冲突 也就是不同的元素 通过Hash得到了一样的值

可能2 5 7 已经被置为1

那么S进来 就会报告S已经存在 实则并没有 就发生了误报

误报率如何计算:

n: 插入的元素数量。

m: 布隆过滤器的比特数组长度。

k: 哈希函数的数量。

p: 误报率(False Positive Probability)

每次插入一个元素时,会有 k 个位置被设置为1,每个位置的概率是均匀分布的

一个位置在一次hash中被设置为1的概率是:1/m

没有被设置为1 1-1/m

k个Hash产生K位 没有被设置为1: 1-1/m的k次方

插入n个元素之后 没有被设置为1: 1-1/m的nk次方

插入n个元素之后 被设置为1: 1 - (1-1/m的nk次方)

S现在进入布隆过滤器 误报率就为 (1 - (1-1/m的nk次方))整体的k次方

空值回写:

image.png

缓存穿透的解决方案有哪些?

  • 缓存null值
  • 布隆过滤
  • 增强id的复杂度,避免被猜测id规律
  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限流

利用互斥锁解决缓存击穿问题

因为锁能实现互斥性。假设线程过来,只能一个人一个人的来访问数据库,从而避免对于数据库访问压力过大,但这也会影响查询的性能,因为此时会让查询的性能从并行变成了串行,我们可以采用tryLock方法 + double check来解决这样的问题。

缓存雪崩问题

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

解决方案:

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