redis缓存

目录

1、使用redis解决session共享问题

2、redis缓存

对查找店铺功能加入缓存

缓存更新策略

内存淘汰

超时剔除

主动更新

 对修改店铺功能的缓存加入主动更新

缓存穿透

缓存空数据

 布隆过滤

 缓存雪崩

缓存击穿

互斥锁 

逻辑过期

 缓存工具封装


1、使用redis解决session共享问题

使用session时,容易在集群时出现问题

那么如何解决session的不共享问题:使用redis来记录登陆后的用户信息。因为redis和session都是存储在内存,查询速度快,并且redis的信息,是所有tomcat共享的。

 业务逻辑:用户使用手机号码+验证码进行登录;

1、redis存储数据:1、 以手机号码为key,验证码为value 存储验证码  2、以一个随机的token为key,存储用户信息

2、用户生成验证码请求:用户输入手机号,点击发送验证码,后端生成一个验证码,以手机号为key,验证码为value,存入redis。

3、用户登录请求:用户输入手机号,输入验证码  点击登录,即可发送请求。后端 根据手机号获取验证码,和用户输入验证码进行比对。 比对成功,从mysql的tb_user中根据手机号码获取用户信息,查找成功,将user信息存入redis,失败,根据手机号码创建新用户,将用户信息存入redis。存入时,以随机的token为key,user对象为value,将token值返回前端。

4、用户登录状态验证:从redis根据token获取对象,判断用户是否存在。

CREATE TABLE `tb_user`  (
  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
  `phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号码',
  `password` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '密码,加密存储',
  `nick_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '昵称,默认是用户id',
  `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '人物头像',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `uniqe_key_phone`(`phone`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1010 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
@Data
public class User {
    private int id;
    private  String  phone;
    private String  password;
    private String  nick_name;
    private  String icon;
    private String create_time;
    private String update_time;
}

1、生成验证码

package com.example.demo.Controller;


import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.LineCaptcha;
import cn.hutool.core.util.RandomUtil;
import com.example.demo.Model.Result;
import com.sun.org.apache.regexp.internal.RE;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * 获取验证码图片
 */

@Controller
@ResponseBody
@RequestMapping("/app")
public class SendCodeController {
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @PostMapping("/getcode")
    public Result sendCode( String tel, HttpServletRequest request, HttpServletResponse response) throws IOException {
        //1、检验手机号码格式
        if (!checkTel(tel)) return Result.fail("手机号码错误");
        //2、生成验证码图片
        //生成 带有线条的验证码图片
        LineCaptcha circleCaptcha = CaptchaUtil.createLineCaptcha(100/*宽*/, 40/*高*/, 4/*验证码显示几个数据*/, 50/*有几个线段*/);
        //告诉浏览器输出内容为jpeg类型的图片
        response.setContentType("image/png");
        //禁止浏览器缓存
        response.setHeader("Pragma", "No-cache");
        //输出图形 内的数字
        //    System.out.println(circleCaptcha.getCode());

        //3、将 验证码数据 和 手机号存入redis
        stringRedisTemplate.opsForValue().set("user:login:tel"+tel, circleCaptcha.getCode());
        //设置有效期  两分钟
        //redis的有效期指的是  在tel存入后,两分钟后就会过期,不论期间是否访问 tel, 有效期都不会改变
        //但是重新set之后  有效期要重新设置
        stringRedisTemplate.expire("user:login:tel"+tel, 2, TimeUnit.MINUTES);

        //图片写出
        circleCaptcha.write(response.getOutputStream());
        return Result.ok("验证码发送成功");

    }
    public boolean checkTel(String tel) {
        if (tel.length() != 11) return false;
        for (int i = 0; i < 11; i++) {
            if (!(tel.charAt(i) <= '9' && tel.charAt(i) >= '0'))
                return false;
        }
        return true;
    }
}

在刷新之后,重置value值,并重新设置有效值

 

 2、用户登录

package com.example.demo.Controller;

import cn.hutool.core.util.RandomUtil;
import com.example.demo.Mapper.SaveUserByTelMapper;
import com.example.demo.Mapper.getUserByTelMapper;
import com.example.demo.Model.Result;
import com.example.demo.Model.User;
import com.sun.org.apache.regexp.internal.RE;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 用户登录请求处理
 */

@Controller
@ResponseBody
@RequestMapping("/app")
public class UserController {

    @Autowired
    com.example.demo.Mapper.getUserByTelMapper getUserByTelMapper;
    @Autowired
    SaveUserByTelMapper saveUserByTelMapper;
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @PostMapping("/login")
    public Result user(String tel,String code) {
        System.out.println(code);
        System.out.println(tel);
        //1、用户手机号码验证
        if (!checkTel(tel)) return Result.fail("手机号码错误");
        //2、校验验证码是否正确
        String cache_code = stringRedisTemplate.opsForValue().get("user:login:tel" + tel);
        if (cache_code == null) {//验证码获取不到 (过期)
            return Result.fail("验证码过期");
        }
        System.out.println(cache_code);
        if (!cache_code.equals(code))
            return Result.fail("验证码错误");
        //3、验证码校验成功了  从mysql数据库根据手机号码 查找用户  查找不到,就根据手机号码创建用户
        User user = getUserByTelMapper.get(tel);
        if (user == null) {
            //根据手机号创建用户  用户的其他信息为随机
            user = new User();
            user.setPhone(tel);
            user.setNick_name("user_" + RandomUtil.randomString(10));

            //将用户信息存入mysql
            saveUserByTelMapper.save(user);
        }
        //4、此时登录成功,将用户信息存入redis
        Map<String, String> map = new HashMap<>();
        map.put("phone", user.getPhone());
        map.put("icon", user.getIcon());
        map.put("nick_name", user.getNick_name());
        UUID uuid = UUID.randomUUID();
        stringRedisTemplate.opsForHash().putAll("user:login" + uuid, map);
        //设置有效期  30分钟

        //这里的有效期  在 此时计时  30分钟; 这个逻辑在用户正常使用时,30分钟也会过期  因此在登录验证时 要重新更新有效期
        stringRedisTemplate.expire("user:login" + uuid, 30, TimeUnit.MINUTES);
        return Result.ok(uuid);
    }
    public boolean checkTel(String tel) {
        if (tel.length() != 11) return false;
        for (int i = 0; i < 11; i++) {
            if (!(tel.charAt(i) <= '9' && tel.charAt(i) >= '0'))
                return false;
        }
        return true;
    }
}

 

 登录成功  返回token值

redis中存储如下:

 mysql新增用户记录:

 3、拦截器


public class LoginInterceptor implements HandlerInterceptor {
//
//    @Autowired
//    RedisTemplate redisTemplate;

    //不能通过   @Autowired注入  因为LoginInterceptor 这个类 没有被Spring管理
    //因此使用构造函数构造  也就是调用 LoginInterceptor对象的类 去创建 redisTemplate对象
    //即 LoginConfig类
    private StringRedisTemplate redisTemplate;

    public LoginInterceptor(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        //1、从redis获取登录的用户  来检验用户登录状态
        String token = request.getHeader("token");//从请求头获取 token值
        if (token == null)
            return false;
        //2、从redis获取不到登录的用户
        //redisTemplate.opsForHash().entries("login:user" + token)  获取token对应的map结构
        Map<Object, Object> map = redisTemplate.opsForHash().entries("user:login" + token);
        if (map.isEmpty()) //entries方法 在获取map时,如果获取不到,会返回一个空的map
            return false;
        System.out.println(map.get("phone"));
        //3、获取到了这个用户  那么就去刷新有效期 不要 只在用户登录请求时设置有效期
        redisTemplate.expire("user:login" + token, 30, TimeUnit.MINUTES);
        return true;
    }
}

@Configuration
public class LoginConfig implements WebMvcConfigurer {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override

    //无论用户是否登录  都会放行 getcode 和 login接口
    //在用户没有登陆时,拦截text接口     在用户登陆时,放行text接口
//text接口会进入LoginInterceptor类,进行用户登录校验,校验用户登录后才会放行
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor(redisTemplate)).addPathPatterns("/**")
                .excludePathPatterns("/app/getcode")
                .excludePathPatterns("/app/login");
    }
}

 除了/app/getcode和/app/login不会做任何处理之外,其余接口会进入LoginInterceptor,进行用户登录的校验,一旦校验用户已经登录成功,那么就刷新有效期,才可以放行请求。

LoginInterceptor可以放行的除了登录接口外,还有的就是获取了登录权限的其余接口

但是,在某些业务,比如用户访问商品列表,是不需要获取登录权限的,用户可以在不登陆时查看商品信息。但是用户如果此时是登录的,也应该去刷新有效期。

就是说,基于不登陆时可以访问,这个接口就不能被拦截进入LoginInterceptor进行登陆状态验证,但是基于如果此时是登录的,也应该去刷新有效期,他又应该进入LoginInterceptor进行登陆状态验证,验证后进行有效期刷新。

也就是说需要再加入一个拦截器,拦截所有接口,让所有接口进入RefreshInterceptor,如果登录了,就去刷新有效期。在LoginInterceptor去判断那些需要登陆的接口的登陆状态


/**
 * 定制拦截规则  不管用户是否登录  都放行
 */
public class RefreshInterceptor implements HandlerInterceptor {
    private StringRedisTemplate redisTemplate;

    public RefreshInterceptor(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("RefreshInterceptor接口调用");
        //1、从redis获取登录的用户  来检验用户登录状态
        String token = request.getHeader("token");//从请求头获取 token值
        if (token == null)
            return true;
        //2、从redis获取不到登录的用户
        //redisTemplate.opsForHash().entries("login:user" + token)  获取token对应的map结构
        Map<Object, Object> map = redisTemplate.opsForHash().entries("user:login" + token);
        if (map.isEmpty()) //entries方法 在获取map时,如果获取不到,会返回一个空的map
            return true;
        User user = new User();
        user.setPhone((String) map.get("phone"));
        user.setIcon((String) map.get("icon"));
        user.setNick_name((String) map.get("nick_name"));

        //3、获取到了这个用户  那么就去刷新有效期 不要 只在用户登录请求时设置有效期
        redisTemplate.expire("user:login" + token, 30, TimeUnit.MINUTES);

        //4、在RefreshInterceptor 是放行所有的接口 让所有接口都可以实现 在用户登陆时刷新有效期
        //在LoginInterceptor 是 放行检测登录状态成功之后的接口

        //那么这里 通过ThreadLocal去存储已经登录的用户信息,避免在LoginInterceptor 二次判断

        UserHolder.set(user);
        return true;

    }
}

public class LoginInterceptor implements HandlerInterceptor {
//
//    @Autowired
//    RedisTemplate redisTemplate;


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        System.out.println("LoginInterceptor接口调用");

        //没有用户登录
        if (UserHolder.get() == null) {
            return false;
        }
        return true;

    }
}


@Configuration
public class LoginConfig implements WebMvcConfigurer {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override

    //无论用户是否登录  都会放行 getcode 和 login接口
    //拦截 getcode 和 login接口 外的 所有接口 让所有路径进入LoginInterceptor制定的规则

    //在用户没有登陆时,拦截text接口     在用户登陆时,放行text接口
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**")
                .excludePathPatterns("/app/getcode")
                .excludePathPatterns("/app/login").order(1);

        //拦截所有接口 让所有路径进入RefreshInterceptor制定的规则
        registry.addInterceptor(new RefreshInterceptor(redisTemplate)).addPathPatterns("/**").order(0);

        //order约小 先执行  让RefreshInterceptor先执行
    }


}

public class UserHolder {
    private static ThreadLocal<User> t = new ThreadLocal<>();

    public static void set(User user) {
        t.set(user);
    }

    public static User get() {
        return t.get();
    }
}

这时,在没有登录下,去访问text接口,会进入RefreshInterceptor,检验没有登录,放行请求,进入LoginInterceptor,检验没有登录,从而请求被拦截

 在登录状态下,去访问text接口,会进入RefreshInterceptor,检验已经登录,刷新有效期,放行请求,进入LoginInterceptor,检验已经登录,从而请求被放行

 如果想要让text接口在没有登陆时可以放行,就需要设置在LoginInterceptor时不拦截此接口,从而不进入LoginInterceptor进行登录验证。

2、redis缓存

缓存是实现数据存储的位置,cpu的计算和读写速度都很快,为了平衡和内存之间的速度,引入缓存。cpu从缓存读取数据,提高运行速度。

redis是可以充当缓存的,因为redis是存于内存的,读写速度很快,请求来到服务器,服务器从redis读取数据,就返回数据,可以降低响应时间,提高读写速度,降低后端负载。

当请求发出之后,先去redis查找,redis命中直接返回结果,否则从数据库查找,数据库查找到了之后,将数据加入redis,然后返回结果

对查找店铺功能加入缓存

第一次调用,在数据库查找,并且加入缓存

 

 第二次查找,直接在缓存查

缓存更新策略

缓存在提高响应速度的同时,会引入问题:缓存和数据库内容不一致,比如数据库内容修改,缓存数据没有修改;缓存内容修改,数据库内容没修改。

内存淘汰

机制:redis在内存不足时,会自动淘汰部分数据。这些淘汰的数据在下一次请求访问时,就会进入mysql查找,将从数据库的新数据加入缓存。

好处:不用自己实现

坏处:淘汰的数据不一定是我们想要删除的数据,不可控

超时剔除

机制:给缓存数据加入TTL有效期,过期之后会自动删除

好处:数据在过期之后被删除,下一次请求到来,就要到数据库查询,查找的数据加入缓存,缓存里就会是新数据。

坏处:在TTL有效期内查询数据,都是从缓存中读取,一旦数据库修改数据,缓存没有同步修改,得到的仍然是旧数据。

主动更新

   在业务逻辑上处理:更新数据库的同时,更新缓存

实现方式:

1、由更新数据库的调用者,在更新数据库的同时,更新缓存

2、将缓存和数据库整合成为一个服务,由服务来维护一致性,调用者调用该服务即可实现缓存一致性。

3、调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库。

 考虑三个点:

1、删除缓存还是更新缓存:如果更新缓存,数据库如果对同一行数据修改100次,此时缓存也要更新100次,但其实最终数据也是以第100次为准; 删除缓存,数据库修改100次,最终在请求时,缓存只需要从数据库刷新最终数据。  一般选取删除缓存方案

2、更新数据库和删除缓存两个操作要是原子性的:单体系统:两个方法在同一个事务中。分布式系统:分布式事务的解决方案

3、线程安全问题:在多线程并发情况下,是先删除缓存还是先修改数据库

 对修改店铺功能的缓存加入主动更新

先修改数据库,后删除缓存


@Service
public class UpdateShopByIdService {

    @Autowired
    UpdateShopByIdMapper updateShopByIdMapper;
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    //保证两个操作是原子性的  加入事务
    @Transactional
    public Result update(Shop shop) {
        if (shop == null || shop.getId() == null)
            return Result.fail("数据错误");
        //1、先修改数据库
        updateShopByIdMapper.update(shop);
        //2、删除缓存
        stringRedisTemplate.delete("shop" + shop.getId());
        return Result.ok("修改成功");
    }
}

缓存穿透

指的是一个数据,在缓存中找不到,在数据库中也找不到。从而在并发请求时,所有的请求都会来到数据库。

解决办法:缓存空数据  布隆过滤

缓存空数据

缓存空数据是指:比如查找id为3的店铺,在缓存中找不到,在数据库中也找不到,这时在缓存放置 key为“shop3”,value为空的数据,下一次请求查找id为3的店铺,在缓存中就获取到了这个值,只不过内容为空而已

优点:实现简单。 缺点:1、发送多个不存在的id时,会存储多个垃圾数据。2、在并发时可能会存在不一致性。解决办法:设置有效期,清楚垃圾数据

 

 布隆过滤

它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

 

 使用缓存空数据,解决缓存穿透


@Service
public class GetShopByIdService {

    @Autowired
    com.example.demo.Mapper.getShopByIdMapper getShopByIdMapper;
    @Autowired
    StringRedisTemplate redisTemplate;

           public Result get(Integer id) {
            //1、现在缓存查找数据
            String str = redisTemplate.opsForValue().get("shop" + id);
            // 1.1 在缓存查找到了数据 
            if (StrUtil.isNotBlank(str)) {
//isNotBlank(str) 在str是 ”“,null,"\t\n" 是返回false  在”abc“这种正常字符串 返回true
                Shop shop = JSONUtil.toBean(str, Shop.class);//将 字符串转为 shop对象
                return Result.ok(shop);

            }
            //redis没有命中一个合法的字符串   判断是否命中了空值  如果是空值 就应该去返回
            if (Objects.equals(str, "")) {
                return Result.fail("商品不存在");
            }
            //2、在缓存没有查找到数据  在数据库查找
            Shop shop = getShopByIdMapper.get(id);
            //2.1 在数据库没有找到数据
            if (shop == null) {
                //这里 向缓存存入空数据
                redisTemplate.opsForValue().set("shop" + id, JSONUtil.toJsonStr(""), 30, TimeUnit.MINUTES);
                return Result.fail("商品不存在");
            }
            //2.2 在数据库中找到了  将这个数据加入缓存
            redisTemplate.opsForValue().set("shop" + id, JSONUtil.toJsonStr(shop), 30, TimeUnit.MINUTES);
            return Result.ok(shop);

        }

}

 缓存雪崩

在同一时间段大量的缓存失效或者redis宕机,导致大量的请求来到数据库,造成巨大压力

解决办法:

1、设置不同的有效期,让缓存不至于同时大量失效

2、设置redis集群,来实现redis的高可用性

3、给缓存业务加入降级:在redis发生问题时,直接返回服务不可用,不至于让请求来到数据库

4、添加多级缓存:比如在浏览器加入缓存,redis出现问题之后,就可以从浏览器缓存获取数据

缓存击穿

也就是热点k问题:一个被高并发访问,并且重建困难的 缓存失效了,数据库就会在瞬间接收到巨大的请求

比如秒杀,多个用户秒杀商品,突然缓存失效了,那么数据库就会在瞬间接收到巨大的请求

 解决办法:互斥锁  逻辑过期

互斥锁 

多个线程并发,只会有一个线程可以查询数据库,获取数据,并加入缓存

 出现的问题就是:如果这个查询数据库的时间过长,其余线程就只能不断循环,等到查询完成之后,写入缓存,才会命中缓存,返回结果。这个等待时间之内,是无法返回结果的。引入了逻辑过期。

逻辑过期

不给缓存设置有效期,而是设置一个字段,表示它的结束时间,在业务逻辑上进行判断是否过期。

这样,缓存是不会找不到的。

也就是线程本身直接返回缓存旧数据,在发现数据过期之后,开启一个线程完成缓存更新,

之后,其余线程从缓存拿到的就是新数据。

 1、使用互斥锁,解决查询店铺信息的缓存击穿问题

这里不能使用synchronized锁,因为它是自旋锁,而我们现在想要的是等待后自旋

这里除了自己实现锁外,可以借助redis的setnx命令:存在时不可修改,不存在时才会被创建

    public Result throwSolveByMudex(Integer id) {
        //1、基于缓存查找数据
        String str = redisTemplate.opsForValue().get("shop" + id);
        // 1.1 在缓存查找到了数据
//   //isBlank 方法会先将字符串去除头尾空格后再进行判断。如果字符串为 null 或者去除头尾空格后的长度为0,则返回 true;否则返回 false。
        if (StrUtil.isNotBlank(str)) {
            Shop shop = JSONUtil.toBean(str, Shop.class);//将 字符串转为 shop对象
            return Result.ok(shop);
        }
        // 1.2  商品存入的是 空值  如果 str 是 "" 说明是存入的不存在的数据
        if ("".equals(str)) {
            return Result.fail("商品不存在");
        }

        //2、在缓存没有查找到数据
        //2.1 获取互斥锁
        boolean islock = trylock("lock" + id);
        //2.2 竞争互斥锁失败 休眠一段时间之后  重新执行业务逻辑
        Shop shop = null;
        try {
            if (!islock) {

                Thread.sleep(60);
                return throwSolveByMudex(id);
            }
            //2.3 竞争互斥锁  获取成功
            //2.3.1 考虑并发操作 :可能获取锁之后,缓存其实已经刷新了
            str = redisTemplate.opsForValue().get("shop" + id);

            if (StrUtil.isNotBlank(str)) {
                 shop = JSONUtil.toBean(str, Shop.class);//将 字符串转为 shop对象
                return Result.ok(shop);
            }
            if ("".equals(str)) {
                return Result.fail("商品不存在");
            }

            //2.3.2 这时 缓存没有被刷新 且拿到了互斥锁 就去数据库操作
            System.out.println("获取锁成功");
            //在数据库查找
            shop = getShopByIdMapper.get(id);
            //加入等待 模拟 数据库查询慢的情况
            Thread.sleep(200);
            // 在数据库没有找到数据
            if (shop == null) {
                //这里 向缓存存入空数据
                redisTemplate.opsForValue().set("shop" + id, JSONUtil.toJsonStr(""), 30, TimeUnit.MINUTES);
                return Result.fail("商品不存在");
            }
            // 在数据库中找到了  将这个数据加入缓存
            redisTemplate.opsForValue().set("shop" + id, JSONUtil.toJsonStr(shop), 30, TimeUnit.MINUTES);

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放锁
            unlock("lock" + id);
        }
        return Result.ok(shop);

    }

 在1000个并发线程里,有两个线程查询了数据库,其他线程都是在缓存获取了数据

 使用逻辑过期时间来解决

1、封装新的对象

@Data
public class ShopWithOutTime {
    private LocalDateTime time;
    private Object object;
}

可以创建此对象,设置过期时间和内部object对象

2、使用单元测试,在redis存入数据

@SpringBootTest
public class DemoTest {

    @Autowired
    com.example.demo.Mapper.getShopByIdMapper getShopByIdMapper;
    @Autowired
    StringRedisTemplate redisTemplate;


    /**
     * 在redis 存入ShopWithOutTime 对象  过期时间20s后
     */
    @Test
    public void test() {
        //1、从数据库读数据
        Shop getshop = getShopByIdMapper.get(1);
        //2、设置到期时间
        ShopWithOutTime shop1 = new ShopWithOutTime();
        shop1.setObject(getshop);
        shop1.setTime(LocalDateTime.now().plusSeconds(20));
        //3、存入redis
        redisTemplate.opsForValue().set("shopwithouttime" + 1, JSONUtil.toJsonStr(shop1));
    }
}

3、然后去修改数据库的数据 那么此时,数据库和redis的数据是不同的

 

 4、此时,并发执行多个线程 ,去查找id为1的店铺

预期结果:如果此时redis数据过期,那么并发情况就是有一部分获取得到的是旧数据,在启动的线程修改完缓存数据之后,获得的是新数据

    private Result throwSolveBySetOutTime(Integer id) {
        //1、基于缓存查找数据
        String str = redisTemplate.opsForValue().get("shopwithouttime" +id);
        //2、缓存没有命中 
//   isBlank 方法会先将字符串去除头尾空格后再进行判断。如果字符串为 null 或者去除头尾空格后的长度为0,则返回 true;否则返回 false。

//这里直接返回的原因是 : 这里是设置逻辑的过期时间,没有设置ttl,不会存在数据不存在的情况
//如果是热点k,这个数据本身 就是 存在 于缓存的。
        if (StrUtil.isBlank(str)) {
            return Result.fail("缓存不存在数据");
        }

        //3、缓存命中 判断是否过期
        ShopWithOutTime shopWithOutTime = JSONUtil.toBean(str, ShopWithOutTime.class);//将 字符串转为 ShopWithOutTime对象
        //3.1 缓存没有到期 不需要更新
        if (shopWithOutTime.getTime().isAfter(LocalDateTime.now())) {
            return Result.ok(shopWithOutTime);
        }

        //3.2缓存到期

        //4、尝试竞争互斥锁
        boolean islock = trylock("shopwithouttimelock" + id);
        //4.1 获取锁失败 返回旧数据
        if (!islock)
            return Result.ok(shopWithOutTime);

        //4.2 获取锁成功  启动其余线程 去刷新缓存
        // 4.2.1考虑并发操作 :可能获取锁之后,缓存其实已经刷新了
        str = redisTemplate.opsForValue().get("shopwithouttime" +id);
        if (StrUtil.isBlank(str)) {
            return Result.fail("缓存不存在数据");
        }

        //4.2.2 缓存没有刷新  启动其余线程 去刷新缓存
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                //1、从数据库读数据
                Shop getshop = getShopByIdMapper.get(id);
                try {
                    Thread.sleep(200);
                    //2、设置到期时间
                    ShopWithOutTime shop1 = new ShopWithOutTime();
                    shop1.setObject(getshop);
                    shop1.setTime(LocalDateTime.now().plusSeconds(20));

                    //3、存入redis
                    redisTemplate.opsForValue().set("shopwithouttime" + id, JSONUtil.toJsonStr(shop1));
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    //4、释放锁
                    unlock("shopwithouttimelock" + id);
                }


            }
        });
        //4.2.3 返回旧值
        return Result.ok(shopWithOutTime);

}

只会有一个线程去执行数据库查询

 缓存工具封装

@Component
public class ClientCatch {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    //1、set方法 设置ttl过期
    public void setExpire(String key, Object value, Long time, TimeUnit timeUnit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, timeUnit);
    }
    //2、set方法  设置逻辑过期  创建RedisData对象 ()

    //public class RedisData {
    //    private LocalDateTime time;
    //    private Object object;
    //}

    public void setLogicalExpire(String key, Object value, Long time, TimeUnit timeUnit) {
        RedisData redisData = new RedisData();
        redisData.setObject(value);
        redisData.setTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time)));

        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

3、设置缓存穿透:存入空值

这里,就需要使用泛型,传入返回类.class,传入参数也是泛型,同时要传入数据库查询的方法

因为匹配所有类型,这里的数据库查询方法返回值也要是泛型,且应该有增删改查四种。这里就可以使用MyBatis-Plus,MyBatis-Plus可以简化单表操作。

1、添加依赖

<dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.0.5</version>
        </dependency>

2、使用方法:(假设实体类为USer)

service层接口可以继承IService接口,IService的使用(需要另外两个接口baseMapper和ServiceImpl的配合)

①mapper接口继承basemapper接口:

@Mapper
public interface UserMapper extends BaseMapper<User> {}

②service接口继承Iservice:

@Service

public interface IUserService extends IService<User> {
}


③service接口的实现类继承ServiceImpl<继承basemapper的mapper,实体类>,实现IUserService接口

@Service
public class IUserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
}

这时,就可以在IUserServiceImpl类里,调用相关方法,实现对Shop数据库的增删改查

如果说,Shop类对应的表名不是Shop,可以在Shop的实体类前加注解@TableName("tb_shop"),就会对tb_shop表进行操作

    //3、解决缓存穿透  : 存入空值

    /**
     *
     * @param keyPrefix   redis缓存前缀
     * @param value       redis的key
     * @param type        返回的对象的类型
     * @param function    调用数据库的逻辑 是一个有返回值,有参数的函数   参数1 参数类型 参数2 返回值类型

     * @param <T>
     * @param <V>
     * @return
     */
    public <T,V> T ThrowSolveBySavezero(String keyPrefix, V value, Class<T> type, Function<V,T> function,Long time,TimeUnit timeUnit) {

        //1、现在缓存查找数据
        String str = stringRedisTemplate.opsForValue().get(keyPrefix + value);
        // 1.1 在缓存查找到了数据
        if (StrUtil.isNotBlank(str)) {
            //返回
            T t = JSONUtil.toBean(str, type);//将 字符串转为 T 对象
            return t;
        }
        //redis没有命中一个合法的字符串   判断是否命中了空值  如果是空值 就应该去返回
        if (Objects.equals(str, "")) {
            return null;
        }

        //2、在缓存没有查找到数据  在数据库查找
        T t = function.apply(value);

        //2.1 在数据库没有找到数据
        if (t == null) {
            //这里 向缓存存入空数据
            stringRedisTemplate.opsForValue().set(keyPrefix + value, JSONUtil.toJsonStr(""), time, timeUnit);
            return null;
        }
        //2.2 在数据库中找到了  将这个数据加入缓存
        stringRedisTemplate.opsForValue().set(keyPrefix + value, JSONUtil.toJsonStr(t), time, timeUnit);
        return t;

    }

 调用这个方法:

 查询id为0的店铺,redis存入空值

使用逻辑过期, 解决缓存击穿


private ExecutorService executorService= Executors.newFixedThreadPool(10);
 
    public <T,V> T ThrowSolveBySetTimeout(String keyPrefix, V value, Class<T> type, Function<V,T> function,Long time,TimeUnit timeUnit) {

        //1、基于缓存查找数据
        String str = stringRedisTemplate.opsForValue().get(keyPrefix + value);
        //2、缓存没有命中  直接返回
        //isBlank 方法会先将字符串去除头尾空格后再进行判断。如果字符串为 null 或者去除头尾空格后的长度为0,则返回 true;否则返回 false。
        if (StrUtil.isBlank(str)) {
            return null;
        }

        //3、缓存命中 判断是否过期
        RedisData redisData = JSONUtil.toBean(str, RedisData.class);//将 字符串转为 RedisData对象
        //3.1 缓存没有到期 不需要更新
        if (redisData.getTime().isAfter(LocalDateTime.now())) {
            //获取 对象 返回
            return JSONUtil.toBean((JSONObject) redisData.getObject(), type);
        }

        //3.2缓存到期
        //4、尝试竞争互斥锁
        boolean islock = trylock("lock" + keyPrefix + value);
        //4.1 获取锁失败 返回旧数据
        //4.2 获取锁成功  启动其余线程 去刷新缓存
        // 4.2.1考虑并发操作 :可能获取锁之后,缓存其实已经刷新了
        if (islock) {
            str = stringRedisTemplate.opsForValue().get(keyPrefix + value);
            //元素没有命中
            if (StrUtil.isBlank(str)) {
                return null;
            }

            //4.2.2 缓存没有刷新  启动其余线程 去刷新缓存
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    //1、从数据库读数据
                    T t = function.apply(value);
                    try {
                        Thread.sleep(200);
                        //2、设置到期时间
                        RedisData redisData1 = new RedisData();
                        redisData1.setObject(t);
                        redisData1.setTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time)));
                        //3、存入redis
                        stringRedisTemplate.opsForValue().set(keyPrefix + value, JSONUtil.toJsonStr(redisData1));
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    } finally {
                        //4、释放锁
                        unlock("lock" + keyPrefix + value);
                    }
                }
            });
        }
        //4.2.3 返回旧值
        return JSONUtil.toBean((JSONObject) redisData.getObject(), type);
    }

//获取锁
    public boolean trylock(String lockStr) {
        return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(lockStr, "1", 1, TimeUnit.SECONDS));
    }

    //释放锁
    public void unlock(String lockStr) {
        stringRedisTemplate.delete(lockStr);

    }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值