Redis02——缓存(缓存更新策略、缓存穿透、缓存雪崩、缓存击穿、缓存工具封装)

news2024/11/15 16:48:32

目录

缓存概念

添加Redis缓存

业务场景

缓存作用模型

java代码

缓存更新策略

主动更新的三种策略

主动更新——Cache Aside Pattern

实际应用

缓存穿透

概念

解决方法

实际应用

缓存雪崩

概念

解决方法

缓存击穿

互斥锁

介绍

 实际应用

逻辑过期

介绍

实际应用 

 互斥锁 VS 逻辑过期

缓存工具封装


缓存概念

缓存就是数据交换的缓冲区(称作Cache),是存储数据的临时地方,一般读写性能较高。缓存有多种类型,比如以下的几种:

  • 浏览器缓存:常见的是缓存静态资源到本地,如CSS、JS、图片等,这样就不用每次访问都去加载数据,大大降低了网络的延时,提高了页面的显示速度,提升用户体验
  • Tomcat 中应用层缓存:将数据库中的数据缓存到redis中,当有请求访问数据时,就会首先去redis中获取,如果redis中有需要的数据就可以直接返回,不需要再去访问数据库,只有redis查询不到数据时才去访问数据库。redis的读写速度很快,所以可以提高数据的响应速度
  • 数据库缓存:比如可以对索引进行缓存,如id,当根据id查询数据时,可以在内存中进行快速检索,而不需要去读取磁盘中,只有当缓存中找不到时才去读磁盘进行查询,效率也会提高
  • CPU缓存
  • 磁盘缓存

缓存的作用:

  • 降低后端负载
  • 提高读写效率,降低响应时间

缓存的成本:

  • 数据一致性成本:当更新数据库而还没来得及更新缓存时,此时缓存中的数据就是旧数据,就会产生和数据库中的数据不一致的问题
  • 代码维护成本:为了解决数据一致性问题,就会通过较为复杂的代码来维护,而且在数据一致性问题的处理过程中,也可能产生缓存穿透、击穿等问题,解决这些问题也会让代码复杂度提高,也就提高了代码维护的成本
  • 运维成本:为了解决缓存雪崩问题以及保证缓存的高可用性,缓存一般需要搭建集群,而集群的部署、维护等都会产生相应的成本

添加Redis缓存

业务场景

比如有一个后端接口是根据商家id查看商家详情信息,在不使用缓存时,这个后端接口的实现是直接根据传递过来的商家id去数据库查询商家详情信息,然后返回给前端,这里对这个接口用Redis做缓存

缓存作用模型

java代码

只包含service实现类的代码,因为主要业务逻辑都在这个类里

package com.hmdp.service.impl;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

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

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryById(Long id) {
        // 1、从Redis中查询商家信息
        String key = CACHE_SHOP_KEY + id;
        // 可以使用Redis中的Hash结构
//        stringRedisTemplate.opsForHash().entries("" + id);
        // 这里用String来演示
        String jsonStr = stringRedisTemplate.opsForValue().get(key);
        // 2、判断缓存是否命中,即Redis中是否有要查询的商家信息
        if (StrUtil.isNotBlank(jsonStr)) {
            // 2.1 命中,直接返回给前端
            // 先把JSON字符串转出java对象
            Shop shop = JSONUtil.toBean(jsonStr, Shop.class);
            return Result.ok(shop);
        }

        // 2.2 未命中,根据id去数据库里查询
        Shop shop = getById(id);
        // 2.1.1 判断数据库中该商家是否存在
        if (shop == null){
            // 2.1.1.1 数据库中不存在,返回404
            return Result.fail("店铺不存在");
        }

        // 2.1.1.2 数据库中存在,写入Redis,并返回给前端
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
        return Result.ok(shop);
    }
}

缓存更新策略

当更新数据库而没有更新缓存数据时,就会产生数据一致性问题,为了解决这个问题,就需要对缓存中的数据进行更新

有以下三种缓存更新策略:

  • 内存淘汰:当内存不足时,redis自带的内存淘汰机制就会淘汰掉一部分数据,当需要查询这部分数据时,就会去数据库查询,进而重新写入到缓存,也就实现了缓存的更新,在一定程度上保证了数据的一致性。但是这种方式是不可控的,我们不知道什么时候会进行淘汰,不知道淘汰的是哪些数据,有可能内存一直充足,那么就不会进行内存淘汰,获取到的数据就一直是旧数据。但维护成本很低,这是redis自带的功能,默认是开启的,不需要我们维护
  • 超时剔除:在向缓存写入数据的同时设置数据的超时时间,当时间到了就会自动删除数据,然后查询时缓存中没有就会去查询数据库并重新写入缓存,也就实现了缓存的更新。数据的一致性可靠程度可以通过设置超时时间的长短来控制,但是在超时时间没到之前,还是可能产生数据不一致的情况,但总的来说还是比内存淘汰可靠些,而且维护成本也很低,因为只需要在存入时设置一个超时时间即可
  • 主动更新:在数据库更新之后,通过编写代码主动更新缓存中的数据,这种方式的一致性比较好,但维护成本也相对较高,因为需要手动编写代码来进行维护,在业务逻辑复杂时,代码也会较为复杂,代码的维护成本也就会提高

主动更新的三种策略

  • Cache Aside Pattern:由缓存的调用者,在更新数据库的同时更新缓存,也就是自己写代码实现
  • Read/Write Through Pattern:缓存和数据库整合为一个服务,由服务来维护一致性,调用者使用该服务,无需关心缓存的一致性问题。即有一个现成的服务可以直接调用,调用者不需要关心它底层到底是怎么工作的。但是要维护这样的服务也是比较难的,而且一般市面上不容易找到这样的服务,自己开发维护成本会很高
  • Write Behind Caching Pattern:调用者只操作缓存,不关心数据库,由其他线程异步的将缓存数据持久化到数据库,也就是对数据的增删改查操作都是在缓存里进行,而操作结果的持久化由其他线程异步进行,保证一致性。好处是当对缓存中的数据更新N次时,只有第N次是有效的,刚好其他线程就在此时来进行数据持久化,就会把最后一次的缓存更新保持到数据库,即多次缓存更新结果只需一次持久化。坏处一是这个异步任务会比较复杂,需要实时监控缓存数据的变化;二是数据一致性不能保证,当缓存进行多次更新时,此时还未触发线程进行数据持久化,就会造成缓存和数据库数据不一致,如果此时缓存再出现问题,如宕机,数据就会丢失,且也没有进行数据的持久化

综上,第一种方式比较好,下面对第一种方式进行介绍

主动更新——Cache Aside Pattern

操作缓存和数据库时有三个问题需要考虑:

1、删除缓存还是更新缓存 

  • 更新缓存:每次更新数据库都更新缓存,无效写操作比较多。也就是说每次更新数据库,都会去更新一次缓存中对应的数据,但如果是写操作比较多,读操作比较少,比如更新了一百次数据库后,才会进行一次数据查询,那么就会更新一百次缓存,如果在前面的更新中都没有数据查询请求,只在第一百次更新完缓存之后,才有一次数据查询请求,那么就会将缓存中的结果返回(此时返回的是第一百更新后的最新数据),那么就会造成前面九十九次的缓存更新都白费,所以一般不用这种方法
  • 删除缓存,更新数据库时让缓存失效,查询时再更新缓存。每次更新完数据库,就把缓存中对应的数据设置为失效(可以通过设置超时时间来完成),并且没有对缓存中的数据马上进行更新,而是当有数据查询请求时,查询缓存时未命中,就会去查询数据库,然后把查询到数据写入缓存(此时才进行缓存更新)并返回

2、如何保证缓存与数据库的操作同时完成或同时失败

  • 对于单体系统:将缓存和数据库操作放在一个事务内即可
  • 对于分布式系统:利用TCC等分布式事务方案

3、先操作缓存还是先操作数据库(线程安全问题)

  • 先删除缓存,再操作数据库

这种情况下,在多线程并发时出现异常的概率还是比较大的,因为删除缓存,读缓存和查数据库(直接获取数据库数据)以及写入缓存的操作也很快,但是写入数据库(这里要组织数据,然后再进行插入操作,比直接从数据库获取数据要慢)的速度相对上述操作来说就很慢,所以很容易出现下图中的异常情况:

在线程1删除缓存(速度很快)之后,还没把更新数据库的操作完成(因为该操作比较耗时)的这段时间内,有另一个线程2来查找数据,此时因为已经删除了缓存,所以未命中就会去查找数据库然后写入缓存(读缓存->查数据库->写入缓存这三个操作加起来都比更新数据库操作块),此时缓存中再次被写入了旧数据10,而线程2更新完数据库后,数据库中的数据就变成了20,造成了数据不一致的情况

并且,可以从图中看出,出现异常情况之后,缓存中的数据就一直是旧数据,后续如果线程2或其他线程来获取数据,得到的就一直是缓存中的旧数据

  • 先操作数据库,再删除缓存

异常情况1:在线程1更新完数据库之后,删除缓存之前的这段时间里,有另一个线程2来查询数据,此时缓存还未删除,命中并返回旧数据10,然后线程1执行删除缓存操作,缓存变为空,此时也造成了数据不一致

但是,由于线程1在已经更新完数据库到删除缓存的这段时间非常短(因为耗时的写入数据库已经完成,而删除缓存速度非常块),所以在这段很短的时间里出现线程2的概率比较小。其次,就算真的发生这种异常情况,线程2第一次查询得到的是旧数据10,第二次来查询时缓存已经为空了,即未命中,那么就会去查找数据库然后写入缓存,此时得到的又会是最新的数据20,缓存中的数据也被更新为20,所以这种异常情况的代价是比较小的(线程2第一次查询返回的是旧数据)

 异常情况2:刚好缓存中的数据因为某些原因失效(如过期时间到了),可以理解成被删除,假设线程2查询数据未命中(数据已失效),去查询数据库得到10,并准备将10写入缓存,在这期间线程1进行更新数据库和删除缓存操作,然后线程2才执行写入缓存操作,如图所示 

这种情况的概率也很低,要同时满足两个条件:1、有两个进程并行执行;2、线程2查询时恰好缓存失效;同时有另一个线程1要在线程2写入缓存之前来执行更新数据库和删除缓存操作

从上面的《先删除缓存,再操作数据库》分析中知道,在第二个条件中,在线程2已经查询到数据库之后写入缓存之前的这段时间很短,而线程1的更新数据库以及删除缓存操作相比之下耗时更长(其实光更新数据库这一操作就已经很耗时了),所以要同时满足上面的两个条件概率就很低

假如这种概率很小的情况真的发生了,也可以通过设置超时时间,当超时时间到了就会删除旧数据

综上所述,虽然上面两种方式都有可能产生线程安全问题,但是先操作数据库再删除缓存发生的概率更小,所以选择先操作数据库再删除缓存

综上,缓存更新的最佳实践方案为:

实际应用

 java代码 

package com.hmdp.service.impl;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;
import static com.hmdp.utils.RedisConstants.CACHE_SHOP_TTL;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryById(Long id) {
        // 1、从Redis中查询商家信息
        String key = CACHE_SHOP_KEY + id;
        // 可以使用Redis中的Hash结构
//        stringRedisTemplate.opsForHash().entries("" + id);
        // 这里用String来演示
        String jsonStr = stringRedisTemplate.opsForValue().get(key);
        // 2、判断缓存是否命中,即Redis中是否有要查询的商家信息
        if (StrUtil.isNotBlank(jsonStr)) {
            // 2.1 命中,直接返回给前端
            // 先把JSON字符串转出java对象
            Shop shop = JSONUtil.toBean(jsonStr, Shop.class);
            return Result.ok(shop);
        }

        // 2.2 未命中,根据id去数据库里查询
        Shop shop = getById(id);
        // 2.1.1 判断数据库中该商家是否存在
        if (shop == null){
            // 2.1.1.1 数据库中不存在,返回404
            return Result.fail("店铺不存在");
        }

        // 2.1.1.2 数据库中存在,写入Redis,并返回给前端
        // CACHE_SHOP_TTL, TimeUnit.MINUTES:设置Redis过期时间为30分钟
        stringRedisTemplate.opsForValue()
                .set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return Result.ok(shop);
    }

    @Override
    @Transactional  // 事务注解
    public Result update(Shop shop) {
        // 该方法使用了事务注解,所以可以保证更新数据库操作好删除Redis缓存操作同时成功或同时失败
        Long id = shop.getId();
        if (id == null){
            return Result.fail("店铺id不存在");
        }
        // 更新数据库
        updateById(shop);
        // 删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
        return Result.ok();
    }
}

缓存穿透

概念

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,那么查询请求在缓存中找不到数据时就会直接去数据库查询,而数据库中也没有要查询的数据,所以就返回给客户端找不到的提示信息

这种情况下,如果有人恶意使用多线程来请求根本不存在的数据,那么这些请求就会让程序直接去数据库查询(因为redis,也就是缓存中没有),就会给数据库造成巨大压力

解决方法

  • 缓存空对象
    • 优点:实现简单,维护方便
    • 缺点:1、有额外的内存消耗(因为要对不存在的数据值存储为空,当有很多不存在的数据被请求时,这些数据都会被缓存为空,此时缓存中就会有很多没用的key缓存着空值,可以通过给key设置超时时间来解决);2、可能造成短期的数据不一致(比如第一次访问的某个数据确实不存在,将其存为空值,后来真的在数据库中插入该数据,而此时缓存中还是空值,就会产生数据不一致的情况,可以设置超时时间来解决或者当数据库更新时主动更新缓存)
  • 布隆过滤(具体是啥百度吧)
    • 优点:内存占用较少,没有多余的key(因为不用将不存在的数据缓存为空值)
    • 缺点:1、实现复杂(但Redis自带了一个布隆过滤,可以帮助简化开发);2、存在误判的可能(当告诉数据不存在时,是真的不存在,但当告诉存在时,不一定真的存在,此时还是有缓存穿透的风险)
  • 增强id的复杂度,避免被攻击者猜测到id的组成规律
  • 做好数据的基础格式校验(比如校验id是否符合格式,这也是上一条说的增强id复杂度的作用),只有数据符合要求才能访问
  • 加强用户的权限校验(是否已经登录,是否有权限访问)
  • 做好热点参数的限流,对于一些比较热门的请求接口,做限流处理,比如一些秒杀活动,限制用户的访问次数

实际应用

基于上面的代码和业务需求(根据id查询店铺详情信息)

流程图

java代码 

package com.hmdp.service.impl;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

import java.util.concurrent.TimeUnit;

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

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryById(Long id) {
        // 1、从Redis中查询商家信息
        String key = CACHE_SHOP_KEY + id;
        // 这里用String来演示
        String jsonStr = stringRedisTemplate.opsForValue().get(key);
        // 2、判断缓存是否命中,即Redis中是否有要查询的商家信息
        if (StrUtil.isNotBlank(jsonStr)) {
            // 2.1 命中,直接返回给前端
            // 先把JSON字符串转出java对象
            Shop shop = JSONUtil.toBean(jsonStr, Shop.class);
            return Result.ok(shop);
        }

        // 判断命中的是否是空值(缓存雪崩问题解决方案:缓存空值)
        if (jsonStr != null){  // 命中的是空值
            return Result.fail("店铺不存在");
        }

        // 2.2 未命中,根据id去数据库里查询
        Shop shop = getById(id);
        // 2.1.1 判断数据库中该商家是否存在
        if (shop == null){
            // 缓存空值(缓存雪崩问题解决方案),并设置较短的过期时间
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 2.1.1.1 数据库中不存在,返回404
            return Result.fail("店铺不存在");
        }

        // 2.1.1.2 数据库中存在,写入Redis,并返回给前端
        // CACHE_SHOP_TTL, TimeUnit.MINUTES:设置Redis过期时间为30分钟
        stringRedisTemplate.opsForValue()
                .set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return Result.ok(shop);
    }
}

缓存雪崩

概念

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,而此时有大量的请求来访问这些丢失的数据,那么在Redis中没有,这些请求就会直接到达数据库,给数据库带来巨大压力

解决方法

  • 给不同的key设置不同的过期时间,比如过期时间原本要设置为30分钟,可以在这30分钟的基础上随机加上一个时间值,如随机值为3到5分钟(也可以是其他值),这样就不会让多个key同时过期失效(针对同一时段大量的缓存key同时失效的情况)
  • 利用Redis集群提高服务的可用性(避免Redis服务宕机的情况)
  • 给缓存业务添加降级限流策略,如当Redis宕机时,有客户端来请求数据时,一律返回类似于“服务暂时不可用”等提示信息,拒绝服务(Redis真的宕机且无法恢复的情况)
  • 给业务添加多级缓存,如添加浏览器缓存、反向代理Nginx缓存、Redis缓存、JVM缓存、数据库缓存

缓存击穿

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

常见的解决方法有两种:互斥锁和逻辑过期

互斥锁

介绍

当有一个线程1开始重建key时,就用锁锁住,这样当其他线程也想重建key时,就要等待线程1释放锁,这样就不会出现多个线程都重建key的情况

下图的过程是:线程1最先要获取数据,去查询缓存,未命中,开始获取锁,获取锁成功之后开始查询数据库重建缓存数据,将数据写入缓存中,最后释放锁。而在线程1获取锁成功之后,线程2也来查询数据(和线程1要查询的数据相同),去查询缓存,未命中,开始获取锁,获取失败(因为已经被线程1先获取到了),休眠一会,然后重新去缓存中获取数据,如果缓存中已经有了(线程1已经写入)直接返回,如果缓存还是未命中,就去获取锁,还是失败,继续休眠....一直重复,直到缓存中有数据或者锁被释放

互斥锁存在的问题:多个没有拿到锁的线程会一直处于等待状态,如果key构建的时间比较久,那这些线程的等待时间也就会变长,响应速度也就会比较慢,性能就会比较差。而且有可能存在死锁的情况,比如构建一个key需要获取多个锁,而这些锁被不同的线程获取到,它们就会相互等待没有获取到的锁,就会造成死锁现象

 实际应用

业务需求及流程图描述

java代码

代码放在下面逻辑过期中,因为分别把互斥锁和逻辑过期两种实现方式封装成了一个方法

逻辑过期

介绍

不直接给缓存中的key设置过期时间TTL,而是在要缓存的数据中增加一个字段,用这个字段标明过期时间,这个字段一般是用当前时间(也就是向缓存中存入数据时的时间)加上真正要设置的过期时间(如30分钟)得到的,这个字段就是逻辑过期时间

因为没有给key设置实际的过期时间TTL,再配合一些合适的内存淘汰策略,那么理论上key一旦存入Redis就会永不过期,也就是一直能从缓存中获取到,如果想删除掉这个key,再手动进行删除

实际应用 

业务需求及流程图如下

java实现代码

测试类代码,提前向缓存中存入数据

package com.hmdp;

import com.hmdp.service.impl.ShopServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

@SpringBootTest
class HmDianPingApplicationTests {
    @Resource
    private ShopServiceImpl shopService;

    @Test
    void testSaveShop() throws InterruptedException {
        shopService.saveShop2Redis(1l, 10l);
    }
}

RedisData类

package com.hmdp.utils;

import lombok.Data;

import java.time.LocalDateTime;

/**
 * 为了不改动原有的Shop类的代码,在此重新定义一个数据类
 * 该类的expireTime字段就表示逻辑过期时间
 * data字段表示原有的数据,在这里指的是Shop类对象,即店铺信息
 * 也就是通过该类在原有数据的基础上添加一个逻辑过期时间
 * 根据不同的业务需要,data可以表示任意的数据,因为器类型是Object
 */
@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}

 ShopServiceImpl类中的代码

package com.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisData;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

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

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Resource
    private StringRedisTemplate stringRedisTemplate;


    /**
     * 根据id查询店铺详情信息
     *
     * @param id 要查询的店铺id
     * @return 返回店铺信息
     */
    @Override
    public Result queryById(Long id) {
        // 缓存空值解决缓存穿透
//        Shop shop = queryWithPassThrough(id);

        // 互斥锁解决缓存击穿
//        Shop shop = queryWithPassMutex(id);
//        if (shop == null) {
//            return Result.fail("店铺不存在");
//        }

        // 逻辑过期解决缓存击穿
        Shop shop = queryWithLogicalExpire(id);
        if (shop == null) {
            return Result.fail("店铺不存在");
        }
        return Result.ok(shop);
    }

    /**
     * 互斥锁解决缓存击穿
     *
     * @param id 要查询的店铺id
     * @return 店铺信息
     */
    public Shop queryWithPassMutex(Long id) {
        // 从Redis中查询商家信息
        Map<String, Object> stringObjectMap = queryShopFromCache(id);
        Boolean isExist = (Boolean) stringObjectMap.get("isExist");
        if (BooleanUtil.isTrue(isExist)) {
            // 缓存中存在数据,直接返回
            return (Shop) stringObjectMap.get("shop");
        }

        // 缓存未命中数据
        String lockKey = LOCK_SHOP_KEY + id;
        Shop shop = null;
        try {
            // 尝试获取锁
            boolean isGetLock = tryGetLock(lockKey);
            if (!isGetLock) {
                // 获取锁失败
                // 休眠等待
                Thread.sleep(80);
                // 休眠结束之后重新查询,即递归调用queryWithPassMutex方法
                return queryWithPassMutex(id);
            }
            // 获取锁成功
            // 再次判断缓存中是否已经有数据
            Map<String, Object> stringObjectMap2 = queryShopFromCache(id);
            Boolean isExist2 = (Boolean) stringObjectMap2.get("isExist");
            if (BooleanUtil.isTrue(isExist2)) {
                // 缓存中存在数据,直接返回
                return (Shop) stringObjectMap2.get("shop");
            }

            // 开始缓存重建
            // 缓存未命中数据
            String key = CACHE_SHOP_KEY + id;
            // 根据id去数据库里查询
            shop = getById(id);
            // 模拟缓存重建的延时
            Thread.sleep(200);
            // 判断数据库中该商家是否存在
            if (shop == null) {
                // 缓存空值(缓存雪崩问题解决方案),并设置较短的过期时间
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                // 数据库中不存在,返回404
                return null;
            }

            // 数据库中存在,写入Redis,并返回给前端
            // CACHE_SHOP_TTL, TimeUnit.MINUTES:设置Redis过期时间为30分钟
            stringRedisTemplate.opsForValue()
                    .set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 释放锁
            releaseLock(lockKey);
        }
        return shop;
    }

    /**
     * 判断缓存中是否命中数据
     *
     * @param id 要查询的数据id
     * @return 返回一个map集合,表示是否命中数据和命中的数据内容
     */
    private Map<String, Object> queryShopFromCache(Long id) {
        HashMap<String, Object> map = new HashMap<>();
        // 从Redis中查询商家信息
        String key = CACHE_SHOP_KEY + id;
        String jsonStr = stringRedisTemplate.opsForValue().get(key);
        // 判断缓存是否命中,即Redis中是否有要查询的商家信息
        if (StrUtil.isNotBlank(jsonStr)) {
            // 命中,直接返回给前端
            // 先把JSON字符串转出java对象
            Shop shop = JSONUtil.toBean(jsonStr, Shop.class);
            map.put("isExist", true);
            map.put("shop", shop);
            return map;
        }

        // 判断命中的是否是空值(缓存雪崩问题解决方案:缓存空值)
        if (jsonStr != null) {  // 命中的是空值
            map.put("isExist", true);
            map.put("shop", null);
            return map;
        }

        // 未命中,返回false和null
        map.put("isExist", false);
        map.put("shop", null);
        return map;
    }

    /**
     * 在实际业务中,热点key一般都是通过后台系统提前添加进Redis里
     * 这里用单元测试模拟一下后台管理,提前向Redis中存入热点key
     *
     * @param id            店铺的id
     * @param expireSeconds 逻辑过期时间,单位秒
     */
    public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException {
        // 查询数据库获取店铺信息
        Shop shop = getById(id);
        // 模拟重建缓存的延时
        Thread.sleep(200);
        RedisData redisData = new RedisData();
        // 设置Shop数据
        redisData.setData(shop);
        // 设置过期时间
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        // 存入Redis,注意Redis没有设置TTL
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }

    // 定义一个线程池
    private final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 逻辑过期解决缓存击穿
     *
     * @param id 要查询的店铺id
     * @return 店铺信息
     */
    public Shop queryWithLogicalExpire(Long id) {
        // 从Redis中查询商家信息
        String key = CACHE_SHOP_KEY + id;
        String jsonStr = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(jsonStr)) {
            // 未命中数据,直接返回空
            return null;
        }

        // 命中数据,判断数据是否过期
        // 把json反序列化成java对象
        RedisData redisData = JSONUtil.toBean(jsonStr, RedisData.class);
        // redisData.getData()由于是Object类型,在反序列化的时候会反序列化为JSONObject
        // 所以需要再次将JSONObject反序列化为Shop
        // Shop shop = (Shop) redisData.getData();
        // 我觉得redisData.getData()得到的是一个Object对象,可以把Object强转成Shop
        // 但是实际上不可以,会报错:cn.hutool.json.JSONObject cannot be cast to com.hmdp.entity.Shop
        // 具体为啥不能直接强转成Shop我还没弄明白
        JSONObject jsonObj = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(jsonObj, Shop.class);

        // 逻辑时间没有过期
        if (LocalDateTime.now().isBefore(redisData.getExpireTime())) {
            // 当前时间在逻辑过期时间之前 => 逻辑时间没有过期,直接返回数据
            return shop;
        }

        /* 逻辑时间过期 */
        String lockKey = LOCK_SHOP_KEY + id;
        // 获取锁
        if (tryGetLock(lockKey)) {
            // 获取锁成功,需要再次判断缓存中的数据是否过期
            /*
            因为在多线程的情况下,很有可能出现这一种情况:
                线程1判断数据逻辑过期,并且获得锁成功,它会新建一个线程2来重建key,然后线程1返回旧数据
                在线程2重构完成释放锁之前,有另一个线程3判断缓存中的数据过期开始往下尝试获得锁
                切好此时线程2构建完成(此时缓存中已经是新数据了)释放锁,释放掉的锁被线程3拿到
                如果不进行二次判断缓存中的数据是否过期,那么线程3又会再次去重建key,但是此时缓存中的
                数据并没有过期(因为刚刚线程2已经重建好了)
                所以为了避免重复重建缓存,就需要再次进行判断
            */
            String jsonStr2 = stringRedisTemplate.opsForValue().get(key);
            RedisData redisData2 = JSONUtil.toBean(jsonStr2, RedisData.class);
            if (LocalDateTime.now().isBefore(redisData2.getExpireTime())) {
                // 缓存中的数据未过期,直接返回
                return JSONUtil.toBean((JSONObject) redisData2.getData(), Shop.class);
            }
            // 获取锁成功,开启新线程重建缓存数据
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 重建缓存
                    this.saveShop2Redis(id, 10l);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放锁
                    releaseLock(lockKey);
                }
            });
        }


        // 无论获取锁成功还是失败,都要返回旧数据
        // 返回旧数据
        return shop;
    }


    /**
     * 尝试获取锁
     *
     * @param key 代表锁的键
     * @return 返回布尔值,是否获取锁成功
     */
    private boolean tryGetLock(String key) {
        // 设置锁并设置超时时间为10秒
        // setnx命令就对应着java中的setIfAbsent方法
        // 返回一个Boolean类型的变量
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        // return flag  // 不建议直接返回flag,因为flag会被自动拆箱成boolean,在拆箱的过程中可能出现空指针异常
        // 使用工具类BooleanUtil进行返回
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁,即删除key为lock的数据
     *
     * @param key 表示锁的键
     */
    private void releaseLock(String key) {
        stringRedisTemplate.delete(key);
    }

    /**
     * 用缓存空值的方式解决缓存穿透
     *
     * @param id 要查询的店铺id
     * @return 店铺的信息
     */
    public Shop queryWithPassThrough(Long id) {
        // 从Redis中查询商家信息
        String key = CACHE_SHOP_KEY + id;
        // 这里用String来演示
        String jsonStr = stringRedisTemplate.opsForValue().get(key);
        // 判断缓存是否命中,即Redis中是否有要查询的商家信息
        if (StrUtil.isNotBlank(jsonStr)) {
            // 命中,直接返回给前端
            // 先把JSON字符串转出java对象
            return JSONUtil.toBean(jsonStr, Shop.class);
        }

        // 判断命中的是否是空值(缓存雪崩问题解决方案:缓存空值)
        if (jsonStr != null) {  // 命中的是空值
            return null;
        }

        // 根据id去数据库里查询
        Shop shop = getById(id);
        // 判断数据库中该商家是否存在
        if (shop == null) {
            // 缓存空值(缓存雪崩问题解决方案),并设置较短的过期时间
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 数据库中不存在,返回404
            return null;
        }

        // 数据库中存在,写入Redis,并返回给前端
        // CACHE_SHOP_TTL, TimeUnit.MINUTES:设置Redis过期时间为30分钟
        stringRedisTemplate.opsForValue()
                .set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);

        return shop;
    }
}

 互斥锁 VS 逻辑过期

这两种方式都是解决缓存重建key这段时间内产生的并发问题,优缺点如下

缓存工具封装

根据上述所说的,简单的封装一个缓存工具类,工具类要求如下

注意,如果是要调用逻辑过期的方法,要提前向缓存中存入数据,这里封装工具类和上面逻辑过期中的代码差不多,就是修改成了通用的写法

调用工具类

package com.hmdp.service.impl;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.CacheClient;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

import java.util.concurrent.TimeUnit;

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

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Resource
    private CacheClient cacheClient;

    /**
     * 根据id查询店铺详情信息
     *
     * @param id 要查询的店铺id
     * @return 返回店铺信息
     */
    @Override
    public Result queryById(Long id) {
        // 缓存空值解决缓存穿透
        // lambda表达式:id2 -> getById(id2) 可以简化为:this::getById
//        Shop shop = cacheClient.queryWithPassThrough(
//                CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
//        if (shop == null) {
//            return Result.fail("店铺不存在");
//        }
//        return Result.ok(shop);

        // 逻辑过期时间解决缓存击穿
        // 注意提前使用单元测试向缓存写入数据,还有为了测试方便,把逻辑过期时间设置为30秒
        Shop shop = cacheClient.queryWithLogicalExpire(
                CACHE_SHOP_KEY, LOCK_SHOP_KEY, id, Shop.class, this::getById, 30l, TimeUnit.SECONDS);
        if (shop == null) {
            return Result.fail("店铺不存在");
        }
        return Result.ok(shop);
    }
}

工具类具体代码

package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
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;

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

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

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 将任意类型对象缓存到Redis中,并设置缓存过期时间,key的类型为String
     *
     * @param key   Redis的键
     * @param value 要缓存的对象,因为可以保存任意类型,所以这里定义value的类型为Object
     * @param time  缓存过期时间
     * @param unit  时间单位
     */
    public void set(String key, Object value, Long time, TimeUnit unit) {
        // 将java对象序列化为JSON
        String jsonStr = JSONUtil.toJsonStr(value);
        // 存入Redis
        stringRedisTemplate.opsForValue().set(key, jsonStr, time, unit);
    }

    /**
     * 将任意类型对象缓存到Redis中,并设置逻辑过期时间,key的类型为String
     *
     * @param key   Redis的键
     * @param value 要缓存的对象,因为可以保存任意类型,所以这里定义value的类型为Object
     * @param time  逻辑过期时间
     * @param unit  时间单位
     */
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
        // 实例化一个RedisData对象,其中对象的data存储具体的数据,expireTime存储逻辑过期时间
        RedisData redisData = new RedisData();
        // 保存逻辑过期时间
        // unit.toSeconds(time) 因为不确定时间单位是多少,但是这里统一转换成秒
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        redisData.setData(value);
        // 存入Redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 根据id查询数据(用缓存空值解决缓存穿透问题)
     *
     * @param keyPrefix key的前缀
     * @param id        查询对象的id
     * @param type      查询对象的类型
     * @param func      查询对象的数据库方法
     * @param time      缓存过期时间
     * @param unit      时间单位
     * @param <R>       对象类型,因为不知道调用者要查询什么对象,所以这里用泛型
     * @param <ID>      对象id的类型,同理,不知道id的类型,这里用泛型
     * @return 返回查询到的对象数据
     */
    public <R, ID> R queryWithPassThrough(
            String keyPrefix, ID id, Class<R> type,
            Function<ID, R> func, Long time, TimeUnit unit) {
        // 拼接缓存的key
        String key = keyPrefix + id;
        // 查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(json)) {
            // 缓存命中,直接返回值
            return JSONUtil.toBean(json, type);
        }

        if (json != null) {
            // 缓存命中,只是命中的是缓存的空值(缓存空值解决缓存穿透)
            return null;
        }

        // 缓存未命中,查询数据库
        /*
        这里由于是工具类,使用该工具类的可以是任意对象,所以在查找数据库时不能明确知道是查询哪张表
        也就不知道该调用哪个数据库方法来查询,所以需要调用者将查询数据库的方法通过参数传递过来
        即函数式编程(我个人觉得也可以理解为回调)
        */
        R r = func.apply(id);
        if (r == null) {
            // 如果数据库查不到,就缓存空值并设置过期时间,防止缓存穿透
            stringRedisTemplate.opsForValue().set(key, "", time, unit);
            return null;
        }
        // 将数据库查询结果缓存到Redis
//        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(r), time, unit);
        // 也可以调用上述定义好的方法
        this.set(key, r, time, unit);
        return r;
    }

    // 定义一个线程池
    private final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 根据id查询数据(用逻辑过期解决缓存击穿问题)
     *
     * @param keyPrefix     店铺key的前缀
     * @param keyLockPrefix 店铺锁key的前缀
     * @param id            店铺id
     * @param type      查询对象的类型
     * @param func      查询对象的数据库方法
     * @param time      逻辑过期时间
     * @param unit      时间单位
     * @param <R>       对象类型,因为不知道调用者要查询什么对象,所以这里用泛型
     * @param <ID>      对象id的类型,同理,不知道id的类型,这里用泛型
     * @return 返回查询到的对象数据
     */
    public <R, ID> R queryWithLogicalExpire(
            String keyPrefix, String keyLockPrefix, ID id, Class<R> type,
            Function<ID, R> func, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(json)) {
            // 缓存未命中,直接返回空
            // 因为针对热点key且采用逻辑过期时间,那么就不可能取不到数据,真的取不到那就是真的没有,直接返回空
            return null;
        }
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        LocalDateTime expireDate = redisData.getExpireTime();
        JSONObject jsonObject = (JSONObject) redisData.getData();
        R r = JSONUtil.toBean(jsonObject, type);
        if (LocalDateTime.now().isBefore(expireDate)) {
            // 当前时间在逻辑过期时间之前 => 数据未过期,直接返回
            return r;
        }

        // 逻辑时间过期,尝试获取锁
        String lockKey = keyLockPrefix + id;
        if (getLock(lockKey)) {
            // 获取锁成功,再次判断缓存中数据的逻辑时间是否过期,原因已经在之前说明
            String json2 = stringRedisTemplate.opsForValue().get(key);
            if (StrUtil.isBlank(json2)) {
                return null;
            }
            RedisData redisData2 = JSONUtil.toBean(json2, RedisData.class);
            if (LocalDateTime.now().isBefore(redisData2.getExpireTime())) {
                // 缓存中的数据未过期,直接返回
                return JSONUtil.toBean((JSONObject) redisData2.getData(), type);
            }
            // 新开一个线程,重建缓存
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 查询数据库
                    R r1 = func.apply(id);
                    // 模拟重建延时
                    Thread.sleep(200);
                    // 写入缓存
//                    RedisData redisData1 = new RedisData();
//                    redisData1.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
//                    redisData1.setData(r1);
//                    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData1));
                    // 调用本工具类提供的方法写入缓存
                    this.setWithLogicalExpire(key, r1, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放锁
                    unLock(lockKey);
                }
            });
        }
        // 无论获取锁成功还是失败,都要返回旧数据
        return r;
    }

    /**
     * 尝试获取锁
     *
     * @param key 代表锁的键
     * @return 返回布尔值,是否获取锁成功
     */
    private boolean getLock(String key) {
        // 设置锁并设置超时时间为LOCK_SHOP_TTL秒
        // setnx命令就对应着java中的setIfAbsent方法
        // 返回一个Boolean类型的变量
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        // return flag  // 不建议直接返回flag,因为flag会被自动拆箱成boolean,在拆箱的过程中可能出现空指针异常
        // 使用工具类BooleanUtil进行返回
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁,即删除key为lock的数据
     *
     * @param key 表示锁的键
     */
    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }
}

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

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

相关文章

【单片机毕业设计选题24101】-基于单片机的车载事故报警系统

系统功能: 系统上电后&#xff0c;OLED显示“欢迎使用请稍后”两秒后显示“Wait SIM900A”, SIM900A模块初始化OK后进入正常界面显示。 第一行显示采集到的温湿度值 第二行显示系统状态&#xff08;OK或Alarm&#xff09; 第三行显示经度值 第四行显示纬度值 注意经纬度信…

dll文件丢失怎么恢复?超简单的5个方法,1分钟搞定dll文件修复!

DLL&#xff0c;或称动态链接库&#xff0c;是一种重要的文件类型&#xff0c;包含了一系列用于运行几乎所有程序的指令&#xff0c;这些程序在win11、win10、win8和win7系统中都广泛使用。如果Windows操作系统中的dll文件丢失&#xff0c;您可能无法正常启动所需的程序或应用。…

劳易测高防护等级的读码系统提升仓储效率

在现代物流与仓储管理领域&#xff0c;条码识别和数据交换系统已经成为实现智能仓储管理的关键技术。面对特定的工业环境挑战&#xff0c;比如腐蚀性气雾等恶劣条件&#xff0c;具备高防护等级和抗腐蚀能力的条码系统显得尤为重要。今天&#xff0c;小易将为您带来创新的解决方…

Zabbix中文乱码问题解决方案

WinR打开运行&#xff0c;输入fonts&#xff0c;回车进入Windows字体目录&#xff0c;找到微软雅黑-常规字体&#xff0c;复制出来将文件名修改为msyh.ttf&#xff0c;注意后缀ttf 将msyh.ttf上传到服务器zabbix字体目录中&#xff1a;/usr/share/zabbix/fonts/ 注意文件权限 …

代码随想录训练营第五十二天 孤岛的总面积

第一题&#xff1a;孤岛的总面积 第二题&#xff1a;沉没孤岛 思路&#xff1a; 将所有在边界的岛屿所在的visited数组位置都置为true&#xff0c;剩下的visited[i][j] true && grid[i][j] 1的位置就是孤岛&#xff0c;将其置为1即可。 代码如下 #include <io…

WiFi to Ethernet: 树莓派共享无线连接至有线网口,自动通过Captive Poartal网页登录认证

物联网开发系列&#xff1a;物联网开发之旅① WiFi to Ethernet: 树莓派共享无线连接至有线网口&#xff0c;自动通过Captive Poartal验证物联网开发番外篇之 Captive Portal验证原理 文章目录 背景实现工具实现细节一、将无线连接共享到以太网1. 配置静态IP地址2. 启用IP转发3…

Chainlit快速实现AI对话应用的聊天记录如何持久性保存

前言 Chainlit 可以设置聊天记录用户搜索和浏览过去的对话。 如何实现 要启用聊天历史记录,您需要启用: 数据持久性身份验证恢复对话 为了让用户继续持久对话,请使用cl.on_chat_resume 生命周期钩子 装饰器使用户能够继续对话。需要同时启用数据持久性和身份验证。 该…

day21(mysql用户创建与授权、角色创建)

1.环境准备 [rootmysql ~]# rm -rf /etc/my.cnf //清空/etc目录下的my.cnf [rootmysql ~]# yum -y remove mariadb //移除mariadb [rootmysql ~]# find / -name "*mysql*" -exec rm -rf {} \; //删除mysql所有遗留 文件 2.安装绿色mysql [rootmysql ~]# t…

安装svd模型

svd模型简介 Stable Video Diffusion模型基于潜在的视频扩散模型&#xff0c;通过在小型、高质量的视频数据集上插入时间层并进行微调&#xff0c;将传统的2D图像合成模型转化为生成视频模型。这种方法的优点在于&#xff0c;它能够生成高分辨率的视频&#xff0c;并且具有强大…

C++基础编程100题-034 OpenJudge-1.4-15 最大数输出

更多资源请关注纽扣编程微信公众号 http://noi.openjudge.cn/ch0104/15/ 描述 输入三个整数,输出最大的数。 输入 输入为一行&#xff0c;包含三个整数&#xff0c;数与数之间以一个空格分开。 输出 输出一行&#xff0c;包含一个整数&#xff0c;即最大的整数。 样例…

【AI】关于AI和手机

2011 年至2015 年期间&#xff0c;全球智能手机出货量年增长率均超过两位数&#xff0c;显示出强劲的市场需 求和快速扩张趋势。然而&#xff0c;自2016 年起&#xff0c;全球智能手机用户数量趋于饱和&#xff0c;换机周期也逐 渐变长&#xff0c;市场进入存量替换阶段&#x…

人工智能时代,程序员如何保持核心竞争力?

人工智能时代&#xff0c;程序员如何保持核心竞争力&#xff1f; 随着AIGC&#xff08;如chatgpt、midjourney、claude等&#xff09;大语言模型接二连三的涌现&#xff0c;AI辅助编程工具日益普及&#xff0c;程序员的工作方式正在发生深刻变革。有人担心AI可能取代部分编程工…

FreeRTOS基础入门——RTOS背景知识简介(一)

个人名片&#xff1a; &#x1f393;作者简介&#xff1a;嵌入式领域优质创作者&#x1f310;个人主页&#xff1a;妄北y &#x1f4de;个人QQ&#xff1a;2061314755 &#x1f48c;个人邮箱&#xff1a;[mailto:2061314755qq.com] &#x1f4f1;个人微信&#xff1a;Vir2025WB…

必备:2024年四大文件翻译神器盘点!

在现代社会&#xff0c;语言不再是交流的障碍&#xff0c;文件翻译工具的出现&#xff0c;让不同语言之间的信息传递变得轻松自如。以下是几款备受推崇的文件翻译工具&#xff0c;它们将助大家轻松应对各种语言挑战&#xff0c;提升工作效率。 在当今的职场环境中&#xff0c;…

有没有性价比高的蓝牙耳机推荐?盘点4款开放式耳机排行榜10强

那当然是开放式蓝牙耳机性价比高呀&#xff0c;说到开放式蓝牙耳机的性价比呢&#xff0c;那我们可以先了解它的价位&#xff0c;它的价位大致可以分为三个价位。 低价位区间&#xff08;499元以下&#xff09;&#xff1a; 这个价格区间的开放式耳机通常受成本限制&#xff…

使用Step Functions运行AWS Backup时必备的权限要点

引言 在尝试从Step Functions执行AWS Backup的按需备份时&#xff0c;我在权限方面遇到了一些困难。为了备忘&#xff0c;我将这些经验写成这篇文章。 概述 从Step Functions执行AWS Backup时&#xff0c;需要分配以下权限&#xff1a; AWS Backup相关权限 执行备份的权限…

培训第二十一天(mysql用户创建与授权、角色创建)

上午 1、环境准备 [rootmysql ~]# rm -rf /etc/my.cnf //清空/etc目录下的my.cnf[rootmysql ~]# yum -y remove mariadb //移除mariadb[rootmysql ~]# find / -name "*mysql*" -exec rm -rf {} \; //删除mysql所有遗留文件 2、安装mysql绿包 [rootmysql ~]…

c->c++(四):gtest

本文主要探讨gtest相关内容。 gtest安装 wget -O gtest.zip https://github.com/google/googletest/archive/refs/heads/main.zipunzip gtest.zipcd googletest-mainmkdir bulid && cd buildcmake .. && make && make install gtest API TEST/TEST…

MySQL update set语句中 逗号与and的区别

语法 以下是 UPDATE 命令修改 MySQL 数据表数据的通用 SQL 语法&#xff1a; UPDATE table_name SET column1 value1, column2 value2, ... WHERE condition; 参数说明&#xff1a; table_name 是你要更新数据的表的名称。column1, column2, ... 是你要更新的列的名称。v…

减速电机的减速比计算原理

今天因为看到减速电机的内部中有通过齿轮来增加扭矩的结构&#xff0c;想找点原理性的文章来看一下&#xff0c;奈何遍寻不得。故提笔于此记录&#xff0c;以备往后回看。 图1 减速电机的竖截面剖视图 减速比为64&#xff0c;我的理解是在初始的齿轮转一圈的时候&#xff0c;最…