Redis——某马点评day02——商铺缓存

2023-12-07 15:30

本文主要是介绍Redis——某马点评day02——商铺缓存,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

什么是缓存

添加Redis缓存

添加商铺缓存

Controller层中

    /*** 根据id查询商铺信息* @param id 商铺id* @return 商铺详情数据*/@GetMapping("/{id}")public Result queryShopById(@PathVariable("id") Long id) {return shopService.queryById(id);}

Service层中

 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result queryById(Long id) {String key="cache:shop:" + 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.不存在,根据id查询数据库Shop shop = getById(id);if (shop==null) {//5.不存在,返回错误return Result.fail("店铺不存在");}//6.存在,写入RedisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));//7.返回return Result.ok(shop);}
}

练习添加店铺类型缓存

Controller层中

@RestController
@RequestMapping("/shop-type")
public class ShopTypeController {@Resourceprivate IShopTypeService typeService;@GetMapping("list")public Result queryTypeList() {return typeService.queryTypeList();}
}

Service层中

    @Overridepublic Result queryTypeList() {String key="cache:shopType";//1.从Redis查询缓存String shopType = stringRedisTemplate.opsForValue().get(key);//2.判断是否存在if (StrUtil.isNotBlank(shopType)) {//3.存在,直接返回List<ShopType> typeList = JSONUtil.toList(shopType, ShopType.class);return Result.ok(typeList);}//4.不存在,查询数据库List<ShopType> typeList = query().orderByAsc("sort").list();//5.存在,写入RedisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(typeList));//7.返回return Result.ok(typeList);}

缓存更新策略

 通常选择的方案都是第一种

单体系统可以通过@Transactional注解完成事务。

通常是先操作数据库,再删除缓存,出现问题的几率极小。

 

 实现商铺缓存和数据库的双写一致

第一个地方,写入Redis时加上超时时间。 

  //6.存在,写入RedisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);

 第二个地方

controller中

    /*** 更新商铺信息* @param shop 商铺数据* @return 无*/@PutMappingpublic Result updateShop(@RequestBody Shop shop) {return shopService.update(shop);}

service中

    @Overridepublic Result update(Shop shop) {Long id = shop.getId();if(id==null){return Result.fail("店铺id不能为空");}//1.更新数据库updateById(shop);//2.删除缓存stringRedisTemplate.delete(CACHE_SHOP_KEY+id);return null;}

缓存穿透

布隆过滤器的实现不是真的存储数据,而是用某种Hash算法计算之后用二进制压缩之类的方法保存是否存在。但是,也有可能多个数据hash值相同导致错误结果。

编码解决商铺查询的缓存穿透(缓存空对象做法)

 代码修改

    @Overridepublic 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.不存在,根据id查询数据库Shop shop = getById(id);if (shop==null) {//将空值写入RedisstringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);//5.不存在,返回错误return Result.fail("店铺不存在");}//6.存在,写入RedisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);//7.返回return Result.ok(shop);}

 限流可以用sentinel实现.

缓存雪崩

宕机时降级限流也是用sentinel实现。

nginx缓存也是一级缓存.

tmd,一直在说springcloud里面有讲。

缓存击穿

常见解决方案

这里可以参考一下redisson的源码设计思路,设计一个监听通知机制! 

逻辑过期解决方案不会设置ttl过期时间,而是新增一个exprie字段,从redis里面查询发现是过期数据时就需要加锁开启一个新线程去更新缓存,然后直接返回旧数据。有别的线程来获取锁失败时说明已经有线程在进行更新,所以就直接返回过期数据,避免了过多线程等待锁。

 

利用互斥锁解决缓存击穿问题(重点)

这里的锁不能用lock和synchronized进行互斥实现,这两个会一直等待.这里用到Redis的一个命令setnx, 这个是一旦设置之后就不能修改,只能删除,但是如果因为意外原因导致迟迟不能删除会有大问题,所以这里会给锁设置一个有效期.

 代码修改

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result queryById(Long id) {//缓存穿透//Shop shop=queryWithPassThrouh(id);//互斥锁解决缓存击穿Shop shop = queryWithMutex(id);if(shop==null){return Result.fail("店铺不存在!");}//7.返回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.存在,直接返回return JSONUtil.toBean(shopJson, Shop.class);}//判断命中的是否是空值if(shopJson!=null){//返回一个错误信息return null;}//4.实现缓存重建//4.1获取互斥锁String lockkey="lock:shop:"+id;Shop shop = null;try {boolean isLock = tryLock(lockkey);//4.2判断是否获取成功if(!isLock){//4.3失败,休眠并重试Thread.sleep(50);return  queryWithMutex(id);   //这里有可能会出现栈溢出的情况。}//获取成功之后应该再次检查缓存是否存在,有可能别的线程已经重建完了缓存,所以这里就无需再重建缓存shopJson = stringRedisTemplate.opsForValue().get(key);//再次判断是否存在if (StrUtil.isNotBlank(shopJson)) {//存在,直接返回return JSONUtil.toBean(shopJson, Shop.class);}//4.4根据id查询数据库shop = getById(id);//模拟重建的延时Thread.sleep(200);if (shop==null) {//将空值写入RedisstringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);//5.不存在,返回错误return shop;}//6.存在,写入RedisstringRedisTemplate.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;}public Shop queryWithPassThrouh(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.不存在,根据id查询数据库Shop shop = getById(id);if (shop==null) {//将空值写入RedisstringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);//5.不存在,返回错误return shop;}//6.存在,写入RedisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);//7.返回return shop;}private boolean tryLock(String key){Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);}private void unlock(String key){stringRedisTemplate.delete(key);}@Overridepublic Result update(Shop shop) {Long id = shop.getId();if(id==null){return Result.fail("店铺id不能为空");}//1.更新数据库updateById(shop);//2.删除缓存stringRedisTemplate.delete(CACHE_SHOP_KEY+id);return null;}
}

这里可以上Jmeter进行压测,上100个线程进行测试

但是最终实际只查询了一次数据库. 

利用逻辑过期解决缓存击穿问题(重点)

 为了能增加一个逻辑过期时间的字段,新建一个对象

@Data
public class RedisData {private LocalDateTime expireTime;private Object data;
}

代码修改

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result queryById(Long id) {//缓存穿透//Shop shop=queryWithPassThrouh(id);//互斥锁解决缓存击穿
//        Shop shop = queryWithMutex(id);//逻辑过期解决缓存击穿问题Shop shop = queryWithLogicalExpire(id);if(shop==null){return Result.fail("店铺不存在!");}//7.返回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.isBlank(shopJson)) {//3.存在,直接返回nullreturn 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){//这里应该再次检测缓存是否过期,做双重判断,如果没过期就不需重建了,因为可能别的线程已经重建了shopJson = stringRedisTemplate.opsForValue().get(key);redisData = JSONUtil.toBean(shopJson, RedisData.class);expireTime = redisData.getExpireTime();if(expireTime.isAfter(LocalDateTime.now())){//返回前先释放锁unlock(lockKey);//5.1未过期,直接返回店铺信息return shop;}//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 Shop queryWithMutex(Long id){String key=CACHE_SHOP_KEY+ id;//1.从Redis查询缓存String shopJson = stringRedisTemplate.opsForValue().get(key);//2.判断是否存在if (StrUtil.isNotBlank(shopJson)) {//3.存在,直接返回return JSONUtil.toBean(shopJson, Shop.class);}//判断命中的是否是空值if(shopJson!=null){//返回一个错误信息return null;}//4.实现缓存重建//4.1获取互斥锁String lockkey="lock:shop:"+id;Shop shop = null;try {boolean isLock = tryLock(lockkey);//4.2判断是否获取成功if(!isLock){//4.3失败,休眠并重试Thread.sleep(50);return  queryWithMutex(id);   //这里有可能会出现栈溢出的情况。}//获取成功之后应该再次检查缓存是否存在,有可能别的线程已经重建完了缓存,所以这里就无需再重建缓存shopJson = stringRedisTemplate.opsForValue().get(key);//再次判断是否存在if (StrUtil.isNotBlank(shopJson)) {//存在,直接返回return JSONUtil.toBean(shopJson, Shop.class);}//4.4根据id查询数据库shop = getById(id);//模拟重建的延时//Thread.sleep(200);if (shop==null) {//将空值写入RedisstringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);//5.不存在,返回错误return shop;}//6.存在,写入RedisstringRedisTemplate.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;}public Shop queryWithPassThrouh(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.不存在,根据id查询数据库Shop shop = getById(id);if (shop==null) {//将空值写入RedisstringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);//5.不存在,返回错误return shop;}//6.存在,写入RedisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);//7.返回return shop;}private boolean tryLock(String key){Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);}private void unlock(String key){stringRedisTemplate.delete(key);}public void saveShop2Redis(Long id,Long expireSeconds) throws InterruptedException {//1.查询店铺数据Shop shop = getById(id);//模拟延时
//        Thread.sleep(200);//2.封装逻辑过期时间RedisData redisData = new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));//3.写入RedisstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));}@Overridepublic Result update(Shop shop) {Long id = shop.getId();if(id==null){return Result.fail("店铺id不能为空");}//1.更新数据库updateById(shop);//2.删除缓存stringRedisTemplate.delete(CACHE_SHOP_KEY+id);return null;}
}

缓存工具封装(重点)

封装工具类里用到的实体

@Data
public class RedisData {private LocalDateTime expireTime;private Object data;
}

工具类代码

@Slf4j
@Component
public class CacheClient {@Resourceprivate StringRedisTemplate stringRedisTemplate;String LOCK_SHOP_KEY="lock:shop:";public CacheClient(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}public  void set(String key, Object value, Long time, TimeUnit timeUnit){stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,timeUnit);}public  void setWithLogicalExpire(String key, Object value, Long time, TimeUnit timeUnit){//设置逻辑过期RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time)));//写入RedisstringRedisTemplate.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 timeUnit){String key=keyPrefix+id;//1.从Redis查询缓存String json = stringRedisTemplate.opsForValue().get(key);//2.判断是否存在if (StrUtil.isNotBlank(json)) {//3.存在,直接返回return  JSONUtil.toBean(json, type);}//判断命中的是否是空值if(json!=null){//返回一个错误信息return null;}//4.不存在,根据id查询数据库R r=dbFallback.apply(id);//5.不存在,返回错误if (r==null) {//将空值写入RedisstringRedisTemplate.opsForValue().set(key,"",2L, TimeUnit.MINUTES);return null;}//6.存在,写入Redisthis.set(key,r,time,timeUnit);//7.返回return r;}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 timeUnit){String key=keyPrefix+ id;//1.从Redis查询缓存String json = stringRedisTemplate.opsForValue().get(key);//2.判断是否存在if (StrUtil.isBlank(json)) {//3.存在,直接返回nullreturn 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=LOCK_SHOP_KEY+id;boolean isLock = tryLock(lockKey);//6.2判断是否获取锁成功if(isLock){//这里应该再次检测缓存是否过期,做双重判断,如果没过期就不需重建了,因为可能别的线程已经重建了json = stringRedisTemplate.opsForValue().get(key);redisData = JSONUtil.toBean(json, RedisData.class);r = JSONUtil.toBean((JSONObject)redisData.getData(), type);expireTime = redisData.getExpireTime();if(expireTime.isAfter(LocalDateTime.now())){//返回前先释放锁unlock(lockKey);//5.1未过期,直接返回店铺信息return r;}//6.3成功,开启独立线程,实现缓存重建CACHE_REBUILD_EXECUTOR.submit(()->{try {//重建缓存//查询数据库R r1 = dbFallback.apply(id);//写入Redisthis.setWithLogicalExpire(key,r1,time,timeUnit);} 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.SECONDS);return BooleanUtil.isTrue(flag);}private void unlock(String key){stringRedisTemplate.delete(key);}
}

Service层修改后代码

里面有缓存穿透的调用,也有缓存击穿的调用.

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate CacheClient cacheClient;@Overridepublic Result queryById(Long id) {//缓存穿透Shop shop=cacheClient.queryWithPassThrough(CACHE_SHOP_KEY,id, Shop.class,id2->getById(id2),CACHE_SHOP_TTL,TimeUnit.MINUTES);//Shop shop=cacheClient.queryWithPassThrough(CACHE_SHOP_KEY,id, Shop.class,this::getById,,CACHE_SHOP_TTL,TimeUnit.MINUTES);//互斥锁解决缓存击穿//Shop shop = queryWithMutex(id);//逻辑过期解决缓存击穿问题
//        Shop shop = cacheClient
//                .queryWithLogicalExpire(CACHE_SHOP_KEY,id,Shop.class,this::getById,CACHE_SHOP_TTL,TimeUnit.SECONDS);if(shop==null){return Result.fail("店铺不存在!");}//7.返回return Result.ok(shop);}@Overridepublic Result update(Shop shop) {Long id = shop.getId();if(id==null){return Result.fail("店铺id不能为空");}//1.更新数据库updateById(shop);//2.删除缓存stringRedisTemplate.delete(CACHE_SHOP_KEY+id);return null;}
}

内容总结:

去看文档资料里面xmind文档,那个里面总结的很好。

这篇关于Redis——某马点评day02——商铺缓存的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/466382

相关文章

Redis的Zset类型及相关命令详细讲解

《Redis的Zset类型及相关命令详细讲解》:本文主要介绍Redis的Zset类型及相关命令的相关资料,有序集合Zset是一种Redis数据结构,它类似于集合Set,但每个元素都有一个关联的分数... 目录Zset简介ZADDZCARDZCOUNTZRANGEZREVRANGEZRANGEBYSCOREZ

Redis多种内存淘汰策略及配置技巧分享

《Redis多种内存淘汰策略及配置技巧分享》本文介绍了Redis内存满时的淘汰机制,包括内存淘汰机制的概念,Redis提供的8种淘汰策略(如noeviction、volatile-lru等)及其适用场... 目录前言一、什么是 Redis 的内存淘汰机制?二、Redis 内存淘汰策略1. pythonnoe

Redis主从/哨兵机制原理分析

《Redis主从/哨兵机制原理分析》本文介绍了Redis的主从复制和哨兵机制,主从复制实现了数据的热备份和负载均衡,而哨兵机制可以监控Redis集群,实现自动故障转移,哨兵机制通过监控、下线、选举和故... 目录一、主从复制1.1 什么是主从复制1.2 主从复制的作用1.3 主从复制原理1.3.1 全量复制

Redis延迟队列的实现示例

《Redis延迟队列的实现示例》Redis延迟队列是一种使用Redis实现的消息队列,本文主要介绍了Redis延迟队列的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习... 目录一、什么是 Redis 延迟队列二、实现原理三、Java 代码示例四、注意事项五、使用 Redi

Redis缓存问题与缓存更新机制详解

《Redis缓存问题与缓存更新机制详解》本文主要介绍了缓存问题及其解决方案,包括缓存穿透、缓存击穿、缓存雪崩等问题的成因以及相应的预防和解决方法,同时,还详细探讨了缓存更新机制,包括不同情况下的缓存更... 目录一、缓存问题1.1 缓存穿透1.1.1 问题来源1.1.2 解决方案1.2 缓存击穿1.2.1

redis-cli命令行工具的使用小结

《redis-cli命令行工具的使用小结》redis-cli是Redis的命令行客户端,支持多种参数用于连接、操作和管理Redis数据库,本文给大家介绍redis-cli命令行工具的使用小结,感兴趣的... 目录基本连接参数基本连接方式连接远程服务器带密码连接操作与格式参数-r参数重复执行命令-i参数指定命

深入理解Redis大key的危害及解决方案

《深入理解Redis大key的危害及解决方案》本文主要介绍了深入理解Redis大key的危害及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着... 目录一、背景二、什么是大key三、大key评价标准四、大key 产生的原因与场景五、大key影响与危

Redis主从复制的原理分析

《Redis主从复制的原理分析》Redis主从复制通过将数据镜像到多个从节点,实现高可用性和扩展性,主从复制包括初次全量同步和增量同步两个阶段,为优化复制性能,可以采用AOF持久化、调整复制超时时间、... 目录Redis主从复制的原理主从复制概述配置主从复制数据同步过程复制一致性与延迟故障转移机制监控与维

Redis过期键删除策略解读

《Redis过期键删除策略解读》Redis通过惰性删除策略和定期删除策略来管理过期键,惰性删除策略在键被访问时检查是否过期并删除,节省CPU开销但可能导致过期键滞留,定期删除策略定期扫描并删除过期键,... 目录1.Redis使用两种不同的策略来删除过期键,分别是惰性删除策略和定期删除策略1.1惰性删除策略

Linux(Centos7)安装Mysql/Redis/MinIO方式

《Linux(Centos7)安装Mysql/Redis/MinIO方式》文章总结:介绍了如何安装MySQL和Redis,以及如何配置它们为开机自启,还详细讲解了如何安装MinIO,包括配置Syste... 目录安装mysql安装Redis安装MinIO总结安装Mysql安装Redis搜索Red