SpringBoot整合Redis实现常用功能

1 登陆功能

1.1 基于Session实现登录流程

功能流程:

SpringBoot整合Redis实现常用功能-LMLPHP

1.1.1 session共享问题

核心思路分析:

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

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

SpringBoot整合Redis实现常用功能-LMLPHP


1.2 Redis替代Session

1.2.1、设计key的结构

SpringBoot整合Redis实现常用功能-LMLPHP

1.2.2、设计key的具体细节

1.2.3、整体访问流程

SpringBoot整合Redis实现常用功能-LMLPHP

2 缓存功能

2.1 什么是缓存?

例1:Static final ConcurrentHashMap<K,V> map = new ConcurrentHashMap<>(); 本地用于高并发

例2:static final Cache<K,V> USER_CACHE = CacheBuilder.newBuilder().build(); 用于redis等缓存

例3:Static final Map<K,V> map =  new HashMap(); 本地缓存

2.1.1 为什么要使用缓存

SpringBoot整合Redis实现常用功能-LMLPHP

2.1.2 如何使用缓存

SpringBoot整合Redis实现常用功能-LMLPHP

2.2.使用缓存

2.2.1 、缓存模型和思路

SpringBoot整合Redis实现常用功能-LMLPHP

2.3 缓存更新策略

内存淘汰:redis自动进行,当redis内存达到咱们设定的max-memery的时候,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)

超时剔除:当我们给redis设置了过期时间ttl之后,redis会将超时的数据进行删除,方便咱们继续使用缓存

主动更新:我们可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题

SpringBoot整合Redis实现常用功能-LMLPHP

2.3.1 、数据库缓存不一致解决方案:

SpringBoot整合Redis实现常用功能-LMLPHP

2.3.2 、数据库和缓存不一致采用什么方案

SpringBoot整合Redis实现常用功能-LMLPHP

2.4 缓存穿透问题的解决思路


SpringBoot整合Redis实现常用功能-LMLPHP

小总结:

3 工具类

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.CACHE_NULL_TTL;

/**
 * @author : look-word
 * 2022-08-19 17:02
 **/
@Component
public class CacheClient {
    @Resource
    private 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.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        redisData.setData(value);
        String redisDataJson = JSONUtil.toJsonStr(redisData);
        // 写入Redis
        stringRedisTemplate.opsForValue().set(key, redisDataJson);
    }

    /**
     * 解决缓存穿透 对未存在的数据 设置为null
     */
    public <R, ID> R queryWithPassThrough
    (String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long cacheTime, TimeUnit cacheUnit) {
        // 缓存key
        String key = keyPrefix + id;
        // 1 查询缓存中是否命中
        String json = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(json)) {
            R r = JSONUtil.toBean(json, type);
            return r;
        }
        // 解决缓存穿透 数据库不存在的数据 缓存 也不存在 恶意请求
        if (json != null) {
            return null;
        }

        // 2 查询数据库 存在 存入缓存 返回给前端
        R r = dbFallback.apply(id);
        if (r == null) {
            // 解决缓存穿透
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        // 2.1 转换成json 存入缓存中
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(r), cacheTime, cacheUnit);

        return r;
    }

    // 线程池
    public static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 解决缓存击穿 逻辑过期时间方式
     */
    public <R, ID> R queryWithLogicalExpire
    (String keyPrefix, ID id, Class<R> type, String lockKeyPrefix, Function<ID, R> dbFallback, Long expiredTime, TimeUnit expiredUnit) {
        // 缓存key
        String key = keyPrefix + id;
        // 1 查询缓存中是否命中
        String redisDataJson = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(redisDataJson)) {
            return null;
        }
        // 2.命中 查看是否过期,
        //     2.1 未过期 直接返回旧数据
        //     2.2 过期 获取锁 查询数据写入Redis设置新的过期时间
        //     2.3 过期 未获取锁 返回 旧数据
        RedisData redisData = JSONUtil.toBean(redisDataJson, RedisData.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        if (LocalDateTime.now().isBefore(expireTime)) {
            return r;
        }
        String lockKey = lockKeyPrefix + id;
        // 获取锁
        boolean isLock = tryLock(lockKey);
        if (isLock) {
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 查询数据库
                    R r1 = dbFallback.apply(id);
                    // 存储Redis 设置逻辑过期 过期时间
                    setWithLogicalExpire(key, r1, expiredTime, expiredUnit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放锁
                    unlock(lockKey);
                }
            });
        }
        // 未获取到锁
        return r;
    }

    /**
     * 获取锁
     */
    public boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 100, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     */
    public void unlock(String key) {
        stringRedisTemplate.delete(key);
    }

}
08-20 12:35