《优化接口设计的思路》系列:第九篇—用好缓存,让你的接口速度飞起来

一、前言

大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。

作为一名从业已达六年的老码农,我的工作主要是开发后端Java业务系统,包括各种管理后台和小程序等。在这些项目中,我设计过单/多租户体系系统,对接过许多开放平台,也搞过消息中心这类较为复杂的应用,但幸运的是,我至今还没有遇到过线上系统由于代码崩溃导致资损的情况。这其中的原因有三点:一是业务系统本身并不复杂;二是我一直遵循某大厂代码规约,在开发过程中尽可能按规约编写代码;三是经过多年的开发经验积累,我成为了一名熟练工,掌握了一些实用的技巧。

前面的文章都是写接口如何设计、接口怎么验权以及一些接口常用的组件,这篇写点接口性能相关的。接口性能优化有很多途径,比如表建索引、SQL优化、加缓存、重构代码等等,本篇文章主要讲一下我是怎么在项目中使用缓存来提高接口响应速度的。我觉得缓存的使用主要有下面几个方面:

  • 缓存预热
    • 定时任务预热:定时任务在系统低峰期预加载数据到缓存中。
    • 启动预热:系统启动时预加载必要的数据到缓存中。
  • 缓存层次化
    • 多级缓存:实现本地缓存和分布式缓存相结合,例如,先在本地缓存中查询,如果没有再查询Redis等分布式缓存,最后才查询数据库。
    • 热点数据缓存:对频繁访问的数据进行缓存,如用户会话、热门商品信息、高频访问的内容等。

缓存提高接口响应速度主要是上面这些思路,不过我不是来讲概念的,那是面试要用的东西。我要讲的是如何用代码实现这些思路,把它们真正用到项目中来,水平有限,我尽力说,不喜勿喷。

由于文章经常被抄袭,开源的代码甚至被当成收费项,所以源码里面不是全部代码,有需要的同学可以留个邮箱,我给你单独发!

二、缓存预热:手撸一个缓存处理器

上面说了缓存预热主要是定时任务预热、启动预热,那么我们实现这个功能的时候,一般使用ConcurrentHashMapRedis来暂存数据,然后加上SpringBoot自带的@Scheduled定时刷新缓存就够了。虽然这样可以实现缓存预热,但缺陷很多,一旦需要预热的东西多起来就会变得越来越复杂,那么如何实现一个好的缓存处理器呢?接着看!

1、缓存处理器设计

(1)一个好的缓存处理器应该是这样搭建的

  1. DAL实现,产出DAO和DO对象,定义缓存领域模型
  2. 定义缓存名称,特别关注缓存的初始化顺序
  3. 编写数据仓库,通过模型转换器实现数据模型到缓存模型的转化
  4. 编写缓存管理器,推荐继承抽象管理器 {@link AbstractCacheManager}
  5. 根据业务需求,设计缓存数据接口(putAll,get,getCacheInfo等基础API)
  6. 完成bean配置,最好是可插拔的注册方式,缓存管理器和数据仓库、扩展点服务

(2)思路分析

2、代码实现

a. 每个处理器都有缓存名字、描述信息、缓存初始化顺序等信息,所以应该定义一个接口,名字为CacheNameDomain;

CacheNameDomain.java

package com.summo.demo.cache;

public interface CacheNameDomain {

    /**
     * 缓存初始化顺序,级别越低,越早被初始化
     * <p>
     * 如果缓存的加载存在一定的依赖关系,通过缓存级别控制初始化或者刷新时缓存数据的加载顺序<br>
     * 级别越低,越早被初始化<br>
     * <p>
     * 如果缓存的加载没有依赖关系,可以使用默认顺序<code>Ordered.LOWEST_PRECEDENCE</code>
     *
     * @return 初始化顺序
     * @see org.springframework.core.Ordered
     */
    int getOrder();

    /**
     * 缓存名称,推荐使用英文大写字母表示
     *
     * @return 缓存名称
     */
    String getName();

    /**
     * 缓存描述信息,用于打印日志
     *
     * @return 缓存描述信息
     */
    String getDescription();
}
b. 可以使用一个枚举类将不同的缓存处理器分开,有利于管理,取名为CacheNameEnum;

CacheNameEnum.java

package com.summo.demo.cache;

import org.springframework.core.Ordered;

/**
 * @description 缓存枚举
 */
public enum CacheNameEnum implements CacheNameDomain {
    /**
     * 系统配置缓存
     */
    SYS_CONFIG("SYS_CONFIG", "系统配置缓存", Ordered.LOWEST_PRECEDENCE),
    ;

    private String name;

    private String description;

    private int order;

    CacheNameEnum(String name, String description, int order) {
        this.name = name;
        this.description = description;
        this.order = order;
    }

    @Override
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public int getOrder() {
        return order;
    }

    public void setOrder(int order) {
        this.order = order;
    }
}
c. 缓存信息转换工具,以便dump出更友好的缓存信息,取名为CacheMessageUtil;

CacheMessageUtil.java

package com.summo.demo.cache;


import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * @description 缓存信息转换工具,以便dump出更友好的缓存信息
 */
public final class CacheMessageUtil {

    /** 换行符 */
    private static final char ENTERSTR  = '\n';

    /** Map 等于符号 */
    private static final char MAP_EQUAL = '=';

    /**
     * 禁用构造函数
     */
    private CacheMessageUtil() {
        // 禁用构造函数
    }

    /**
     * 缓存信息转换工具,以便dump出更友好的缓存信息<br>
     * 对于List<?>的类型转换
     *
     * @param cacheDatas 缓存数据列表
     * @return 缓存信息
     */
    public static String toString(List<?> cacheDatas) {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < cacheDatas.size(); i++) {
            Object object = cacheDatas.get(i);
            builder.append(object);

            if (i != cacheDatas.size() - 1) {
                builder.append(ENTERSTR);
            }
        }

        return builder.toString();
    }

    /**
     * 缓存信息转换工具,以便dump出更友好的缓存信息<br>
     * 对于Map<String, Object>的类型转换
     *
     * @param map 缓存数据
     * @return 缓存信息
     */
    public static String toString(Map<?, ?> map) {
        StringBuilder builder = new StringBuilder();
        int count = map.size();
        for (Iterator<?> i = map.keySet().iterator(); i.hasNext();) {
            Object name = i.next();
            count++;

            builder.append(name).append(MAP_EQUAL);
            builder.append(map.get(name));

            if (count != count - 1) {
                builder.append(ENTERSTR);
            }
        }

        return builder.toString();
    }

}
d. 每个处理器都有生命周期,如初始化、刷新、获取处理器信息等操作,这应该也是一个接口,处理器都应该声明这个接口,名字为CacheManager;

CacheManager.java

package com.summo.demo.cache;

import org.springframework.core.Ordered;

public interface CacheManager extends Ordered {

    /**
     * 初始化缓存
     */
    public void initCache();

    /**
     * 刷新缓存
     */
    public void refreshCache();

    /**
     * 获取缓存的名称
     *
     * @return 缓存名称
     */
    public CacheNameDomain getCacheName();

    /**
     * 打印缓存信息
     */
    public void dumpCache();

    /**
     * 获取缓存条数
     *
     * @return
     */
    public long getCacheSize();
}
e. 定义一个缓存处理器生命周期的处理器,会声明CacheManager,做第一次的处理,也是所有处理器的父类,所以这应该是一个抽象类,名字为AbstractCacheManager;

AbstractCacheManager.java

package com.summo.demo.cache;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;

/**
 * @description 缓存管理抽象类,缓存管理器都要集成这个抽象类
 */
public abstract class AbstractCacheManager implements CacheManager, InitializingBean {

    /**
     * LOGGER
     */
    protected static final Logger LOGGER = LoggerFactory.getLogger(AbstractCacheManager.class);

    /**
     * 获取可读性好的缓存信息,用于日志打印操作
     *
     * @return 缓存信息
     */
    protected abstract String getCacheInfo();

    /**
     * 查询数据仓库,并加载到缓存数据
     */
    protected abstract void loadingCache();

    /**
     * 查询缓存大小
     *
     * @return
     */
    protected abstract long getSize();

    /**
     * @see InitializingBean#afterPropertiesSet()
     */
    @Override
    public void afterPropertiesSet() {
        CacheManagerRegistry.register(this);
    }

    @Override
    public void initCache() {

        String description = getCacheName().getDescription();

        LOGGER.info("start init {}", description);

        loadingCache();

        afterInitCache();

        LOGGER.info("{} end init", description);
    }

    @Override
    public void refreshCache() {

        String description = getCacheName().getDescription();

        LOGGER.info("start refresh {}", description);

        loadingCache();

        afterRefreshCache();

        LOGGER.info("{} end refresh", description);
    }

    /**
     * @see org.springframework.core.Ordered#getOrder()
     */
    @Override
    public int getOrder() {
        return getCacheName().getOrder();
    }

    @Override
    public void dumpCache() {

        String description = getCacheName().getDescription();

        LOGGER.info("start print {} {}{}", description, "\n", getCacheInfo());

        LOGGER.info("{} end print", description);
    }

    /**
     * 获取缓存条目
     *
     * @return
     */
    @Override
    public long getCacheSize() {
        LOGGER.info("Cache Size Count: {}", getSize());
        return getSize();
    }

    /**
     * 刷新之后,其他业务处理,比如监听器的注册
     */
    protected void afterInitCache() {
        //有需要后续动作的缓存实现
    }

    /**
     * 刷新之后,其他业务处理,比如缓存变通通知
     */
    protected void afterRefreshCache() {
        //有需要后续动作的缓存实现
    }
}
f. 当有很多缓存处理器的时候,那么需要一个统一注册、统一管理的的地方,可以实现对分散在各处的缓存管理器统一维护,名字为CacheManagerRegistry;

CacheManagerRegistry.java

package com.summo.demo.cache;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.OrderComparator;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @description 缓存管理器集中注册接口,可以实现对分散在各处的缓存管理器统一维护
 */
@Component
public final class CacheManagerRegistry implements InitializingBean {

    /**
     * LOGGER
     */
    private static final Logger logger = LoggerFactory.getLogger(CacheManagerRegistry.class);

    /**
     * 缓存管理器
     */
    private static Map<String, CacheManager> managerMap = new ConcurrentHashMap<String, CacheManager>();

    /**
     * 注册缓存管理器
     *
     * @param cacheManager 缓存管理器
     */
    public static void register(CacheManager cacheManager) {
        String cacheName = resolveCacheName(cacheManager.getCacheName().getName());
        managerMap.put(cacheName, cacheManager);
    }

    /**
     * 刷新特定的缓存
     *
     * @param cacheName 缓存名称
     */
    public static void refreshCache(String cacheName) {
        CacheManager cacheManager = managerMap.get(resolveCacheName(cacheName));
        if (cacheManager == null) {
            logger.warn("cache manager is not exist,cacheName=", cacheName);
            return;
        }

        cacheManager.refreshCache();
        cacheManager.dumpCache();
    }

    /**
     * 获取缓存总条数
     */
    public static long getCacheSize(String cacheName) {
        CacheManager cacheManager = managerMap.get(resolveCacheName(cacheName));
        if (cacheManager == null) {
            logger.warn("cache manager is not exist,cacheName=", cacheName);
            return 0;
        }
        return cacheManager.getCacheSize();
    }

    /**
     * 获取缓存列表
     *
     * @return 缓存列表
     */
    public static List<String> getCacheNameList() {
        List<String> cacheNameList = new ArrayList<>();
        managerMap.forEach((k, v) -> {
            cacheNameList.add(k);
        });
        return cacheNameList;
    }

    public void startup() {
        try {

            deployCompletion();

        } catch (Exception e) {

            logger.error("Cache Component Init Fail:", e);

            // 系统启动时出现异常,不希望启动应用
            throw new RuntimeException("启动加载失败", e);
        }
    }

    /**
     * 部署完成,执行缓存初始化
     */
    private void deployCompletion() {

        List<CacheManager> managers = new ArrayList<CacheManager>(managerMap.values());

        // 根据缓存级别进行排序,以此顺序进行缓存的初始化
        Collections.sort(managers, new OrderComparator());

        // 打印系统启动日志
        logger.info("cache manager component extensions:");
        for (CacheManager cacheManager : managers) {
            String beanName = cacheManager.getClass().getSimpleName();
            logger.info(cacheManager.getCacheName().getName(), "==>", beanName);
        }

        // 初始化缓存
        for (CacheManager cacheManager : managers) {
            cacheManager.initCache();
            cacheManager.dumpCache();
        }
    }

    /**
     * 解析缓存名称,大小写不敏感,增强刷新的容错能力
     *
     * @param cacheName 缓存名称
     * @return 转换大写的缓存名称
     */
    private static String resolveCacheName(String cacheName) {
        return cacheName.toUpperCase();
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        startup();
    }
}

3、使用方式

项目结构如下:

这是完整的项目结构图,具体的使用步骤如下:
step1、在CacheNameEnum中加一个业务枚举,如 SYS_CONFIG("SYS_CONFIG", "系统配置缓存", Ordered.LOWEST_PRECEDENCE)
step2、自定义一个CacheManager继承AbstractCacheManager,如public class SysConfigCacheManager extends AbstractCacheManager
step3、实现loadingCache()方法,这里将你需要缓存的数据查询出来,但注意不要将所有的数据都放在一个缓存处理器中,前面CacheNameEnum枚举类的作用就是希望按业务分开处理;
step4、在自定义的CacheManager类中写自己的查询数据方法,因为不同业务的场景不同,查询参数、数据大小、格式、类型都不一致,所以AbstractCacheManager并没有定义统一的取数方法,没有意义;

下面是一个完整的例子
SysConfigCacheManager.java

package com.summo.demo.cache.manager;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import com.summo.demo.cache.AbstractCacheManager;
import com.summo.demo.cache.CacheMessageUtil;
import com.summo.demo.cache.CacheNameDomain;
import com.summo.demo.cache.CacheNameEnum;
import org.springframework.stereotype.Component;

/**
 * 系统配置管理器
 */
@Component
public class SysConfigCacheManager extends AbstractCacheManager {

    /**
     * 加个锁,防止出现并发问题
     */
    private static final Lock LOCK = new ReentrantLock();

    /**
     * 底层缓存组件,可以使用ConcurrentMap也可以使用Redis,推荐使用Redis
     */
    private static ConcurrentMap<String, Object> CACHE;

    @Override
    protected String getCacheInfo() {
        return CacheMessageUtil.toString(CACHE);
    }

    @Override
    protected void loadingCache() {
        LOCK.lock();
        try {
            //存储数据,这里就模拟一下了
            CACHE = new ConcurrentHashMap<>();
            CACHE.put("key1", "value1");
            CACHE.put("key2", "value2");
            CACHE.put("key3", "value3");
        } finally {
            LOCK.unlock();
        }

    }

    @Override
    protected long getSize() {
        return null == CACHE ? 0 : CACHE.size();
    }

    @Override
    public CacheNameDomain getCacheName() {
        return CacheNameEnum.SYS_CONFIG;
    }

    /**
     * 自定义取数方法
     *
     * @param key
     * @return
     */
    public static Object getConfigByKey(String key) {
        return CACHE.get(key);
    }
}

三、缓存层次化:使用函数式编程实现

1、先举个例子

现有一个使用商品名称查询商品的需求,要求先查询缓存,查不到则去数据库查询;从数据库查询到之后加入缓存,再查询时继续先查询缓存。

(1)思路分析

可以写一个条件判断,伪代码如下:

//先从缓存中查询
String goodsInfoStr = redis.get(goodsName);
if(StringUtils.isBlank(goodsInfoStr)){
	//如果缓存中查询为空,则去数据库中查询
	Goods goods = goodsMapper.queryByName(goodsName);
	//将查询到的数据存入缓存
	goodsName.set(goodsName,JSONObject.toJSONString(goods));
	//返回商品数据
	return goods;
}else{
	//将查询到的str转换为对象并返回
	return JSON.parseObject(goodsInfoStr, Goods.class);
}

流程图如下

上面这串代码也可以实现查询效果,看起来也不是很复杂,但是这串代码是不可复用的,只能用在这个场景。假设在我们的系统中还有很多类似上面商品查询的需求,那么我们需要到处写这样的if(...)else{...}。作为一个程序员,不能把类似的或者重复的代码统一起来是一件很难受的事情,所以需要对这种场景的代码进行优化。

上面这串代码的问题在于:入参不固定、返回值也不固定,如果仅仅是参数不固定,使用泛型即可。但最关键的是查询方法也是不固定的,比如查询商品和查询用户肯定不是一个查询方法吧。

所以如果我们可以把一个方法(即上面的各种查询方法)也能当做一个参数传入一个统一的判断方法就好了,类似于:

/**
 * 这个方法的作用是:先执行method1方法,如果method1查询或执行不成功,再执行method2方法
 */
public static<T> T selectCacheByTemplate(method1,method2)

想要实现上面的这种效果,就不得不提到Java8的新特性:函数式编程

2、什么是函数式编程

在Java中有一个package:java.util.function ,里面全部是接口,并且都被@FunctionalInterface注解所修饰。

Function分类

  • Consumer(消费):接受参数,无返回值
  • Function(函数):接受参数,有返回值
  • Operator(操作):接受参数,返回与参数同类型的值
  • Predicate(断言):接受参数,返回boolean类型
  • Supplier(供应):无参数,有返回值

具体我就不再赘述了,可以参考:https://blog.csdn.net/hua226/article/details/124409889

3、代码实现

核心代码非常简单,如下

/**
  * 缓存查询模板
  *
  * @param cacheSelector    查询缓存的方法
  * @param databaseSelector 数据库查询方法
  * @return T
  */
public static <T> T selectCacheByTemplate(Supplier<T> cacheSelector, Supplier<T> databaseSelector) {
  try {
    log.info("query data from redis ······");
    // 先查 Redis缓存
    T t = cacheSelector.get();
    if (t == null) {
      // 没有记录再查询数据库
      return databaseSelector.get();
    } else {
      return t;
    }
  } catch (Exception e) {
    // 缓存查询出错,则去数据库查询
    log.info("query data from database ······");
    return databaseSelector.get();
    }
}

这里的Supplier 就是一个加了@FunctionalInterface注解的接口。

4、使用方式

使用方式也非常简单,如下

@Component
public class UserManager {

    @Autowired
    private CacheService cacheService;

    public Set<String> queryAuthByUserId(Long userId) {
        return BaseUtil.selectCacheByTemplate(
            //从缓存中查询
            () -> this.cacheService.queryUserFromRedis(userId),
            //从数据库中查询
            () -> this.cacheService.queryUserFromDB(userId));
    }
}

这样就可以做到先查询Redis,查询不到再查询数据库,非常简单也非常好用,我常用于查询一些实体信息的场景。不过这里有一个注意的点:缓存一致性。因为有时候底层数据会变化,需要做好一致性,否则会出问题。

四、小结一下

首先,缓存确实可以提高API查询效率,这点大家应该不会质疑,但缓存并不是万能的,不应该将所有数据都缓存起来,应当评估数据的访问频率和更新频率,以决定是否缓存。
其次,在实施缓存策略时,需要平衡缓存的开销、复杂性和所带来的性能提升。此外,缓存策略应该根据实际业务需求和数据特征进行定制,不断调整优化以适应业务发展。
最后,缓存虽好,但不要乱用哦,否则会出现令你惊喜的BUG!😇

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

学习笔记Day14:Linux下软件安装

软件安装 Anaconda 所有语言的包(package)、依赖(dependency)和环境(environment)管理器&#xff0c;类似应用商店 Conda < Miniconda < Anaconda&#xff08;有交互界面&#xff09; Linux下Miniconda即可 安装Miniconda 搜索北外/清华miniconda镜像网站&#xff…

使用专属浏览器在国内直连GPT教程

Wildcard官方推特发文说他们最近推出了一款专门为访问OpenAI设计的浏览器。 根据官方消息&#xff0c;这是一款专门为访问OpenAI优选网络设计的浏览器&#xff0c;它通过为用户提供专用的家庭网络出口&#xff0c;确保了快速、稳定的连接。 用这个浏览器的最大好处就是直接用浏…

【前端寻宝之路】学习和总结HTML的标签属性

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法|MySQL| ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” #mermaid-svg-tgsZb9zTBxJHHYhD {font-family:"trebuchet ms",verdana,arial,sans-serif;f…

蓝桥杯 2022 省B 李白打酒加强版

这题用递归暴力的方法如下&#xff1a; #include<iostream> #include<bits/stdc.h> using namespace std; int num; int N,M; void dfs(int now,int n,int m) {if(now<0 || n>N ||m>M)return ;if(nN && mM){if(now1)num1;return;}dfs(now-1,n,m1…

(一)、Doris安装使用(基于Doris 2.0.6)

第 1 章Doris简介 1.1、 Doris 概述 ​ Apache Doris由百度大数据部研发&#xff08;之前叫百度 Palo&#xff0c;2018年贡献到 Apache 社区后&#xff0c;更名为 Doris&#xff09;&#xff0c;在百度内部&#xff0c;有超过200个产品线在使用&#xff0c;部署机器超过1000台…

【力扣白嫖日记】613.直线上的最近距离

前言 练习sql语句&#xff0c;所有题目来自于力扣&#xff08;https://leetcode.cn/problemset/database/&#xff09;的免费数据库练习题。 今日题目&#xff1a; 613.直线上的最近距离 表&#xff1a;Point 列名类型xint 在SQL中&#xff0c;x是该表的主键列。该表的每一…

Redis 不再“开源”,对中国的影响及应对方案

Redis 不再“开源”&#xff0c;使用双许可证 3 月 20 号&#xff0c;Redis 的 CEO Rowan Trollope 在官网上宣布了《Redis 采用双源许可证》的消息。他表示&#xff0c;今后 Redis 的所有新版本都将使用开源代码可用的许可证&#xff0c;不再使用 BSD 协议&#xff0c;而是采用…

linux sh脚本编写

linux中bash Shell 是 Linux 的核心部分&#xff0c;它允许你使用各种诸如 cd、ls、cat 等的命令与 Linux 内核进行交互。Bash脚本和Shell脚本实际上是指同一种类型的脚本&#xff0c;只不过Bash是其中最常用的一种Shell。除了Bash之外&#xff0c;常见的Shell解释器还有C She…

【Django框架学习笔记】超详细的Python后端开发Django框架学习笔记

十二&#xff0c;Django框架 可以以下链接获取Django框架学习笔记,md文档和pdf文档 Django框架超详细的学习笔记&#xff0c;点击我获取 12.1 命令行操作 # 创建django项目 django-admin startproject aini# 启动项目 cd /mysite python3 manage.py runserver## 创建应用 …

BUUCTF---WEEK3(Rabin‘s RSA)

题目&#xff1a; from Crypto.Util.number import * from secret import flag p getPrime(64) q getPrime(64) assert p % 4 3 assert q % 4 3n p * qe 2 m bytes_to_long(flag)c pow(m,e,n)print(n , n) print(c , c)# n 201354090531918389422241515534761536573 …

MySQL面试题--事务

目录 1.什么是数据库事务&#xff1f;事务的特性是什么&#xff1f; 2.什么是ACID&#xff1f; 3.并发事务会有哪些问题&#xff1f; 4.什么是 脏读、丢失修改、不可重复读、幻读 5.不可重复读和幻读有什么区别&#xff1f; 6.Mysql是如何避免事务并发问题的&#xff1f; …

加一——大数据的应用

题目链接&#xff1a;66. 加一 - 力扣&#xff08;LeetCode&#xff09; 解题思路&#xff1a; 先将输入的数组转换成整数&#xff0c;对整数进行加一操作&#xff0c;然后再转换回数组&#xff0c;这样就不用考虑加一进位和数位增加的问题&#xff0c;很简单的思路但是运行时间…

操作简单的城市内涝一维二维耦合模拟软件

原文链接&#xff1a;最简单的城市内涝一维二维耦合模拟软件https://mp.weixin.qq.com/s?__bizMzUzNTczMDMxMg&mid2247598401&idx3&sn0c4c86b3a5d09a75b8f07e6fad81aa9c&chksmfa8200a6cdf589b0970a6854869e8e3a9f132fe40a19977863c091cbcf6d9786f067e0c5651e&…

UE5 GameMode C++函数 学习

已经尝试&#xff0c;确实能重启游戏 类描述符加了noplaceable过后即使是Actor也不能放到场景中了&#xff0c;关卡蓝图&#xff0c;GameMode&#xff0c;GameState这些就不能放场景中了 UFUNCTION(exec)

【Python + Django】表结构创建

以员工管理系统为例。 事前呢&#xff0c;我们先把项目和app创建出来&#xff0c;详细步骤可以看我同栏目的第一篇、第二篇文章。 我知道你们是不会下来找的&#xff0c;就把链接贴在下面吧&#xff1a; 【Python Django】启动简单的文本页面-CSDN博客 【Python Django】…

优维全面可观测产品能力分解⑥:运维状态可观测

本文是《优维全面可观测产品能力分解》系列文章的第六篇&#xff1a;『运维状态可观测』。基于可观测的数据体系&#xff0c;「运维状态可观测」是实现于运维状态的一次深入可观测。 在日常运维场景中&#xff0c;系统/应用运维人员重点关注的是系统/应用是否可用&#xff0c;…

大数据开发扩展shell--尚硅谷shell笔记

大数据开发扩展shell 学习目标 1 熟悉shell脚本的原理和使用 2 熟悉shell的编程语法 第一节 Shell概述 1&#xff09;Linux提供的Shell解析器有&#xff1a; [atguiguhadoop101 ~]$ cat /etc/shells /bin/sh/bin/bash/sbin/nologin/bin/dash/bin/tcsh/bin/csh2&#xff09…

男性三十三岁,头晕头疼,心慌和后背发紧,竟被它治好了!

植物神经紊乱是一种影响现代人健康的常见问题&#xff0c;它源于植物神经系统功能失调&#xff0c;导致身心健康出现一系列不适症状。植物神经紊乱对身体健康的影响是多方面的&#xff0c;它可能导致睡眠问题、情绪波动和自律神经功能紊乱等多种不适症状&#xff0c;严重影响个…

深度学习,CRNN+CTC和Attention OCR你更青睐哪一种?

深度学习在OCR领域的应用已经取得了瞩目的成果&#xff0c;而选择合适的算法对于提升OCR的识别准确率至关重要。在众多算法中&#xff0c;CRNN和Attention OCR犹如两颗璀璨的明珠&#xff0c;备受瞩目。 CRNN&#xff0c;这位结合了卷积神经网络&#xff08;CNN&#xff09;和…

如何在 iPad 上恢复已删除的历史记录?

iPad 配备了一个名为 Safari 的内置网络浏览器。这是一种在旅途中保持联系和浏览网页的强大且便捷的方式。但如果您不小心删除了浏览历史记录&#xff0c;则尝试恢复它可能会很令人沮丧。 幸运的是&#xff0c;您可以通过多种方法在 iPad 上恢复已删除的 Safari 历史记录。您应…