https://www.bilibili.com/video/BV1cr4y1671t?p=2&vd_source=414a65d222d782153399fbb03cc8be4b

redis是一个键值型的数据库

image.png
image.png
nosql不严格要求约束
image.png
nosql语句的查询方式 不统一 没有复杂的语法

差异4 事务
事务要满足 原子性 一致性 隔离性所有关系型数据库都是满足ACID的
Nosql一般无事务 只有基本的一致性 安全性要求高的应该选择关系型数据库
image.png
扩展性 SQL垂直导致不好拆分库表

认识redis

image.png
image.png

一、短信登陆实战

1、基于Session实现短信登陆流程

image.png

1.session方法生成手机号验证码
  1. @Override
  2. public Result sendCode(String phone, HttpSession session) {
  3. // 1校验手机号
  4. if (RegexUtils.isPhoneInvalid(phone)) {
  5. //2.如果不符合返回错误信息
  6. return Result.fail("手机号格式错误");
  7. }
  8. //3. 符合生成验证码
  9. String code = RandomUtil.randomNumbers(6);
  10. //4.保存验证码到session
  11. session.setAttribute("code", code);
  12. //5.发送验证码
  13. log.debug("发送短信 验证码成功,验证码:{}",code);
  14. //返回OK
  15. return Result.ok();
  16. }

2.session
  1. package com.hmdp.utils;
  2. import cn.hutool.core.bean.BeanUtil;
  3. import cn.hutool.core.util.StrUtil;
  4. import com.hmdp.dto.UserDTO;
  5. import com.hmdp.entity.User;
  6. import org.springframework.data.redis.core.StringRedisTemplate;
  7. import org.springframework.web.servlet.HandlerInterceptor;
  8. import org.springframework.web.servlet.ModelAndView;
  9. import javax.annotation.Resource;
  10. import javax.servlet.http.HttpServletRequest;
  11. import javax.servlet.http.HttpServletResponse;
  12. import javax.servlet.http.HttpSession;
  13. import java.util.Map;
  14. import java.util.concurrent.TimeUnit;
  15. /**
  16. * @Description:拦截器,用于拦截未登录用户的请求,并通过拦截器刷新用户的token有效期,此拦截器通过MvcConfig加载到spring并设置拦截请求地址
  17. * @Auther:$
  18. */
  19. public class LoginInterceptor implements HandlerInterceptor {
  20. private StringRedisTemplate stringRedisTemplate;
  21. public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
  22. this.stringRedisTemplate = stringRedisTemplate;
  23. }
  24. @Override
  25. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  26. //1.获取请求头中的token
  27. //HttpSession session = request.getSession();
  28. String token = request.getHeader("authorization");
  29. if (StrUtil.isBlank(token)) {
  30. //获取请求头 如果没有就false
  31. response.setStatus(401);
  32. return false;
  33. }
  34. //2.获取session中的用户 session中的user是UserDTO类型的,所以拦截器保存当ThreadLocal也需要是UserDTO类型的
  35. //Object user = session.getAttribute("user");
  36. //2.基于TOKEN获取redis中的用户
  37. String key = RedisConstants.LOGIN_USER_KEY+token;//取出token
  38. Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
  39. .entries(key);//通过token取出用户
  40. //3.判断用户是否存在
  41. if (userMap.isEmpty()) {
  42. //4.不存在拦截
  43. response.setStatus(401);
  44. return false;
  45. }
  46. //5. 将查询到的Hash数据转为UserDTO对象
  47. UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
  48. //6.存在用户,保存用户信息到ThreadLocal
  49. UserHolder.saveUser((UserDTO) userDTO);
  50. //7.通过token,刷新token有效期
  51. stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS);
  52. //8.放行
  53. return true;
  54. }
  55. @Override
  56. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
  57. //移除用户
  58. UserHolder.removeUser();
  59. }
  60. }

二、商户查询缓存

当用户去查询商户信息,如果商户信息是比较热门的那么会经常去数据库中查询这个信息,会对数据库有非常大的损耗,所以在客户端和数据库之间可以加一层缓存,把用户访问过的信息存到redis缓存中,这样当用户再去查询相同内容的时候就不必从数据库中重新查找而是直接从缓存中拿取,非常方便

缓存流程

image.png

  1. public class ShopServiceImpl extends ServiceImpl<ShopMapper,Shop> implements IShopService{
  2. @Resource
  3. private StringRedisTemplate stringRedisTemplate;
  4. @Override
  5. public Result queryById(Long id){
  6. //1.从redis查询商铺缓存
  7. String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:"+id);
  8. //2.判断是否存在
  9. if(StrUtil.isNotBlank(shopJson)){
  10. //3. 存在直接返回
  11. Shop shop= JSONUtil.toBean(ShopJson,Shop.class);
  12. return Result.ok(shop);
  13. }
  14. //4.不存在根据id查询数据库
  15. Shop shop = getById(id);
  16. //5.不存在返回错误
  17. if(shop ==null){
  18. return Result.fail("此店铺不存在");
  19. }
  20. //6.存在,写入redis
  21. stringRedisTemplate.opsForValue().set("cache:shop:"+id,JSONUtil.toJsonStr(shop));
  22. //7.返回
  23. return Result.ok(shop);
  24. }
  25. }

现在又有了新的问题,虽然说是能够将缓存保存到redis中,查询更快,但是可能会出现,我修改了店铺信息,但是redis中存储的信息依旧是之前的信息,导致数据不一致,redis和数据库中存储的信息不一样。
所以对缓存的更新有更高的要求:

缓存更新策略

image.png
业务场景: 一致性区别
低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存

image.png

image.png
image.png
先删除缓存,再操作数据库:
在线程执行的过程中另外一个线程来了
线程1去删除了缓存,此时缓存中没有了 ,但是当线程1还没更新数据库时,线程2来了,线程2去查缓存发现没有就去查数据库,此时线程2去查的值是之前的值,因为新的值线程1还没存到数据库中,此时线程2去查数据库查的是之前的值,并写入了缓存,缓存中还是旧的值,线程2完成了之后线程1该开始更新数据库了,结果就是线程1更新完数据库了 但是因为线程2的出现导致 缓存中还是旧的值,而且缓存和数据库的值未统一。这就是线程安全产生的问题。发生概率高。
先操作数据库再删缓存:
容易出错情况:恰好缓存失效了,线程1去查,缓存中没有,未命中 去数据库里查了,然后写入缓存,但是正在此时,线程2更新数据库,然后再删缓存,然后线程1去写入缓存,但是因为线程1查的是旧数据,所以写入的也是旧数据。
这种情况概率很低。 而且可以在修改之前让缓存失效时间增加避免此情况
image.png

实现商铺缓存和数据库一致

  1. public class ShopServiceImpl extends ServiceImpl<ShopMapper,Shop> implements IShopService{
  2. @Resource
  3. private StringRedisTemplate stringRedisTemplate;
  4. @Transactional
  5. public Result update(Shop shop){
  6. Long id = shop.getId();
  7. if(id == null){
  8. return Result.fail("店铺id不能为空");
  9. }
  10. //1.更新数据库
  11. updateById(shop);
  12. //2.删除缓存
  13. stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
  14. return Result.ok();
  15. }}

缓存穿透

image.png
企业遇到这种情况有的就会 即时是数据库中没有这个东西也会缓存到redis中 尽管值是null,但是有额外的内存消耗,也可以设置ttl

布隆过滤是一个算法,先去布隆过滤去查这个数据存不存在,如果不存在就直接拒绝,如果存在就去redis里查询,布隆过滤器怎么知道存在?把数据基于Hash算法计算成hash值,将hash值转换为二进制位,去保存,再判断是否存在 并不是百分比准确,不存在是真不存在 ,说存在不一定存在,有一点穿透风险。

解决缓存穿透

image.png
image.png
解决缓存穿透,1是将不存在的商铺以空值的形式写入Redis 2是请求的时候命中了要判断一下是不是空值,避免返回一个null 如果是空的就直接结束

  1. public class ShopServiceImpl extends ServiceImpl<ShopMapper,Shop> implements IShopService{
  2. @Resource
  3. private StringRedisTemplate stringRedisTemplate;
  4. @Override
  5. public Result queryById(Long id){
  6. //1.从redis查询商铺缓存
  7. String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:"+id);
  8. //2.判断是否存在
  9. if(StrUtil.isNotBlank(shopJson)){
  10. //3. 存在直接返回
  11. Shop shop= JSONUtil.toBean(ShopJson,Shop.class);
  12. return Result.ok(shop);
  13. }
  14. //判断命中的是否是空值
  15. if(shopJson !=null){
  16. //返回一个错误信息
  17. return Result.fail("店铺信息不存在");
  18. }
  19. //4.不存在根据id查询数据库
  20. Shop shop = getById(id);
  21. //5.不存在返回错误
  22. if(shop ==null){
  23. //将空值写入redis
  24. stringRedisTemplate.opsForValue().set(key,"",2,TimeUtil.MINUTES);
  25. return Result.fail("此店铺不存在");
  26. }
  27. //6.存在,写入redis
  28. stringRedisTemplate.opsForValue().set("cache:shop:"+id,JSONUtil.toJsonStr(shop));
  29. //7.返回
  30. return Result.ok(shop);
  31. }
  32. }

image.png

缓存雪崩

image.png
就是短时间内大量的key同时失效,或者Redis宕机后 导致大量请求 请求到数据库,带来很大的压力
解决:给不太的key的TTL添加随机值
利用Redis集群提高服务的可用性
给缓存业务添加降级限流策略:让请求失败拒绝服务,而不是让请求压到数据库中去,保护数据库牺牲服务
给业务添加多级缓存:多添加几级缓存,可以在nginx中添加缓存,再去redis
以上的问题可以通过springcloud解决 记得看!

缓存击穿

部分key过期导致的
缓存击穿问题也叫热点key问题,就是被高并发访问并且缓存重建业务较为复杂的key突然消失了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
就是有一些复杂的业务,访问的时间很长,导致缓存到redis中的时间很久,导致大量的访问请求到了数据库当中,导致数据库崩塌。
image.png
两种解决方案:互斥锁,逻辑过期
image.png
互斥锁:一个线程进行查询其他等待,会导致效率低
image.png
逻辑过期:因为短期内大量的key过期,所以不设置过期时间了,直接逻辑过期
,在字段中加上一个expire 注意 这个不是ttl只是字段,针对热点key,

image.png

基于互斥锁方式解决缓存击穿问题

image.png

  1. public class ShopServiceImpl extends ServiceImpl<ShopMapper,Shop> implements IShopService{
  2. @Resource
  3. private StringRedisTemplate stringRedisTemplate;
  4. public Result queryById(Long id){
  5. //缓存穿透
  6. //Shop shop = queryWithPassThrough(id);
  7. //互斥锁解决缓存击穿
  8. Shop shop = queryWithMutex(id);
  9. //返回
  10. return Result.ok(shop);
  11. }
  12. public Shop queryWithMutex(Long id){
  13. //1.从redis查询商铺缓存
  14. String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:"+id);
  15. //2.判断是否存在
  16. if(StrUtil.isNotBlank(shopJson)){
  17. //3. 存在直接返回
  18. return JSONUtil.toBean(ShopJson,Shop.class) ;
  19. }
  20. //判断命中是否为空值
  21. if(shopJson!=null){
  22. //返回一个错误信息
  23. return null;
  24. }
  25. //实现缓存重建
  26. //4.1获取互斥锁
  27. String lockKey = "lock:shop:"+id;
  28. boolean isLock = tryLock(lockKey);
  29. //4.2判断是否获取成功
  30. if(!isLock){
  31. //4.3失败,则休眠并重试
  32. Thread.sleep(50);
  33. //重试 重新去执行查询操作
  34. return queryWithMutex(id);
  35. }
  36. //4.4成功 根据id查询数据库
  37. Shop shop = getById(id);
  38. ///未命中 情况 尝试使用互斥锁去解决
  39. //5.不存在返回错误
  40. if(shop ==null){
  41. //将空值写入redis
  42. stringRedisTemplate.opsForValue().set(key,"",5,TimeUtil.MINUTES;
  43. //返回错误信息
  44. return null;
  45. }
  46. //6.存在,写入redis
  47. stringRedisTemplate.opsForValue().set("cache:shop:"+id,JSONUtil.toJsonStr(shop));
  48. //7.释放互斥锁
  49. unlock(lockKey);
  50. //8.返回
  51. return shop;
  52. }
  53. public Shop queryWithPassThrough(Long id){
  54. //1.从redis查询商铺缓存
  55. String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:"+id);
  56. //2.判断是否存在
  57. if(StrUtil.isNotBlank(shopJson)){
  58. //3. 存在直接返回
  59. return JSONUtil.toBean(ShopJson,Shop.class) ;
  60. }
  61. //判断命中是否为空值
  62. if(shopJson!=null){
  63. //返回一个错误信息
  64. return null;
  65. }
  66. //4.不存在根据id查询数据库
  67. Shop shop = getById(id);
  68. //5.不存在返回错误
  69. if(shop ==null){
  70. //将空值写入redis
  71. stringRedisTemplate.opsForValue().set(key,"",5,TimeUtil.MINUTES;
  72. //返回错误信息
  73. return null;
  74. }
  75. //6.存在,写入redis
  76. stringRedisTemplate.opsForValue().set("cache:shop:"+id,JSONUtil.toJsonStr(shop));
  77. //7.返回
  78. return shop;
  79. }
  80. //开锁
  81. private boolean tryLock(String key){
  82. Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key,"1",10,TimeUnit.SECONDS);//设置锁 和锁过期时间
  83. return Boolean.isTrue(falg);//由于方法是基本数据类型boolean,如果直接返回这个flag,会做拆箱的,可能会有空指针,所以用方法
  84. }
  85. //关锁
  86. private void unlock(String key){
  87. stringRedisTemplate.delete(key);
  88. }