Redis 篇-深入了解查询缓存与缓存所带来的问题(读写不一致、缓存穿透、缓存雪崩、缓存击穿)

news2024/11/26 13:36:18

🔥博客主页: 【小扳_-CSDN博客】
❤感谢大家点赞👍收藏⭐评论✍

本章目录

        1.0 什么是缓存

        2.0 项目中具体如何添加缓存

        3.0 添加缓存后所带来的问题

        3.1 读写不一致问题

        3.1.1 缓存更新策略

        3.1.2 具体实现缓存与数据库的双写一致

        3.2 缓存穿透问题

        3.2.1 具体解决缓存穿透问题

        3.3 缓存雪崩问题

        3.4 缓存击穿问题

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

        3.4.2 利用逻辑过期解决缓存击穿问题

        4.0 封装 Redis 工具类


        1.0 什么是缓存

        缓存就是数据交换的缓冲器,称作为 Cache,是存放数据的零时地方,一般读写性能较高。缓存的作用可以降低后端负载,提高读写效率、降低响应时间。缓存的成本包括数据一致性成本、代码维护成本、运维成本等。

        

        2.0 项目中具体如何添加缓存

        举例子,在实现根据用户 id 来查询用户信息的功能中,添加缓存的步骤:

        首先,提交用户 id ,先从缓存中查找是否命中目标,就是是否有相同的 id 关键字 key 。如果命中,直接返回该 key 对应的 value 即可;如果没有命中,就需要来到数据库中查询用户信息,继续判断数据库中是否存在该用户 id ,如果不存在,那么返回报错信息;如果存在,那么返回该用户信息的同时,将用户信息写回到 Redis 缓存中。

缓存作用模型图:

代码实现:

    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Override
    public String getUserNameById(Integer userId) throws Exception {
        //先判断userId是否为空
        if (userId == null){
            throw new Exception("userId is null");
        }
        //先从缓存中查看是否存在该key
        String s = stringRedisTemplate.opsForValue().get("user:" + userId);
        if (s != null){
            //如果缓存中不为null,则成功从缓存中获取值
            return s;
        }

        //如果从缓存中获取不到,则需要到数据库中获取数据
        String userName = adminMapper.getUserNameById(userId);
        //如果数据返回为null,那么数据库中查找不到数据
        if (userName == null){
            //直接抛出异常
            throw new Exception("根据该用户id查找不到用户信息");
        }
        //判断数据不为null之后,则需要将该用户信息写到redis中
        stringRedisTemplate.opsForValue().set("user:"+userId,userName);
        //最后返回值即可
        return userName;

    }

运行结果:

        在第一次查询的时候,redis 第一次时找不到该用户信息,那么就会到数据库中查询,查询完毕之后,将数据写回到 redis 中,再到第二次查询的时候,就可以直接到 redis 中获取数据了。

发送的请求:

第一次获取数据:

        到数据库中获取了

此时 redis 中:

        已经存在该用户信息了

        3.0 添加缓存后所带来的问题

        添加缓存之后,会带来一些问题,比如说:数据库更新之后,缓存还没来得及更新所带来的缓存与数据库数据不一致问题,还有缓存穿透、缓存雪崩、缓存击穿等问题给数据库带来的沉重的“打击”。

        3.1 读写不一致问题

        顾名思义,数据库与缓存中的数据两者不一致,为了解决这个问题,就有了缓存更新策略,可以极大可能维护缓存中的数据和数据库中的数据一致性。

        3.1.1 缓存更新策略

通常的方法有三种:

        1)内存淘汰:不用自己维护,利用 redis 的内存淘汰机制,当内存不足自动淘汰部分数据,下次查询时更新缓存。该方法一致性比较差,无维护成本。

        2)超时剔除:给缓存数据添加 TTL 时间,到期后自动删除缓存,下次查询时更新缓存。该方法一致性一般,维护成本低。

        3)主动更新:

        编写业务逻辑,在修改数据库的同时,更新缓存。该方法一致性比较好,维护成本高。主动更新包含三种常见的策略:

        第一种:Cache Aside Pattern:由缓存的调用者,在更新数据库的同时更新缓存。

        第二种:Read/Write Through Pattern:缓存与数据库整合一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题。

        第三种:Write Behind Caching Pattern:调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库,保证最终一致。

在主动更新中,第一种方式比较常见,实现比较简单。但是在操作缓存和数据库时有三个问题需要考虑:

        第一个问题:删除缓存还是更新缓存?

                更新缓存:每次更新数据库都更新缓存,无效写操作较多。

                删除缓存:更新数据库时让缓存失效,查询时在更新缓存。

                因此,一般来说,选择删除缓存。

        第二个问题:如何保证缓存与数据库的操作的同时成功或失败?

                将缓存与数据库操作放在同一个事务即可,保证其原子性。

        第三个问题:先操作缓存还是先操作数据库?

                先写数据库,然后删除缓存。

缓存更新策略的最佳实践方案:

        1)低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存。

        2)高一致性需求:主动更新,并以超时剔除作为兜底方案。

                读操作:

                        缓存命中则直接返回,缓存未命中则查询数据库,并写入缓存,设定超时时间

                写操作:

                        先写数据库,然后再删除缓存,要确保数据库与缓存操作的原子性。

        3.1.2 具体实现缓存与数据库的双写一致

实现高一致性需求:主动更新策略代码:

        1)读操作:缓存命中则直接返回,缓存未命中则查询数据库,并写入缓存,设定超时时间。

    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Override
    public String getUserNameById(Integer userId) throws Exception {
        //先判断userId是否为空
        if (userId == null){
            throw new Exception("userId is null");
        }
        //先从缓存中查看是否存在该key
        String s = stringRedisTemplate.opsForValue().get("user:" + userId);
        if (s != null){
            //如果缓存中不为null,则成功从缓存中获取值
            return s;
        }

        //如果从缓存中获取不到,则需要到数据库中获取数据
        String userName = adminMapper.getUserNameById(userId);
        //如果数据返回为null,那么数据库中查找不到数据
        if (userName == null){
            //直接抛出异常
            throw new Exception("根据该用户id查找不到用户信息");
        }
        //判断数据不为null之后,则需要将该用户信息写到redis中,且设定超时时间
        stringRedisTemplate.opsForValue().set("user:"+userId,userName,100, TimeUnit.SECONDS);

        //最后返回值即可
        return userName;
    }

        这里的重点是:设置超时时间。

        2)写操作:先写数据库,然后再删除缓存,要确保数据库与缓存操作的原子性。

        为了保证原子性,需要加上 @Transactional 注解

    @Override
    @Transactional
    public void modifyUser(UserDTO userDTO) throws Exception {
        //先判断userDTO是否为null
        if (userDTO == null){
            throw new Exception("userDTO is null");
        }
        //先更新数据库
        adminMapper.modifyUser(userDTO);
        //再删除redis缓存
        Integer userId = userDTO.getUserId();
        stringRedisTemplate.delete("user"+userId);
    }

运行结果:

        先查询用户信息,因为第一次 redis 不存在该用户信息,因此需要到数据库中获取该用户信息。

        从数据库中查询信息:

        redis 缓存情况:

        接着去更新用户信息:

        此时,redis 中的用户信息就被删除掉了:

                下一次查询就需要到数据库中查询了。

        再一次查询:

                会到数据库中查询用户信息。

        3.2 缓存穿透问题

        是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远都不会生效,这些请求都会打到数据库。则会给数据库的压力非常大,因此需要解决这种情况发生。

        常见的解决方案有四种:

        1)增强 id 的复杂度,避免被猜测 id 规律。

        2)做好数据的基础格式校验。

        3)缓存空对象:实现简单,维护方便。

        该方法的缺点:额外的内存消耗,因为设置 key 对应的 value 为 null ,占用了一定的缓存空间,因此为了减少内存浪费,会设置缓存时间 TTL ;还可能造成短期的不一致,当数据库中 key 有对应的 value 了,当前的 key 还在缓存中,value 还是为 null ,所以造成一定的不一致性。

        4)布隆过滤:内存占用较少,没有多余 key ,该方法的缺点为实现复杂,存在误判的可能。

        3.2.1 具体解决缓存穿透问题

使用缓存空对象来解决缓存穿透问题步骤:

        首先,从缓存中查询用户,判断缓存是否命中,如果命中,则直接返回用户信息;如果没有命中,根据用户 id 到数据库中查询用户信息,如果用户信息不为 null ,则说明用户信息是存在的,那么将用户信息写回到缓存中,方便下一次查询可以直接从缓存中获取用户信息;如果用户信息为 null ,则说明数据库中也不存在该用户信息,那么下一次就不需要继续查询该用户信息了,让其在缓存中查询,再抛出异常即可。

具体的流程图:

代码如下:

    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Override
    public String getUserNameById(Integer userId) throws Exception {
        //先判断userId是否为空
        if (userId == null){
            throw new Exception("userId is null");
        }
        //先从缓存中查看是否存在该key
        String s = stringRedisTemplate.opsForValue().get("user:" + userId);

        if (StrUtil.isNotBlank(s)){
            //如果缓存中不为null,则成功从缓存中获取值
            return s;
        }

        if (s != null){
            //直接抛出异常
            throw new Exception("该用户信息不存在!");
        }

        //如果从缓存中获取不到,则需要到数据库中获取数据
        String userName = adminMapper.getUserNameById(userId);
        //如果数据返回为null,那么数据库中查找不到数据
        if (userName == null){
            //如果在数据库中找不到该信息,则将该 key 值对应的 value 为 "" 写到缓存中
            stringRedisTemplate.opsForValue().set("user:"+userId,"",100,TimeUnit.SECONDS);
        }
        //判断数据不为null之后,则需要将该用户信息写到redis中,且设定超时时间
        stringRedisTemplate.opsForValue().set("user:"+userId,userName,100, TimeUnit.SECONDS);

        //最后返回值即可
        return userName;
    }

 运行结果:

        查询数据库不存在的用户信息:

                第一次会到数据库查询该用户信息,当该用户信息不存在时,则会在 redis 中设置空值,这样的好处,下一次的查询该用户,就不会打到数据库中了,减少了数据库的压力。

        3.3 缓存雪崩问题

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

        3.3.1 解决缓存雪崩方案

        1)给不同的 key 的 TTL 添加随机值。

        2)利用 Redis 集群提高服务的可用性。

        3)给缓存业务添加降级限流策略。

        4)给业务添加多级缓存。

        3.4 缓存击穿问题

        缓存击穿问题也叫热点 Key 问题,就是一个被高并发访问并且缓存重建业务交复杂的 Key 突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

如图:

        常见的解决方法:

        1)利用互斥锁解决击穿问题

        没有额外的内存消耗,保证一致性,实现简单。该方法的缺点:线程需要等待,性能受影响,可能有死锁的风险。

        2)利用逻辑过期解决缓存击穿问题 

        线程无需等待,性能较好。该方法的缺点,不保证一致性,有额外的内存消耗,实现复杂。

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

        利用互斥锁解决的步骤:

        首先,查询缓存是否命中,如果命中,直接返回;如果没有命中,则需要判断是否能获取互斥锁,如果获取到了互斥锁,则查询数据库重建缓冲数据,最后释放锁,再返回数据;如果没有获取互斥锁,则休眠一段时间,再重试,直到从缓冲中获取到数据返回。

流程图:

代码如下:

        解决缓存穿透与缓存击穿:

    //解决缓存穿透与缓存击穿
    public String getUserNameById2(Integer userId) throws Exception {
        //先判断userId是否为空
        if (userId == null){
            throw new Exception("userId is null");
        }
        //先从缓存中查看是否存在该key
        String s = stringRedisTemplate.opsForValue().get("user:" + userId);

        if (StrUtil.isNotBlank(s)){
            //如果缓存中不为null,则成功从缓存中获取值
            return s;
        }

        if (s != null){
            //直接抛出异常
            throw new Exception("该用户信息不存在!");
        }

        //如果从缓存中获取不到,则需要到数据库中获取数据
        //判断释放可以获取到锁
        String lock = "getLock";

        String userName = null;
        try {
            boolean b = tryLock(lock);
            if (!b) {
                //如果没有获取到锁,休眠一会,再重新从缓存中获取数据
                Thread.sleep(50);
                return getUserNameById2(userId);

            }

            userName = adminMapper.getUserNameById(userId);
            //如果数据返回为null,那么数据库中查找不到数据
            if (userName == null){
                //如果在数据库中找不到该信息,则将该 key 值对应的 value 为 "" 写到缓存中
                stringRedisTemplate.opsForValue().set("user:"+userId,"",100,TimeUnit.SECONDS);
            }
            //判断数据不为null之后,则需要将该用户信息写到redis中,且设定超时时间
            stringRedisTemplate.opsForValue().set("user:"+userId,userName,100, TimeUnit.SECONDS);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            //释放锁
            unlock(lock);
        }

        //返回值即可
        return userName;
    }


    //获取锁
    private boolean tryLock(String key){
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 200, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(aBoolean);
    }

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

        3.4.2 利用逻辑过期解决缓存击穿问题

        设置缓存中 key 的逻辑过期,顾名思义:在实际上,缓存中的 key 是设置永远不过期,将其添加过期字段,通过查看该字段,来判断该 key 在缓存中是否已经过期了。

        利用逻辑过期解决缓存击穿问题步骤:

        首先,判断缓存是否命中,如果没有命中,则返回空;如果命中,继续判断该字段是否过期,如果没有过期,则直接获取并且返回该值;如果已经过期,再继续判断能否获取锁,如果获取锁失败,则直接返回已经过期的值;如果获取锁成功,创建一个线程来做查询数据库,并且写入到缓存中,对于主线程来说,仍然返回旧的数据。

流程图:

代码实现:

        利用逻辑过期实现解决缓存击穿问题:

    //获取锁
    private boolean tryLock(String key){
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 200, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(aBoolean);
    }

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

    //解决缓存穿透

    public String getUserNameById(Integer userId) throws Exception {
        //先判断userId是否为空
        if (userId == null){
            throw new Exception("userId is null");
        }
        //先从缓存中查看是否存在该key
        String s = stringRedisTemplate.opsForValue().get("user:" + userId);

        //如果从缓存中没有获取到数据,则直接抛出异常
        if (s == null){
            throw new Exception("该用户不存在!!!");
        }

        //反序列化
        RedisData redisData = JSON.parseObject(s, RedisData.class);
        String data = (String) redisData.getData();
        LocalDateTime localDateTime = redisData.getLocalDateTime();
        //判断是否过期
        if (localDateTime.isAfter(LocalDateTime.now())){
            //如果没有过期,则直接返回数据
            return data;

        }

        //创建线程池
        ExecutorService pool = Executors.newFixedThreadPool(10);
        //如果过期了
        //判断能否获取到互斥锁
        String lock = "getLock";

        boolean b = tryLock(lock);
        if (b) {
            //获取到锁,从线程池中获取一个线程来从数据库获取信息,再将信息写入到缓存中
            pool.submit(() -> {
                try {
                    //先从数据库中获取到数据
                    String userName = adminMapper.getUserNameById(userId);

                    //再将数据写入到缓存中
                    RedisData red = new RedisData();

                    //设置过期时间
                    red.setLocalDateTime(LocalDateTime.now().plusSeconds(100L));
                    red.setData(userName);
                    //将其序列化
                    String jsonString = JSON.toJSONString(red);
                    stringRedisTemplate.opsForValue().set("user:"+userId,jsonString);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放锁
                    unlock(lock);
                }
            });

        }

        //最后返回
        return data;
    }

        4.0 封装 Redis 工具类

        基于 StringRedisTemplate 封装一个缓存工具类,满足下列需要:

        1)方法1:将任意 Java 对象序列化为 json 并存储在 string 类型的 key 中,并且可以设置 TTL 过期时间。

代码如下:

    public void set(String key, Object value, Long time, TimeUnit timeUnit){
        stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(value),time,timeUnit);
    }

        2)方法2:将任意 Java 对象序列化为 json 并存储在 string 类型的 key 中,并且可以设置逻辑过期时间,用于处理缓存击穿问题。

代码如下:

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

        String jsonString = JSON.toJSONString(redisData);
        stringRedisTemplate.opsForValue().set(key,jsonString);
        
    }

        3)方法3:根据指定的 key 查询缓存,并反序列化为指定类型,利用缓存空值的方式解决穿透问题。

    //利用缓存空值解决缓存穿透
    public <R,ID> R queryWithPassThrough(String prefix,ID id,Class<R> type,Function<ID,R> function,Long time,TimeUnit unit ){

        String key = prefix + id;
        //判断在缓存中是否能命中
        String jsonString = stringRedisTemplate.opsForValue().get(key);


        if (StrUtil.isNotBlank(jsonString)){
            //反序列化
            return JSON.parseObject(jsonString, type);
        }

        if (jsonString != null){
            return null;
        }

        //查询数据库,且将数据信息写入到缓存中
        R apply = function.apply(id);
        //判断是否为空值
        if (apply == null){
            //如果为空
            //将其写进缓存中
            stringRedisTemplate.opsForValue().set(key,"",50,TimeUnit.SECONDS);
            return null;
        }
        //序列化
        String json = JSON.toJSONString(apply);
        //如果不为空
        stringRedisTemplate.opsForValue().set(key,json,time,unit);
        return apply;

    }

        4)方法4:根据指定的 key 查询缓存,并反序列为指定类型,需要利用逻辑过期解决缓存击穿问题。

    //利用逻辑过期解决缓存击穿
    public <R,ID> R queryWithLogicalExpire(String prefix,ID id,Class<R> type,Function<ID,R> function,Long time,TimeUnit unit){
        String key = prefix + id;
        //判断在缓存中是否命中
        String s = stringRedisTemplate.opsForValue().get(key);
        //如果不存在,直接返回null
        if (s == null){
            return null;
        }
        //如果存在,还得判断是否过期
        //反序列化
        RedisData redisData = JSONUtil.toBean(s, RedisData.class);
        JSONObject d = (JSONObject) redisData.getData();
        R data = JSONUtil.toBean(d, type);

        LocalDateTime localDateTime = redisData.getLocalDateTime();
        if (localDateTime.isAfter(LocalDateTime.now())){
            //如果没有过期
            //直接返回数据
            return data;
        }
        //创建线程池
        ExecutorService pool = Executors.newFixedThreadPool(10);
        //过期了,判断是否可以获取锁
        String lock = "getLock";
        boolean b = tryLock(lock);
        if (b){
            //如果获取锁成功,
            pool.submit(() -> {
                //从数据库中获取数据,再将数据写回缓存中
                try {
                    R apply = function.apply(id);
                    setWithLogicalExpire(key,apply,time,unit);

                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放锁
                    unlock(lock);
                }
            });
        }
        return data;
    }

        5)完整 Redis 的工具类

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.example.bookproject20.pojo.RedisData;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

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

@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 timeUnit){
        stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(value),time,timeUnit);
    }

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

        String jsonString = JSON.toJSONString(redisData);
        stringRedisTemplate.opsForValue().set(key,jsonString);
        
    }

    //利用缓存空值解决缓存穿透
    public <R,ID> R queryWithPassThrough(String prefix,ID id,Class<R> type,Function<ID,R> function,Long time,TimeUnit unit ){

        String key = prefix + id;
        //判断在缓存中是否能命中
        String jsonString = stringRedisTemplate.opsForValue().get(key);


        if (StrUtil.isNotBlank(jsonString)){
            //反序列化
            return JSON.parseObject(jsonString, type);
        }

        if (jsonString != null){
            return null;
        }

        //查询数据库,且将数据信息写入到缓存中
        R apply = function.apply(id);
        //判断是否为空值
        if (apply == null){
            //如果为空
            //将其写进缓存中
            stringRedisTemplate.opsForValue().set(key,"",50,TimeUnit.SECONDS);
            return null;
        }
        //序列化
        String json = JSON.toJSONString(apply);
        //如果不为空
        stringRedisTemplate.opsForValue().set(key,json,time,unit);
        return apply;

    }

    //利用逻辑过期解决缓存击穿
    public <R,ID> R queryWithLogicalExpire(String prefix,ID id,Class<R> type,Function<ID,R> function,Long time,TimeUnit unit){
        String key = prefix + id;
        //判断在缓存中是否命中
        String s = stringRedisTemplate.opsForValue().get(key);
        //如果不存在,直接返回null
        if (s == null){
            return null;
        }
        //如果存在,还得判断是否过期
        //反序列化
        RedisData redisData = JSONUtil.toBean(s, RedisData.class);
        JSONObject d = (JSONObject) redisData.getData();
        R data = JSONUtil.toBean(d, type);

        LocalDateTime localDateTime = redisData.getLocalDateTime();
        if (localDateTime.isAfter(LocalDateTime.now())){
            //如果没有过期
            //直接返回数据
            return data;
        }
        //创建线程池
        ExecutorService pool = Executors.newFixedThreadPool(10);
        //过期了,判断是否可以获取锁
        String lock = "getLock";
        boolean b = tryLock(lock);
        if (b){
            //如果获取锁成功,
            pool.submit(() -> {
                //从数据库中获取数据,再将数据写回缓存中
                try {
                    R apply = function.apply(id);
                    setWithLogicalExpire(key,apply,time,unit);

                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放锁
                    unlock(lock);
                }
            });
        }
        return data;
    }


    //获取锁
    private boolean tryLock(String key){
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 200, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(aBoolean);
    }

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



}

        6)依赖:

        <!--fastJSON-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>

        <!--redis、redis连接池依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.17</version>
        </dependency>

        7)Redis 配置:

  data:
    redis:
      password: 你的redis密码
      host: 你的redis主机号,IP地址
      lettuce:
        pool:
          max-active: 10
          max-idle: 10
          min-idle: 1
          time-between-eviction-runs: 10s
      database: 0

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2100421.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DAC专用功能芯片TI DAC8562/8563

DAC8563具有 2.5V、4ppm/C 基准的 16 位、双通道、低功耗、超低短时脉冲波形干扰、缓冲电压输出 DAC。 DAC8562是一款16位、双通道、串行接口的DAC&#xff0c;采用SPI接口进行通信&#xff0c;具有内部参考电压、软件可编程增益和输出保护等功能。 一、DAC8563的主要参数 供…

JVM面试(二)内存区域划分

内存区划分 Java虚拟机在执行Java程序的过程中会把它锁管理的内存划分为若干个不同的数据区域。 这些区域有各自不同的用途&#xff0c;以及创建和销毁的时间。 有的区域随着虚拟机的进程一直存在&#xff0c;有的区域依赖用户线程的启动和结束而建立和销毁。 根据《Java虚拟…

YOLOv8 训练15种动物分类模型

1. 下载数据集 https://hyper.ai/datasets/31084 2. 进行训练 这里数据集没做拆分&#xff0c;训练测试都是同一个数据集。 train.py:python from ultralytics import YOLO from ultralytics.utils import DEFAULT_CFG from datetime import datetimecurrent_time datetim…

2.1CPU内部结构

&#x1f393; 微机原理考点专栏&#xff08;通篇免费&#xff09; 欢迎来到我的微机原理专栏&#xff01;我将帮助你在最短时间内掌握微机原理的核心内容&#xff0c;为你的考研或期末考试保驾护航。 为什么选择我的视频&#xff1f; 全程考点讲解&#xff1a;每一节视频都…

利用深度学习实现验证码识别-3-ResNet18

在当今数字化时代&#xff0c;验证码作为一种重要的安全验证手段&#xff0c;广泛应用于各种网络场景。然而&#xff0c;传统的验证码识别方法往往效率低下&#xff0c;准确率不高。今天&#xff0c;我们将介绍一种基于 ResNet18 的验证码识别方法&#xff0c;它能够高效、准确…

AI大模型优化技巧:参数高效微调(PEFT)与LoRA微调深度解析

1. Fine-tuning 相较于基础大模型动辄万卡的代价&#xff0c;微调可能是普通个人或者企业少数能够接受的后训练大模型(post-training)的方式。 微调是指在一个预训练模型(pre-training)的基础上&#xff0c;通过少量的数据和计算资源&#xff0c;对模型进行进一步训练&#x…

阿里巴巴数学竞赛成绩未公布:背后的权衡与期待

文 | 头部财经首席评论员白立新 发布 | 头部财经 top168.com 导语&#xff1a;2024 年阿里巴巴数学竞赛成绩迟未公布&#xff0c;引发广泛猜测。中专生姜萍的表现备受瞩目&#xff0c;达摩院陷入两难困境。这场竞赛结果的公布&#xff0c;关乎多方利益与社会影响&#xff0c;…

UML(ER) manual book

图形与符号 实体 真实世界的表示&#xff08;实物&#xff09;&#xff0c;负责数据的发送或者接收&#xff0c;通常使用矩形表示。 处理和加工 通常使用圆圈表示数据时如何被处理&#xff0c;比如下订单&#xff0c;付款等动作。 数据存储 通常使用两条平行线表示&…

碲化镉太阳能电池:绿色能源的新星,高效转换引领未来

随着全球对清洁能源需求的持续增长和技术的不断进步&#xff0c;碲化镉太阳能电池必将在未来的能源市场中占据重要地位。‌‌PicoQuant公司一直致力于碲化镉太阳能电池新材料、‌新工艺的探索与研发&#xff0c;充分利用其在时间分辨技术上的优势&#xff0c;‌为碲化镉太阳能电…

5、LVGL控件-滑轮、滑动条、圆弧

本篇文章目录导航 ♠♠ LVGL控件-滑轮、滑动条、圆弧 ♣♣♣♣ 一、LVGL 滑轮部件 ♦♦♦♦♦♦♦♦ 1.1 滑轮部件组成部分 ♦♦♦♦♦♦♦♦ 1.2 滑轮部件基本API ♦♦♦♦♦♦♦♦ 1.3 实验小演示 ♣♣♣♣ 二、LVGL 滑动条部件 ♦♦♦♦♦♦♦♦ 2.1 滑动条部件组成部分 ♦…

论文阅读:MambaVision: A Hybrid Mamba-Transformer Vision Backbone

论文地址&#xff1a;arxiv 摘要 作者提出了一种新型的混合 Mamba-Transformer 主干网络。通过重新设计 Mamba 公式&#xff0c;增强了其高效建模视觉特征的能力。 此外&#xff0c;作者还通过对 ViT 与 Mamba 消融研究&#xff0c;实验结果表明了&#xff1a;在最后几层为 …

“双碳”减排背景下企业自发电系统该具备哪些功能?

随着全球能源危机加剧、用能需求上升以及新能源技术的迅速发展&#xff0c;新能源发电的应用范围不断扩大&#xff0c;并逐步形成了新型能源与电力市场。然而&#xff0c;由于新能源的能量密度普遍较低&#xff0c;进行大规模发电时需精心挑选适合的位置&#xff0c;因此新能源…

【网络安全】服务基础第一阶段——第九节:Windows系统管理基础---- Windows_AD域

目录 一、域与活动目录 1.1 工作组 1.2 域 1.2.1 域&#xff08;Domain&#xff09; 1.2.2 域控制器&#xff08;Domain Controller&#xff0c;DC&#xff09; 1.2.3 功能和角色 1.2.4 管理和监控 1.2 5 域结构 1.3 组织单元&#xff08;Organizational Unit&#xff…

Seata 的4种事务模式(XA、AT、TCC、SAGA)

目录 前言 Seata架构 事务模式 XA AT TCC 区别 前言 在分布式系统中&#xff0c;实现一个功能可能需要由几个不同的服务来共同实现。这就会带来一个问题&#xff0c;不同的服务之间无法做到使用同一个事务&#xff0c;这就无法保证数据的一致性了。在一些对数据一致性要…

基于SSM的“基于决策树算法的大学生就业预测系统”的设计与实现(源码+数据库+文档)

基于SSM的“基于决策树算法的大学生就业预测系统”的设计与实现&#xff08;源码数据库文档) 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SSM 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 系统用户用例图 学校基础信息管理 毕业生基…

Java设计模式之装饰器模式详细讲解和案例示范

1. 引言 装饰器模式&#xff08;Decorator Pattern&#xff09;是一种结构型设计模式&#xff0c;它允许向现有对象添加新的功能&#xff0c;而无需修改其结构。这种模式通过使用组合而非继承来扩展对象的行为&#xff0c;在许多实际应用中极为常见。本文将详细介绍装饰器模式…

世界复合医学杂志社世界复合医学编辑部2024年第4期目录

论著 苏子降气汤联合三子养亲汤治疗痰浊壅肺型慢性阻塞性肺疾病急性加重期的临床疗效 周芹;周磊; 1-437 天麻钩藤汤加减联合依那普利叶酸片对原发性高血压患者血压水平与中医证候积分的影响 邹文博;王世雄; 5-8 伏诺拉生联合康复新液治疗反流性食管炎的临床研究 孙…

山体滑坡监测预警系统—百科分享

GNSS山体滑坡监测预警系统是一种利用全球导航卫星系统(GNSS)技术&#xff0c;对易发生山体滑坡的地段进行24小时不间断监测的先进系统。该系统能够实时记录易滑坡地段山体的各种变化情况&#xff0c;为灾害预警和防治提供科学依据。 GNSS山体滑坡监测预警系统通过在地表关键位置…

05-最新PyCharm安装详细教程及pycharm配置

一、PyCharm简介及其下载网站 PyCharm是由JetBrains打造的一款Python IDE&#xff08;Integrated Development Environment&#xff0c;集成开发环境&#xff09;&#xff0c;带有一整套可以帮助用户在使用Python语言开发时提高其效率的工具。PyCharm提供了代码编辑、调试、语…

700道学生百科知识题库ACCESS\EXCEL数据库

今天这个题库虽然记录数不多&#xff0c;但是题目很经典、精彩、精华&#xff0c;分7个难度级别&#xff0c;每个级别100题&#xff1b;分类也很多&#xff0c;包含&#xff1a;百科、常识、地理、动画、国学、化学、历史、旅游、美食、诗词、数学、体育、天文、文学、物理、星…