Redis总结大全2022

一,Redis快速入门

redis是一个开源的、使用C语言编写的、支持网络交互的、可基于内存也可持久化的Key-Value结构的NoSQL型数据库。

1.SQL和NoSQL的区别:

在这里插入图片描述

2.特征:

1.键值(key-value)型,value支持多种不同数据结构,功能丰富
2.单线程,每个命令具备原子性
3.低延迟,速度快(基于内存、IO多路复用、良好的编程)
4.支持数据持久化
5.支持主从集群、分片集群
6.支持多语言客户端

二,Redis的安装(Linux)

1.安装Redis依赖

Redis 是基于C语言编写的,因此首先需要安装Redis所需要的gcc依赖:

yum install -y gcc tcl

2.上传安装包并解压

安装包下载地址:https://redis.io/download/
在这里插入图片描述

3.解压:

tar -zxvf redis-6.2.6.tar.gz

进入redis目录

cd redis-6.2.6

4.运行编译命令:

make && make install

默认的安装路径是在/usr/local/bin 目录下

5.启动redis-默认启动

redis-server

6.启动redis-指定配置启动

6.1先将配置文件备份一份

cp redis.conf redis.conf.bck

6.2修改redis.conf文件中的一些配置:

vi redis.conf

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

7.开机自启

7.1首先新建一个系统服务文件

vi /etc/systemd/system/redis.service

内容如下:
在这里插入图片描述
7.2让它生效需重载系统服务

systemctl daemon-reload

7.3启动在这里插入图片描述

7.4查看进程

ps-ef |grep redis

7.5执行下面的命令,可以让redis

systemctl enable redis

三,Redis客户端

1.Redis客户端

Redis 安装完成后就自带了命令行客户端:redis-cli,使用方式如下:

redis-cli[options][commonds]

在这里插入图片描述

2.图形化桌面客户端

在下面这个仓库可以找到免费的安装包:
https://github.com/lework/RedisDesktopManager-Windows/releases
双击运行
redis默认有16个库
在这里插入图片描述

四,Redis命令

1.Redis数据结构介绍

Redis为了方便学习,将操作不同数据类型的命令也做了分组,在官网(https://redis.io/commands)可以查看到不同的命令:
在这里插入图片描述

在这里插入图片描述

2.Redis通用命令

通用指令是部分数据类型的,都可以使用的指令,常见的有:
keys:查看符合模板的所有key,不建议在生产环境设备上使用
del:删除一个指定的key,返回interger删除的个数
exist:判断key是否存在,返回1是,返回0否
expire:给一个key设置有效期,有效期到期时改key会自动删除
ttl:查看一个key的剩余有效期
通过help[command] 可以查看一个命令的具体用法,例如:
在这里插入图片描述

3.String类型

在这里插入图片描述
在这里插入图片描述

4.key的层级结构

在这里插入图片描述
在这里插入图片描述

5.Hash类型

在这里插入图片描述
在这里插入图片描述

6.List类型

在这里插入图片描述
在这里插入图片描述

7.Set类型

在这里插入图片描述
在这里插入图片描述

8.SortedSet

在这里插入图片描述
在这里插入图片描述

五 ,Redis客户端

1.Redis的java客户端

客户端网站:https://redis.io/clients
在这里插入图片描述

2.三种常见实现的优缺点

在这里插入图片描述

六, Jedis

Jedis的官网地址:https://github.com/redis/jedis

1.流程

在这里插入图片描述
在这里插入图片描述

2.基本步奏:

在这里插入图片描述

3.配置连接池

在这里插入图片描述

七,SpringDataRedis

官网地址:https://spring.io/projects/spring-data-redis

1.定义

在这里插入图片描述

2.快速入门

在这里插入图片描述

3.流程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.基本步奏

在这里插入图片描述

5.自动序列化和反序列化

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
因为自动序列化的@class占用了额外的空间

6.手动序列化方式:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

7.两种序列化实践方案

在这里插入图片描述

8.hash案例:

在这里插入图片描述

八,实战-session实现短信登入

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
注意:在当前目录中输入cmd点击回车可以进入 此目录对应窗口

1.设计

在这里插入图片描述

2.发送验证码

在这里插入图片描述

3.登入

在这里插入图片描述
在这里插入图片描述

4.实现登录效验拦截

在这里插入图片描述
前置拦截:
在这里插入图片描述
在这里插入图片描述

后置拦截-关闭
在这里插入图片描述

5.配置拦截器

在这里插入图片描述
在这里插入图片描述

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/shop/**",
                        "/upload/**",
                        "/user/code",
                        "/user/login"
                        );
    }
}

在这里插入图片描述
在这里插入图片描述

6.隐藏用户登入信息

降低session存放的数据量,将user变为userDto,修改如下
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
service层业务代码如下:

@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    //验证码发送
    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1.效验手机号
        if(RegexUtils.isPhoneInvalid(phone)|| StrUtil.isBlank(phone)){
            //2.不符合 返回错误
            return Result.fail("手机号格式错误");
        }
        //3.生成验证码 6位
        String code= RandomUtil.randomNumbers(6);
        //4.保存验证码
        session.setAttribute("code",code);
        //5.发送验证码
        log.info("发送验证码成功:"+code);
        //返回ok
        return Result.ok();
    }
    //登入
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.效验手机号
        String phone=loginForm.getPhone();
        if(RegexUtils.isPhoneInvalid(phone)|| StrUtil.isBlank(phone)){
            //不符合 返回错误
            return Result.fail("手机号格式错误");
        }
        //2.效验验证码
        String code=loginForm.getCode();
        Object cacheCode= session.getAttribute("code");
        if(RegexUtils.isCodeInvalid(code)|| StrUtil.isBlank(code)||!code.equals(cacheCode.toString())){
            //不符合 返回错误
            return Result.fail("验证码错误");
        }
        //3.判断客户是否存在
        User user=query().eq("phone",phone).one();
        if(user==null){
            //4.如果不存在,创建新用户并保存
            user= createUserWithPhone(phone);

        }
        // 5.将用户存入session
        session.setAttribute("user", BeanUtil.copyProperties(user,UserDTO.class ));
        return null;
    }
    //注册
    private User createUserWithPhone(String phone) {
        User user= new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME+RandomUtil.randomString(10));
        return user;
    }
}

拦截器代码如下:


public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //TODO 1.获取session
        HttpSession session = request.getSession();
        //TODO 2.获取session中的用户
        UserDTO user = (UserDTO)session.getAttribute("user");
        //TODO 3.判断用户是否存在
        if(user==null){
            //TODO 4.不存在,拦截
            response.setStatus(401);
            return false;
        }
        //TODO 5.存在,保存用户信息到ThreadLocal
        UserHolder.saveUser(user);
        return true;
    }

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

}

拦截器配置代码如下:


public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/shop/**",
                        "/upload/**",
                        "/user/code",
                        "/user/login"
                        );
    }
}

Threadlocal代码如下

public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    public static void saveUser(UserDTO user){
        tl.set(user);
    }

    public static UserDTO getUser(){
        return tl.get();
    }

    public static void removeUser(){
        tl.remove();
    }
}

5.session共享问题的替代方案-redis
在这里插入图片描述

九,实战-redis实现短信登入

1.设计

在这里插入图片描述
在这里插入图片描述

2.前端实现

在这里插入图片描述
登入时会将后端给的token放入sessionStorage中,然后定义拦截器去拦截,每次发请求都拦截,携带token
在这里插入图片描述

3.后端实现

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
需要刷新token过期时间,否则以上代码会造成客户登入30分钟后过期,无法监听客户操作后刷新token过期时间

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
service层业务代码如下:

@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    //验证码发送
    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1.效验手机号
        if(RegexUtils.isPhoneInvalid(phone)|| StrUtil.isBlank(phone)){
            //2.不符合 返回错误
            return Result.fail("手机号格式错误");
        }
        //3.生成验证码 6位
        String code= RandomUtil.randomNumbers(6);
        //4.保存验证码到redis
       // session.setAttribute("code",code);
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
        //5.发送验证码
        log.info("发送验证码成功:"+code);
        //返回ok
        return Result.ok();
    }
    //登入
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.效验手机号
        String phone=loginForm.getPhone();
        if(RegexUtils.isPhoneInvalid(phone)|| StrUtil.isBlank(phone)){
            //不符合 返回错误
            return Result.fail("手机号格式错误");
        }
        //2.从redis获取验证码并效验
        //String code=loginForm.getCode();
        String code = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        Object cacheCode= session.getAttribute("code");
        if(RegexUtils.isCodeInvalid(code)|| StrUtil.isBlank(code)||!code.equals(cacheCode.toString())){
            //不符合 返回错误
            return Result.fail("验证码错误");
        }
        //3.判断客户是否存在
        User user=query().eq("phone",phone).one();
        if(user==null){
            //4.如果不存在,创建新用户并保存
            user= createUserWithPhone(phone);

        }
        //5.保存用户信息到redis中
        //6.随机生成token,作为登入令牌
        String token= UUID.randomUUID().toString();
        //7.将user对象作为map存储
        UserDTO userDTO=BeanUtil.copyProperties(user,UserDTO.class);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>()//stringRedisTemplate的参数必须为String
                ,CopyOptions.create().ignoreNullValue().setIgnoreNullValue(true)//将userDTO中第一个属性id由integet转为String
                        .setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));
        //8.存储
        String tokenKey=LOGIN_USER_KEY+token;
        stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
        //9.设置token有效期
        stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
        //10返回token
        return Result.ok(token);
    }
    //注册
    private User createUserWithPhone(String phone) {
        User user= new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME+RandomUtil.randomString(10));
        return user;
    }
}

拦截器代码

public class LoginInterceptor implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //TODO 1.获取请求头中的token
        String token=request.getHeader("authorization");
        if(StrUtil.isBlank(token)){
            response.setStatus(401);
            return false;
        }
        //2.基于token获取redis中的用户
        String key=LOGIN_USER_KEY+token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        //3.判断用户是否存在
        if(userMap==null){
            //4.不存在,拦截
            response.setStatus(401);
            return false;
        }
        //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.添加Redis缓存

在插入图片描述

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public Result queryById(Long id) {
        String key=CACHE_SHOP_KEY+id;
        //1.从redis中查询商品缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2.判断商品缓存是否存在
        if(StrUtil.isNotBlank(shopJson)){
            //3.存在,返回商品
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //4.不存在,根据去数据库中查询
        Shop shop = getById(id);
        //5.数据库查询也不存在,返回错误
        if (shop==null) {
            return Result.fail("该店铺不存在");
        }
        //6.数据库查询存在,写入redis
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
        //7.返回
        return Result.ok(shop);
    }
}

3.缓存更新策略

在这里插入图片描述
在这里插入图片描述
第三种方案不能完全保证一致性,综上第一种方案较为实用.
在这里插入图片描述
在这里插入图片描述
这种情况下线程穿插时会触发异常,导致缓存和数据库数据不一致,可能性很高
在这里插入图片描述
这种情况下线程穿插时会触发异常,导致缓存和数据库数据不一致,可能性很低.
在这里插入图片描述

4.缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库.
在这里插入图片描述
(1)缓存空对象 解决缓存穿透问题

private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryById(Long id) {
        String key=CACHE_SHOP_KEY+id;
        //1.从redis中查询商品缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2.判断商品缓存是否存在
        if(StrUtil.isNotBlank(shopJson)){
            //3.存在,返回商品
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //判断命中的是否是空值
        if(shopJson!=null){
            return Result.fail("店铺信息不存在!");
        }
        //4.不存在,根据去数据库中查询
        Shop shop = getById(id);
        //5.数据库查询也不存在,返回错误
        if (shop==null) {
            //将空值写入redis,过期时间短一点 2分钟
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
            return Result.fail("该店铺不存在");
        }
        //6.数据库查询存在,写入redis
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
        //7.返回
        return Result.ok(shop);
    }

在这里插入图片描述

在这里插入图片描述

5.缓存雪崩

缓存雪崩是指同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力.
在这里插入图片描述

6.缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击.
在这里插入图片描述
在这里插入图片描述
优缺点:
在这里插入图片描述

(1)基于互斥锁方式解决缓存击穿问题:
 @Override
    public Result queryById(Long id) {
        //缓存击穿
        //Shop shop = queryWithPassThrough(id);
        //互斥锁解决缓存击穿
        Shop shop = queryWithMutex(id);
        if (shop==null) {
            return Result.fail("店铺信息不存在");
        }
        return Result.ok(shop);
    }
//缓存击穿互斥锁
    public Shop queryWithMutex(Long id){
        String key=CACHE_SHOP_KEY+id;
        //1.从redis中查询商品缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2.判断商品缓存是否存在
        if(StrUtil.isNotBlank(shopJson)){
            //3.存在,返回商品
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }
        //判断命中的是否是空值
        if(shopJson!=null){
            return null;
        }
        //4.实现缓存重建
        //4.1.获取互斥锁
        String lockKey=LOCK_SHOP_KEY+id;
        //4.2.判断是否获取成功
        Shop shop = null;
        try {
            boolean isLock = tryLock(lockKey);
            if(!isLock){
                //4.3.失败则休眠并重试
                Thread.sleep(50);
                queryWithMutex(id);
            }
            //4.不存在,根据去数据库中查询
            shop = getById(id);
            //5.数据库查询也不存在,返回错误
            if (shop==null) {
                //将空值写入redis,过期时间短一点 2分钟
                stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            //6.数据库查询存在,写入redis
            stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //7.释放互斥锁
            unLock(lockKey);
        }
        //8.返回
        return shop;
    }

互斥锁的获取和释放

    //获取互斥锁
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.MILLISECONDS);
        return BooleanUtil.isTrue(flag);//自动拆箱
    }
    //释放互斥锁
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }
(2)基于逻辑过期方式解决缓存击穿问题:
    @Override
    public Result queryById(Long id) {
        //缓存击穿
        //Shop shop = queryWithPassThrough(id);
        //互斥锁解决缓存击穿
        //Shop shop = queryWithMutex(id);
        //逻辑过期解决缓存击穿
        Shop shop = queryWithLogicalExpire(id);
        if (shop==null) {
            return Result.fail("店铺信息不存在");
        }
        return Result.ok(shop);
    }
//缓存击穿-逻辑过期
    private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);
    public Shop queryWithLogicalExpire(Long id){
        String key=CACHE_SHOP_KEY+id;
        //1.从redis中查询商品缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否存在
        if(StrUtil.isNotBlank(shopJson)){
            //3.存在,返回
            return null;
        }
        //4.命中,需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        //5.判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())){
           //5.1.未过期,直接返回店铺信息
            return shop;
        }
        //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 {
                    //重建缓存
                    this.saveShop2Redis(id,20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    //释放锁
                    unLock(lockKey);
                }
            });
        }
        //6.4.返回过期的商品信息
        return shop;
    }
    
   //重建缓存
   public void saveShop2Redis(Long id,Long expireSeconds){
    //1.查询店铺数据
    Shop shop = getById(id);
    //2.封装逻辑过期时间
    RedisData redisData=new RedisData();
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
    //3.写入Redis
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
    }
    
    //获取互斥锁
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.MILLISECONDS);
        return BooleanUtil.isTrue(flag);//自动拆箱
    }
    //释放互斥锁
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }

7.缓存工具封装

在这里插入图片描述方法1和方法3处理缓存穿透问题,
方法2和方法4处理热点key缓存击穿问题

(1)用工具类解决缓存穿透问题:

service层代码:

   @Resource
    private CacheClient cacheClient;

    @Override
    public Result queryById(Long id) {
        //缓存击穿
        //Shop shop = queryWithPassThrough(id);
        //用工具类解决缓存击穿
        Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY,id,Shop.class,this::getById,30L,TimeUnit.MINUTES);
        if (shop==null) {
            return Result.fail("店铺信息不存在");
        }
        return Result.ok(shop);
    }

工具类代码:


@Slf4j
@Component
public class CacheClient {
    private final StringRedisTemplate stringRedisTemplate;

    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){  //R和ID都是泛型
        String key=keyPrefix+id;
        //1.从redis中查询商品缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        //2.判断商品缓存是否存在
        if(StrUtil.isNotBlank(json)){
            //3.存在,返回商品
            R r = JSONUtil.toBean(json, type);
            return r;
        }
        //判断命中的是否是空值
        if(json!=null){
            return null;
        }
        //4.不存在,根据去数据库中查询
        R r = dbFallBack.apply(id);
        //5.数据库查询也不存在,返回错误
        if (r==null) {
            //将空值写入redis,过期时间短一点 2分钟
            stringRedisTemplate.opsForValue().set(key,"",time, unit);
            return null;
        }
        //6.数据库查询存在,写入redis
        this.set(key,r,time,unit);
        //7.返回
        return r;
    }
}
(2)用工具类的逻辑过期解决缓存击穿问题:

service层:

   @Resource
    private CacheClient cacheClient;

    @Override
    public Result queryById(Long id) {
        //逻辑过期解决缓存击穿
        //Shop shop = queryWithLogicalExpire(id);
        //用工具类解决缓存击穿
        Shop shop=cacheClient.queryWithLogicalExpire(
                CACHE_SHOP_KEY,id,Shop.class,this::getById,CACHE_SHOP_TTL,TimeUnit.MINUTES);
        if (shop==null) {
            return Result.fail("店铺信息不存在");
        }
        return Result.ok(shop);
    }

工具类代码:


@Slf4j
@Component
public class CacheClient {
    private final StringRedisTemplate stringRedisTemplate;

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

    private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);
    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.isNotBlank(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=keyPrefix+id;
        boolean isLock = tryLock(lockKey);
        //6.2.判断是否获取锁成功
        if(isLock){
            //6.3.成功,开启独立线程,实现缓存重建
               CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    //重建缓存,查询数据库
                    R r1 = dbFallBack.apply(id);
                    //写入Redis
                    this.setWithLogicalExpire(key,r1,time,unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    //释放锁
                    unLock(lockKey);
                }
            });
        }
        //6.4.返回过期的商品信息
        return r;
    }
    //获取互斥锁
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.MILLISECONDS);
        return BooleanUtil.isTrue(flag);//自动拆箱
    }
    //释放互斥锁
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值