Redis高阶3-缓存双写一致性

ops/2025/1/26 8:29:14/

Redis缓存双写一致性

请添加图片描述

Redis双写一致性的理解

  1. 如果Redis中有数据

    需要和数据库中的值相同

  2. 如果redis中无数据

    数据库中的值要是最新值,且准备回写redis

  3. 按照操作分为两种

    • 只读缓存

    • 读写缓存

      同步直写策略

      ​ 写数据库后也同步写redis缓存缓存数据库中的数据⼀致;

      ​ 对于读写缓存来说,要想保证缓存数据库中的数据⼀致,就要采⽤同步直写策略

      异步缓写策略

      ​ 正常业务运行中,mysql数据变动了,但是可以在业务上容许出现一定时间后才作用于redis,比如仓库、物流系统

      ​ 异常情况出现了,不得不将失败的动作重新修补,有可能需要借助kafka或者RabbitMQ等消息中间件,实现重试重写

  4. 实现

    • 双检加锁策略

      多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它。

      其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存

      后面的线程进来发现已经有缓存了,就直接走缓存

      请添加图片描述

    • Code

      @Service
      @Slf4j
      public class UserService {public static final String CACHE_KEY_USER = "user:";@Resourceprivate UserMapper userMapper;@Resourceprivate RedisTemplate redisTemplate;/*** 业务逻辑没有写错,对于小厂中厂(QPS《=1000)可以使用,但是大厂不行* @param id* @return*/public User findUserById(Integer id){User user = null;String key = CACHE_KEY_USER+id;//1 先从redis里面查询,如果有直接返回结果,如果没有再去查询mysqluser = (User) redisTemplate.opsForValue().get(key);if(user == null){//2 redis里面无,继续查询mysqluser = userMapper.selectByPrimaryKey(id);if(user == null){//3.1 redis+mysql 都无数据//你具体细化,防止多次穿透,我们业务规定,记录下导致穿透的这个key回写redisreturn user;}else{//3.2 mysql有,需要将数据写回redis,保证下一次的缓存命中率redisTemplate.opsForValue().set(key,user);}}return user;}/*** 加强补充,避免突然key失效了,打爆mysql,做一下预防,尽量不出现击穿的情况。* @param id* @return*/public User findUserById2(Integer id){User user = null;String key = CACHE_KEY_USER+id;//1 先从redis里面查询,如果有直接返回结果,如果没有再去查询mysql,// 第1次查询redis,加锁前user = (User) redisTemplate.opsForValue().get(key);if(user == null) {//2 大厂用,对于高QPS的优化,进来就先加锁,保证一个请求操作,让外面的redis等待一下,避免击穿mysqlsynchronized (UserService.class){//第2次查询redis,加锁后user = (User) redisTemplate.opsForValue().get(key);//3 二次查redis还是null,可以去查mysql了(mysql默认有数据)if (user == null) {//4 查询mysql拿数据(mysql默认有数据)user = userMapper.selectByPrimaryKey(id);if (user == null) {return null;}else{//5 mysql里面有数据的,需要回写redis,完成数据一致性的同步工作redisTemplate.opsForValue().setIfAbsent(key,user,7L,TimeUnit.DAYS);}}}}return user;}}
      

数据库缓存一致性的几种更新策略

目的:达到最终一致性

​ 给缓存设置过期时间,定期清理缓存并回写,是保证最终一致性的解决方案。

​ 我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存,达到一致性,切记,要以mysql的数据库写入库为准。

  1. 可以停机的情况
  • 挂牌报错,凌晨升级,温馨提示,服务降级
  • 单线程,这样重量级的数据操作最好不要多线程
  1. 4种更新策略

    • 先更新数据库,再更新缓存

      存在问题1

      1 先更新mysql的某商品的库存,当前商品的库存是100,更新为99个。

      2 先更新mysql修改为99成功,然后更新redis

      3 此时假设异常出现,更新redis失败了,这导致mysql里面的库存是99而redis里面的还是100 。

      4 上述发生,会让数据库里面和缓存redis里面数据不一致,读到redis脏数据

      存在问题2

      【先更新数据库,再更新缓存】,A、B两个线程发起调用

      【正常逻辑】

      1 A update mysql 100

      2 A update redis 100

      3 B update mysql 80

      4 B update redis 80

      =============================

      【异常逻辑】多线程环境下,A、B两个线程有快有慢,有前有后有并行

      1 A update mysql 100

      3 B update mysql 80

      4 B update redis 80

      2 A update redis 100

      =============================

      最终结果,mysql和redis数据不一致,o(╥﹏╥)o,

      mysql80,redis100

    • 先更新缓存,再更新数据库(不推荐,一般以数据库数据为主)

      存在问题

      【先更新缓存,再更新数据库】,A、B两个线程发起调用

      【正常逻辑】

      1 A update redis 100

      2 A update mysql 100

      3 B update redis 80

      4 B update mysql 80

      ====================================

      【异常逻辑】多线程环境下,A、B两个线程有快有慢有并行

      A update redis 100

      B update redis 80

      B update mysql 80

      A update mysql 100

      ----mysql100,redis80

    • 先删除缓存,再更新数据库

      流程

      (1)请求A进行写操作,删除redis缓存后,工作正在进行中,更新mysql…A还么有彻底更新完mysql,还没commit

      (2)请求B开工查询,查询redis发现缓存不存在(被A从redis中删除了)

      (3)请求B继续,去数据库查询得到了mysql中的旧值(A还没有更新完)

      (4)请求B将旧值写回redis缓存

      (5)请求A将新值写入mysql数据库

      上述情况就会导致不一致的情形出现。

      时间线程A线程B出现的问题
      t1请求A进行写操作,删除缓存成功后,工作正在mysql进行中…
      t21 缓存中读取不到,立刻读mysql,由于A还没有对mysql更新完,读到的是旧值 2 还把从mysql读取的旧值,写回了redis1 A还没有更新完mysql,导致B读到了旧值 2 线程B遵守回写机制,把旧值写回redis,导致其它请求读取的还是旧值,A白干了。
      t3A更新完mysql数据库的值,overredis是被B写回的旧值,mysql是被A更新的新值。出现了,数据不一致问题。

      总结一下:

      先删除缓存,再更新数据库如果数据库更新失败或超时或返回不及时,导致B线程请求访问缓存时发现redis里面没数据,缓存缺失,B再去读取mysql时,从数据库中读取到旧值,还写回redis,导致A白干了,o(╥﹏╥)o

      解决方案

      延时双删

      请添加图片描述

      存在问题1:休眠时间多久

      线程A sleep的时间,就需要大于线程B读取数据再写入缓存的时间。

      这个时间怎么确定呢?

      第一种方法:

      在业务程序运行的时候,统计下线程读数据和写缓存的操作时间,自行评估自己的项目的读数据业务逻辑的耗时,以此为基础来进行估算。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上加百毫秒即可。

      这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

      第二种方法:

      新启动一个后台监控程序。

      存在问题2:同步淘汰策略,吞吐量降低怎么办?

      请添加图片描述

    • 先更新数据库,再删除缓存

      存在问题

      先更新数据库,再删除缓存

      时间线程A线程B出现的问题
      t1更新数据库中的值…
      t2缓存中立刻命中,此时B读取的是缓存旧值。A还没有来得及删除缓存的值,导致B缓存命中读到旧值。
      t3更新缓存的数据,over
      先更新数据库,再删除缓存假如缓存删除失败或者来不及,导致请求再次访问redis缓存命中,读取到的是缓存旧值。

      解决方案

      请添加图片描述

      1 可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用Kafka/RabbitMQ等)。

      2 当程序没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。

      3 如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复操作,此时,我们也可以保证数据库缓存的数据一致了,否则还需要再次进行重试

      4 如果重试超过的一定次数后还是没有成功,我们就需要向业务层发送报错信息了,通知运维人员。

Redis与Mysql数据双写一致性的代码实现

canal

​ anal [kə’næl],中文翻译为 水道/管道/沟渠/运河,主要用途是用于 MySQL 数据库增量日志数据的订阅、消费和解析,是阿里巴巴开发并开源的,采用Java语言开发;

历史背景是早期阿里巴巴因为杭州和美国双机房部署,存在跨机房数据同步的业务需求,实现方式主要是基于业务 trigger(触发器) 获取增量变更。从2010年开始,阿里巴巴逐步尝试采用解析数据库日志获取增量变更进行同步,由此衍生出了canal项目;

功能
  1. 数据库镜像
  2. 数据库实时备份
  3. 索引构建和实时维护(拆分异构索引、倒排索引等)
  4. 业务 cache 刷新
  5. 带业务逻辑的增量数据处理
下载

https://github.com/alibaba/canal/releases/tag/canal-1.1.6

工作原理

  1. 传统Mysql主从复制工作原理

    请添加图片描述

    MySQL的主从复制将经过如下步骤:

    1、当 master 主服务器上的数据发生改变时,则将其改变写入二进制事件日志文件中;

    2、salve 从服务器会在一定时间间隔内对 master 主服务器上的二进制日志进行探测,探测其是否发生过改变,

    如果探测到 master 主服务器的二进制事件日志发生了改变,则开始一个 I/O Thread 请求 master 二进制事件日志;

    3、同时 master 主服务器为每个 I/O Thread 启动一个dump Thread,用于向其发送二进制事件日志;

    4、slave 从服务器将接收到的二进制事件日志保存至自己本地的中继日志文件中;

    5、salve 从服务器将启动 SQL Thread 从中继日志中读取二进制日志,在本地重放,使得其数据和主服务器保持一致;

    6、最后 I/O Thread 和 SQL Thread 将进入睡眠状态,等待下一次被唤醒;

  2. canal工作原理

    请添加图片描述

redisCoding_366">mysql-canal-redis双写一致性Coding

https://github.com/alibaba/canal/wiki/ClientExample

  1. mysql配置

    • 查看mysql版本

      SELECT VERSION();
      
    • 当前的主机二进制日志

      show master status;
      
    • 查看SHOW VARIABLES LIKE ‘log_bin’;

      SHOW VARIABLES LIKE 'log_bin';
      
    • 开启 MySQL的binlog写入功能

      my.ini

      mysql

      log-bin=mysql-bin #开启binlog

      binlog-format=ROW #选择 ROW 模式

      server_id=1 #配置MySQL replaction需要定义,不要和canal的 slaveId重复

      ROW模式 除了记录sql语句之外,还会记录每个字段的变化情况,能够清楚的记录每行数据的变化历史,但会占用较多的空间。STATEMENT模式只记录了sql语句,但是没有记录上下文信息,在进行数据恢复的时候可能会导致数据的丢失情况;MIX模式比较灵活的记录,理论上说当遇到了表结构变更的时候,就会记录为statement模式。当遇到了数据更新或者删除情况下就会变为row模式;

    • 重启mysql

    • 再次查看SHOW VARIABLES LIKE ‘log_bin’;

    • 授权canal连接MySQL账号

      DROP USER IF EXISTS ‘canal’@‘%’;CREATE USER ‘canal’@‘%’ IDENTIFIED BY ‘canal’; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON . TO ‘canal’@‘%’; FLUSH PRIVILEGES; SELECT * FROM mysql.user;

  2. canal服务端

    • 下载 Linux版本:canal.deplover-1.1.6.tar.gz

    • 解压 整体放入/mycanal下

    • 配置

      修改/mycanal/conf/example路径下的instance.properties文件

      换成自己的mysql主机master的IP地址

      请添加图片描述

      换成自己的mysql新建的mysql的canal账户

      请添加图片描述

    • 启动(需要JDK)

      /mycanal/bin路径下执行 ./startup.sh

    • 查看—判断canal是否启动成功

      查看server日志
      请添加图片描述

      查看样例example的日志

      请添加图片描述

  3. canal客户端(JAVA)

    • SQL脚本

      1 随便选个数据库,以你自己为主,本例bigdata,按照下面建表

      CREATE TABLE `t_user` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`userName` varchar(100) NOT NULL,PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4
      
    • 创建MODEL

    • 改写POM

      <?xml version="1.0" encoding="UTF-8"?>
      <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.zhg</groupId><artifactId>canal_demo02</artifactId><version>1.0-SNAPSHOT</version><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.5.14</version><relativePath/></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><maven.compiler.source>1.8</maven.compiler.source><maven.compiler.target>1.8</maven.compiler.target><junit.version>4.12</junit.version><log4j.version>1.2.17</log4j.version><lombok.version>1.16.18</lombok.version><mysql.version>5.1.47</mysql.version><druid.version>1.1.16</druid.version><mapper.version>4.1.5</mapper.version><mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version></properties><dependencies><!--canal--><dependency><groupId>com.alibaba.otter</groupId><artifactId>canal.client</artifactId><version>1.1.0</version></dependency><!--SpringBoot通用依赖模块--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><!--swagger2--><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>2.9.2</version></dependency><!--SpringBoot与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><!--SpringBoot与AOP--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId></dependency><!--Mysql数据库驱动--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.47</version></dependency><!--SpringBoot集成druid连接池--><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.10</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>${druid.version}</version></dependency><!--mybatis和springboot整合--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>${mybatis.spring.boot.version}</version></dependency><!--通用基础配置junit/devtools/test/log4j/lombok/hutool--><!--hutool--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.2.3</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>${junit.version}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>log4j</groupId><artifactId>log4j</artifactId><version>${log4j.version}</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version><optional>true</optional></dependency><!--persistence--><dependency><groupId>javax.persistence</groupId><artifactId>persistence-api</artifactId><version>1.0.2</version></dependency><!--通用Mapper--><dependency><groupId>tk.mybatis</groupId><artifactId>mapper</artifactId><version>${mapper.version}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-autoconfigure</artifactId></dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>3.8.0</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
      
    • 写YML

      server:port: 5555
      # ==========================================alibaba.druid========================
      spring:datasource:druid:test-while-idle: falsepassword: zhg.168..username: rooturl: jdbc:mysql://127.0.0.1:3306/bigdata?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghaidb-type: com.alibaba.druid.pool.DruidDataSource
      
    • 主启动类

      @SpringBootApplication
      public class CanalDemoApp {public static void main(String[] args) {
      //        SpringApplication.run(CanalDemoApp.class, args);}
      }
      
    • 业务类

      RedisUtils

      public class RedisUtils
      {public static final String  REDIS_IP_ADDR = "192.168.111.185";public static final String  REDIS_pwd = "111111";public static JedisPool jedisPool;static {JedisPoolConfig jedisPoolConfig=new JedisPoolConfig();jedisPoolConfig.setMaxTotal(20);jedisPoolConfig.setMaxIdle(10);jedisPool=new JedisPool(jedisPoolConfig,REDIS_IP_ADDR,6379,10000,REDIS_pwd);}public static Jedis getJedis() throws Exception {if(null!=jedisPool){return jedisPool.getResource();}throw new Exception("Jedispool is not ok");}}
      

      RedisCanalClientExample

      import com.alibaba.fastjson.JSONObject;
      import com.alibaba.otter.canal.client.CanalConnector;
      import com.alibaba.otter.canal.client.CanalConnectors;
      import com.alibaba.otter.canal.protocol.CanalEntry.*;
      import com.alibaba.otter.canal.protocol.Message;
      import com.zhg.canal.util.RedisUtils;
      import redis.clients.jedis.Jedis;
      import java.net.InetSocketAddress;
      import java.util.List;
      import java.util.UUID;
      import java.util.concurrent.TimeUnit;public class RedisCanalClientExample
      {public static final Integer _60SECONDS = 60;public static final String  CANAL_IP_ADDR = "192.168.111.185";private static void redisInsert(List<Column> columns){JSONObject jsonObject = new JSONObject();for (Column column : columns){System.out.println(column.getName() + " : " + column.getValue() + "    update=" + column.getUpdated());jsonObject.put(column.getName(),column.getValue());}if(columns.size() > 0){try(Jedis jedis = RedisUtils.getJedis()){jedis.set(columns.get(0).getValue(),jsonObject.toJSONString());}catch (Exception e){e.printStackTrace();}}}private static void redisDelete(List<Column> columns){JSONObject jsonObject = new JSONObject();for (Column column : columns){jsonObject.put(column.getName(),column.getValue());}if(columns.size() > 0){try(Jedis jedis = RedisUtils.getJedis()){jedis.del(columns.get(0).getValue());}catch (Exception e){e.printStackTrace();}}}private static void redisUpdate(List<Column> columns){JSONObject jsonObject = new JSONObject();for (Column column : columns){System.out.println(column.getName() + " : " + column.getValue() + "    update=" + column.getUpdated());jsonObject.put(column.getName(),column.getValue());}if(columns.size() > 0){try(Jedis jedis = RedisUtils.getJedis()){jedis.set(columns.get(0).getValue(),jsonObject.toJSONString());System.out.println("---------update after: "+jedis.get(columns.get(0).getValue()));}catch (Exception e){e.printStackTrace();}}}public static void printEntry(List<Entry> entrys) {for (Entry entry : entrys) {if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {continue;}RowChange rowChage = null;try {//获取变更的row数据rowChage = RowChange.parseFrom(entry.getStoreValue());} catch (Exception e) {throw new RuntimeException("ERROR ## parser of eromanga-event has an error,data:" + entry.toString(),e);}//获取变动类型EventType eventType = rowChage.getEventType();System.out.println(String.format("================&gt; binlog[%s:%s] , name[%s,%s] , eventType : %s",entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),entry.getHeader().getSchemaName(), entry.getHeader().getTableName(), eventType));for (RowData rowData : rowChage.getRowDatasList()) {if (eventType == EventType.INSERT) {redisInsert(rowData.getAfterColumnsList());} else if (eventType == EventType.DELETE) {redisDelete(rowData.getBeforeColumnsList());} else {//EventType.UPDATEredisUpdate(rowData.getAfterColumnsList());}}}}public static void main(String[] args){System.out.println("---------O(∩_∩)O哈哈~ initCanal() main方法-----------");//=================================// 创建链接canal服务端CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(CANAL_IP_ADDR,11111), "example", "", "");int batchSize = 1000;//空闲空转计数器int emptyCount = 0;System.out.println("---------------------canal init OK,开始监听mysql变化------");try {connector.connect();//connector.subscribe(".*\\..*");connector.subscribe("bigdata.t_user");connector.rollback();int totalEmptyCount = 10 * _60SECONDS;while (emptyCount < totalEmptyCount) {System.out.println("我是canal,每秒一次正在监听:"+ UUID.randomUUID().toString());Message message = connector.getWithoutAck(batchSize); // 获取指定数量的数据long batchId = message.getId();int size = message.getEntries().size();if (batchId == -1 || size == 0) {emptyCount++;try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }} else {//计数器重新置零emptyCount = 0;printEntry(message.getEntries());}connector.ack(batchId); // 提交确认// connector.rollback(batchId); // 处理失败, 回滚数据}System.out.println("已经监听了"+totalEmptyCount+"秒,无任何消息,请重启重试......");} finally {connector.disconnect();}}
      }
    • 测试

      启动

      请添加图片描述

      更新数据库

      请添加图片描述

      查看Redis是否变化

      
      

http://www.ppmy.cn/ops/152847.html

相关文章

Spring Boot 3.4 正式发布,结构化日志!

1 从 Spring Boot 3.3 升级到 3.4 1.1 RestClient 和 RestTemplate 新增对 RestClient 和 RestTemplate 自动配置的支持&#xff0c;可用 Reactor Netty 的 HttpClient 或 JDK 的 HttpClient。支持的客户端优先级&#xff1a; Apache HTTP Components (HttpComponentsClient…

Elasticsearch 和arkime 安装

安装一定要注意版本号&#xff0c;不然使用不了 这里Ubuntu使用ubuntu-20.04.6-desktop-amd64.iso elasticsearch这里使用Elasticsearch 7.17.5 | Elastic arkime这里使用wget https://s3.amazonaws.com/files.molo.ch/builds/ubuntu-20.04/arkime_3.4.2-1_amd64.deb 大家想…

解决 IntelliJ IDEA 项目包后出现“% classes”和“% lines covered”的问题

前言 在使用 IntelliJ IDEA 开发 Java 或其他支持的语言时&#xff0c;您可能会遇到项目包后面意外地出现了“% classes”和“% lines covered”的信息。这些百分比表示的是代码覆盖率&#xff08;Coverage&#xff09;&#xff0c;它们展示了您的测试覆盖了多少比例的类和代码…

从根源分析,调试,定位和解决MacOS ld: unsupported tapi file type ‘!tapi-tbd‘ in YAML file

你要是遇到同样错误&#xff0c;找一圈都没有解决&#xff0c;建议认真读一下本文&#xff0c;这个应该是最终极的解决办法&#xff0c;从原理上剖析了产生的原因&#xff0c;同时给出来了调试和定位的办法。 maccos使用brew安装了一个gcc14, 结果编译一个最简单的程序都报错&a…

低代码系统-钉、微表单控件对比

组件对比 微搭 宜搭 数据列表 单行文本 数据详情 多行文本 表单容器 数值 数据表格 评分 普通容器 单选 网格布局 复选 卡片 日期 轮播容器 日期区间 布局组件 成员 滚动容器 图片上传 弹窗 附件 TAB栏 子表单 菜单导航 下拉单选 宫格导航 下拉复…

低代码系统-氚云、宜搭控件对比

上一篇我们对比了宜搭和微搭&#xff0c;可以发现&#xff0c;两个产品的定位完全不一样&#xff0c;虽然都借助了平台的资源和能力&#xff0c;但很明显&#xff0c;微搭在集成腾讯平台上&#xff0c;更加具有针对性和特性。可以说是一个以腾讯微平台的企业级应用服务平台&…

基于python+Django+mysql鲜花水果销售商城网站系统设计与实现

博主介绍&#xff1a;黄菊华老师《Vue.js入门与商城开发实战》《微信小程序商城开发》图书作者&#xff0c;CSDN博客专家&#xff0c;在线教育专家&#xff0c;CSDN钻石讲师&#xff1b;专注大学生毕业设计教育、辅导。 所有项目都配有从入门到精通的基础知识视频课程&#xff…

LetsWave脑电数据简单ERP分析matlab(一)

LetsWave是基于matlab的一款工具包&#xff0c;类似eeglab&#xff0c;也可以对数据进行预处理。习惯使用eeglab做数据预处理的&#xff0c;可以先在eeglab中做预处理&#xff0c;然后可以保存为*.set格式&#xff0c;最后在letswave中画图。 letswave下载地址&#xff1a;htt…