• IDEA操作Sharding-JDBC实战2


    一、强制路由与数据加解密

    1.强制路由

    • 在一些应用场景中,分片条件并不存在于SQL,而存在于外部业务逻辑。因此需要提供一种通过外部业务代码配置指定路由的一种方式,在ShardingSphere中叫做Hint。如果使用hint强制路由,那么sql将无视原有的分片逻辑,直接路由到指定的数据节点上操作。

    • Hint使用场景:
      数据分片操作,例如分片键没有在SQL或数据表中,而是在业务逻辑代码中;
      读写分离操作,例如强制在主库进行某些数据操作。

    • 在resources目录下创建配置文件application-hint-database.properties:

      # datasource
      spring.shardingsphere.datasource.names=ds0,ds1
      # master
      spring.shardingsphere.datasource.ds0.type=com.zaxxer.hikari.HikariDataSource
      spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.cj.jdbc.Driver
      spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://192.168.126.21:3306/zzx1?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
      spring.shardingsphere.datasource.ds0.username=root
      spring.shardingsphere.datasource.ds0.password=123456
      # slave0
      spring.shardingsphere.datasource.ds1.type=com.zaxxer.hikari.HikariDataSource
      spring.shardingsphere.datasource.ds1.driver-class-name=com.mysql.cj.jdbc.Driver
      spring.shardingsphere.datasource.ds1.jdbc-url=jdbc:mysql://192.168.126.22:3306/zzx1?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
      spring.shardingsphere.datasource.ds1.username=root
      spring.shardingsphere.datasource.ds1.password=123456
      # hint
      spring.shardingsphere.sharding.tables.city.database-strategy.hint.algorithm-class-name=com.zzx.hint.MyHintShardingAlgorithm
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16

      即配置数据源以及强制路由算法

    • 创建一个强制路由算法实现类MyHintShardingAlgorithm:

      public class MyHintShardingAlgorithm implements HintShardingAlgorithm<Integer> {
          @Override
          public Collection<String> doSharding(Collection<String> collection, HintShardingValue<Integer> hintShardingValue) {
              ArrayList<String> result = new ArrayList();
              //数据源datasource集合
              for (String each : collection)
              {
                  //路由键集合
                  for (Integer value : hintShardingValue.getValues())
                  {
                      if(each.endsWith(String.valueOf(value%2)))
                      {
                         result.add(each);
                      }
                  }
              }
      
              return result;
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
    • 在主配置文件application.properties中指定配置文件的名称:

      #指定Sharding-JDBC配置文件的名称
      spring.profiles.active=hint-database
      
      • 1
      • 2
    • 创建一个测试类TestHint:

      @SpringBootTest(classes = RunBoot.class)
      public class TestHint {
          @Resource
          private CityRepository cityRepository;
          @Test
          public void test1()
          {
              HintManager instance = HintManager.getInstance();
              instance.setDatabaseShardingValue(1); //强制路由到ds${xx%2}
              List<City> cityList = cityRepository.findAll();
              cityList.forEach(city -> {
                  System.out.println(city.getId()+" "+city.getName()+" "+city.getProvince());
              });
          }
      }	
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15

    2.数据脱敏

    • 数据脱敏,是指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。涉及客户安全数据或者一些商业性敏感数据,如身份证号、手机号、卡号、客户号等个人信息,按照规定都需要进行数据脱敏。数据脱敏模块属于ShardingSphere分布式治理这一核心功能下的子功能模块。
      • 在更新操作时,它通过对用户输入的SQL进行解析,并依据用户提供的脱敏配置对SQL进行改写,从而实现对原文数据进行加密,,并将密文数据存储到底层数据库。
      • 在查询数据时,它会从数据库中取出密文数据并对其解密,最终将解密后的原始数据返回给用户。
    • Encrypt-JDBC将用户发起的SQL进行拦截,并通过SQL语法解析器进行解析、理解SQL行为,再依据用户传入的脱敏规则,找出需要脱敏的字段和所使用的加解密器对目标字段进行加解密处理后,再与底层数据库进行交互。
    • 脱敏配置,主要分为4个部分,数据源配置、加密器配置、脱敏表配置以及查询属性配置。
      • 数据源配置:指数据源配置。
      • 加密算法配置:指使用什么加密算法进行加解密。目前ShardingSphere内置三种加解密算法,AES、MD5和RC4。用户还可以通过实现ShardingSphere提供的接口,自定义实现一套加解密算法。
      • 加密表配置:用于告诉ShardingSphere数据表里哪一个列用于存储密文数据(cipher Column)、哪个列用于存储明文数据(plainColumn)以及用户想使用哪个列进行SQL编写(logicColumn)。
    • Apache ShardingSphere将面向用户的逻辑列与面向底层数据库的明文列和密文列进行了列名以及数据的加密映射转换。即将逻辑列对应的明文列进行加密,存储到密文列

    3.数据脱敏实战

    • 创建c_user表,在可视化工具或者Mysql命令行执行如下:

      CREATE TABLE `c_user` (
        `id` bigint(11) NOT NULL AUTO_INCREMENT,
        `name` varchar(256) DEFAULT NULL,
        `pwd_plain` varchar(256) DEFAULT NULL,
        `pwd_cipher` varchar(256) DEFAULT NULL,
        PRIMARY KEY (`id`)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
    • 在com.zzx.entity包下,创建一个CUser类,添加如下:

      @Entity
      @Table(name="c_user")
      @Data
      public class CUser implements Serializable {
          @Id
          @GeneratedValue(strategy = GenerationType.IDENTITY)
          private Long id;
      
          @Column(name="name")
          private String name;
      
          @Column(name="pwd")
          private String pwd;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
    • 在com.zzx.repository包下,创建一个接口CUserRepository,添加如下:

      public interface CUserRepository extends JpaRepository<CUser,Long> {
      }
      
      • 1
      • 2
    • 在resources目录下,创建配置文件application-encryptor.properties,添加如下:

      # datasource
      spring.shardingsphere.datasource.names=ds0
      # ds0
      spring.shardingsphere.datasource.ds0.type=com.zaxxer.hikari.HikariDataSource
      spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.cj.jdbc.Driver
      spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://192.168.126.21:3306/zzx1?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
      spring.shardingsphere.datasource.ds0.username=root
      spring.shardingsphere.datasource.ds0.password=123456
      #encryptor 密码列加密
      spring.shardingsphere.encrypt.tables.c_user.columns.pwd.plain-column=pwd_plain
      spring.shardingsphere.encrypt.tables.c_user.columns.pwd.cipher-column=pwd_cipher
      spring.shardingsphere.encrypt.encryptors.my_pwd.type=aes
      spring.shardingsphere.encrypt.encryptors.my_pwd.props.aes.key.value=123456
      spring.shardingsphere.encrypt.tables.c_user.columns.pwd.encryptor=my_pwd
      
      # 主键生成器
      # c_user
      spring.shardingsphere.sharding.tables.c_user.key-generator.column=id
      spring.shardingsphere.sharding.tables.c_user.key-generator.type=SNOWFLAKE
      # false即直接匹配pwd_plain,反之找pwd_cipher进行解密。默认为true
      #spring.shardingsphere.props.query.with.cipher.column=false
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
    • 修改主配置文件application.properties,指定配置文件:

      #指定Sharding-JDBC配置文件的名称
      spring.profiles.active=encryptor
      
      • 1
      • 2
    • 在test目录创建一个测试类TestEncryptor,添加一条数据:

      @SpringBootTest(classes = RunBoot.class)
      public class TestEncryptor {
      
          @Resource
          private CUserRepository cUserRepository;
      
          @Test
          public void testAdd()
          {
              CUser cUser = new CUser();
              cUser.setName("zzx");
              cUser.setPwd("856");
              cUserRepository.save(cUser);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15

      即会将pwd列进行一个加密,加过密的放在密文列pwd_cipher,未加密的放在明文列pwd_plain。

    • 在CUserRepository接口添加如下:

      List<CUser> findByPwd(String pwd);
      
      • 1
    • 在TestEncryptor类,查询数据:

      @Test
      public void testFind()
      {
          List<CUser> cUsers = cUserRepository.findByPwd("856");
          cUsers.forEach(cUser -> {
              System.out.println(cUser.getId()+" "+cUser.getName()+" "+cUser.getPwd());
          });
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8

    二、分布式事务

    1.CAP与BASE

    • CAP(强一致性),对于共享数据系统,最多只能同时拥有CAP其中的两个,任意两个都有其适应的场景。
      • C(consistence)一致性,所有节点上的数据时刻保持同步。
      • A(availiblity)可用性,每个请求都能接受到一个响应,无论响应成功或失败。
      • P(partition tolerance) 分区容错性,系统应该能持续提供服务,即使系统内部有消息丢失。
    • BASE(最终一致性),BASE是指基本可用(Basically Available)、软状态(Soft State)、最终一致性(Eventual Consistency)。它的核心思想是即使无法做到强一致性(CAP就是强一致性),但应用可以采用适合的方式达到最终一致性。

    2.2PC和3PC

    • 2PC模式(强一致性)
      2PC是Two-Phase Commit缩写,即两阶段提交,就是将事务的提交过程分为两个阶段来处理。事务的发起者称协调者,事务的执行者称参与者。协调者统一协调参与者执行。

      • 阶段一:准备阶段,协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待所有参与者答复。各参与者执行事务操作,但不提交事务,将undo和redo信息记入事务日志中。如参与者执行成功,给协调者反馈yes;如执行失败,给协调者反馈no。
      • 阶段二:提交阶段,如果协调者收到了参与者的失败消息或超时,直接给每个参与者发送回滚(rollback)消息;否则,发送提交(commit)消息。
    • 2PC方案实现起来简单,实际项目中使用比较少,主要因为以下问题:

      • 性能问题:所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。
      • 可靠性问题:如果协调者存在单点故障问题,如果协调者出现故障,参与者将一直处于锁定状态。
      • 数据一致性问题:在阶段二中,如果发送局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没有收到提交消息,那么就导致了节点直接数据的不一致。
    • 3PC模式(强一致性)
      3PC(三阶段提交),是两阶段提交的改进版本,与两阶段提交不同的是,引入超时机制。同时在协调者和参与者中都引入超时机制。三阶段提交将两阶段的准备阶段拆分为两个阶段,插入了一个preCommit阶段,解决了原先在两阶段提交中,参与者在准备之后,由于协调者或参与者发生崩溃或错误,而导致参与者无法知晓并处于长时间等待的问题。如果在指定的时间内,协调者没有收到参与者的消息则默认失败。

      • 阶段一:canCommit,协调者向参与者发送commit请求,参与者如果可以提交则返回yes响应,否则返回no响应。
      • 阶段二:preCommit,协调者根据阶段一canCommit参与者的反应情况执行预提交事务或中断事务操作。
        • 参与者均反馈yes:协调者向所有参与者发出preCommit请求,参与者收到preCommit请求后,执行事务操作,但不提交;将undo和redo信息记入事务日志中;各参与者向协调者反馈ack或no响应,并等待最终指令。
        • 任何一个参与者反馈no或等待超时:协调者向所有参与者发出abort请求,无论收到协调者发出的abort请求,或者在等待协调者请求过程中出现超时,参与者均会中断事务。
      • 阶段三:doCommit,该阶段进行真正的事务提交,根据阶段二preCommit参与者反馈的结果完成事务提交或中断操作。
    • 相比2PC模式,3PC模式降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点故障问题,阶段三中,协调者出现问题时(如网络中断等),参与者会继续提交事务。

    3.XA模式(强一致性)

    • XA是由X/Open组织提出的分布式事务的规范,是基于两阶段提交协议。XA规范主要定义了全局事务管理器(TM)和局部资源管理器(RM)之间的接口。目前主流的关系型数据库产品都是实现了XA接口。
    • XA之所以需要引入事务管理器,是因为在分布式系统中,两台机器理论上无法达到一致的状态,需要引入一个单点进行协调。由全局事务管理器管理和协调的事务可以跨越多个资源(数据库)和进程。事务管理器用来保证所有的事务参与者都完成了准备工作(第一阶段)。如果事务管理器收到所有参与者都准备好的消息,就会通知所有的事务都可以提交了(第二阶段)。MySQL在这个XA事务中扮演的是参与者的角色,而不是事务管理器。

    4.TCC模式(最终一致性)

    • TCC(Try-Confirm-Cancel),是服务化的两阶段编程模型,其Try、Confirm、Cancel3个方法均由业务编码实现:
      • Try操作作为一阶段,负责资源的检查和预留;
      • Confirm操作作为二阶段提交操作,执行真正的业务;
      • Cancel是预留资源的取消;
    • TCC模式相比于XA,解决了如下几个缺点:
      • 解决了协调者单点,由业务应用发起并完成这个业务活动。业务活动管理器可以变成多点,引入集群。
      • 同步阻塞,引入超时机制,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。
      • 数据一致性,有了补偿机制之后,由业务活动管理器控制一致性。

    5.Sharding-JDBC整合XA数据源

    • XAShardingSphereTransactionManager为Apache ShardingSphere的分布式事务的XA实现类。它主要负责对多数据源进行管理和适配,并且将相应事务的开启、提交和回滚操作委托给具体的XA事务管理器。ShardingSphere整合XA事务时,分离了XA事务管理和连接池管理,这样接入XA时,可以做到对业务的零侵入。
    • ShardingSphere支持以下功能:
      • 支持数据分片后的跨库XA事务;
      • 两阶段提交保证操作的原子性和数据的强一致性;服务宕机重启后,提交/回滚中的事务可自动恢复;
      • SPI机制整合主流的XA事务管理器,默认Atomikos;
      • 同时支持XA和非XA的连接池;
      • 提供spring-boot和namespace的接入端。
    • 开启全局事务
      XAShardingSphereTransactionManager将调用具体的XA事务管理器开启XA全局事务,以XID的形式进行标记。
    • 执行真实分片SQL
      XAShardingSphereTransactionManager将数据库连接所对应的XAResource注册到当前XA事务之后,事务管理器会在此阶段发送XAResource.start命令至数据库。数据库在收到XAResource.end命令之前的所有SQL操作,会被标记为XA事务。
    • 提交或回滚事务
      XAShardingSphereTransactionManager在接收到接入端的提交命令后,会委托实际的XA事务管理进行提交动作,事务管理器将收集到的当前线程中所有注册的XAResource,并发送XAResource.end指令,用以标记此XA事务边界。接着会依次发送prepare指令,收集所有参与XAResource投票。若所有XAResource的反馈结果均为正确,则调用commit指令进行最终提交;若有任意XAResource的反馈结果不正确,则调用rollback指令进行回滚。在事务管理器发出提交指令后,任何XAResource产生的异常都会通过恢复日志进行重试,以保证提交阶段的操作原子性和数据强一致性。

    6.分布式事务实战

    • ShardingSphere整合了XA,为分布式事务控制提供了极大的便利,可以在应用程序编程时,采用以下统一模式进行使用。

    • 引入Maven依赖

      <!-- https://mvnrepository.com/artifact/org.apache.shardingsphere/sharding-transaction-xa-core -->
      <dependency>
          <groupId>org.apache.shardingsphere</groupId>
          <artifactId>sharding-transaction-xa-core</artifactId>
          <version>4.1.1</version>
      </dependency>
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
    • ShardingSphere默认的XA事务管理器为Atomikos,可以通过在项目的classpath中添加jta.properties(即Resources目录)来定制Atomikos配置项。具体配置如下:

      #指定是否启动磁盘日志,默认为true。在生产环境下一定要保证为true,否则数据的完整性无法保证
      com.atomikos.icatch.enable_logging=true
      #JTA/XA资源是否应该自动注册
      com.atomikos.icatch.automatic_resource_registration=true
      #JTA事务的默认超时时间,默认为10000ms
      com.atomikos.icatch.default_jta_timeout=10000
      #事务的最大超时时间,默认为300000ms。这表示事务超时时间由
      #UserTransaction.setTransactionTimeout()较大者决定。4.x版本之后,指定为0的话则表示不设置超时时间
      com.atomikos.icatch.max_timeout=300000
      #指定在两阶段提交时,是否使用不同的线程(意味着并行)3.7版本之后默认为false,更早的版本默认为true。如果为false,则提交将按照事务中访问资源的顺序进行。
      com.atomikos.icatch.threaded_2pc=false
      #指定最多可以同时运行的事务数量,默认值为50,负数表示没有数量限制。在调用
      #UserTransaction.begin()方法时,可能会抛出一个”Max number of active transactionsreached”异常信息,表示超出最大事务数限制
      com.atomikos.icatch.max_actives=50
      #是否支持subtransaction,默认为true
      com.atomikos.icatch.allow_subtransactions=true
      #指定在可能的情况下,是否应该join子事务(subtransactions),默认值为true。如果设置为false,对于有关联的不同subtransactions,不会调用XAResource.start(TM_JOIN)
      com.atomikos.icatch.serial_jta_transactions=true
      #指定JVM关闭时是否强制(force)关闭事务管理器,默认为false
      com.atomikos.icatch.force_shutdown_on_vm_exit=false
      #在正常关闭(no-force)的情况下,应该等待事务执行完成的时间,默认为Long.MAX_VALUE
      com.atomikos.icatch.default_max_wait_time_on_shutdown=9223372036854775807
      #========= 日志记录配置=======
      #事务日志目录,默认为./。
      com.atomikos.icatch.log_base_dir=./
      #事务日志文件前缀,默认为tmlog。事务日志存储在文件中,文件名包含一个数字后缀,日志文件以.log为扩展名,如tmlog1.log。遇到checkpoint时,新的事务日志文件会被创建,数字增加。
      com.atomikos.icatch.log_base_name=tmlog
      #指定两次checkpoint的时间间隔,默认为500
      com.atomikos.icatch.checkpoint_interval=500
      #=========日志恢复配置=============
      #指定在多长时间后可以清空无法恢复的事务日志(orphaned),默认86400000ms
      com.atomikos.icatch.forget_orphaned_log_entries_delay=86400000
      #指定两次恢复扫描之间的延迟时间。默认值为与#com.atomikos.icatch.default_jta_timeout相同
      com.atomikos.icatch.recovery_delay=${com.atomikos.icatch.default_jta_timeout}
      #提交失败时,再抛出一个异常之前,最多可以重试几次,默认值为5
      com.atomikos.icatch.oltp_max_retries=5
      #提交失败时,每次重试的时间间隔,默认10000ms
      com.atomikos.icatch.oltp_retry_interval=10000
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
    • 在test目录下创建一个类TestShardingTransaction,添加如下:

      @SpringBootTest(classes = RunBoot.class)
      public class TestShardingTransaction {
          @Resource
          private PositionRepository positionRepository;
          @Resource
          private PositionDetailRepository positionDetailRepository;
          @Test
          @Transactional
          //@ShardingTransactionType(TransactionType.XA)
          public void testAdd()
          {
              TransactionTypeHolder.set(TransactionType.XA);
              for (int i = 1; i <= 20; i++) {
                  Position position = new Position();
                  //position.setId((long)i);
                  position.setName("zzx"+i);
                  position.setSalary(2000d);
                  position.setCity("shenzhen");
                  positionRepository.save(position);
                  if(i==3)
                      throw  new RuntimeException("人造异常");
                  PositionDetail positionDetail = new PositionDetail();
                  positionDetail.setPid(position.getId());
                  positionDetail.setDescription("description"+i);
                  positionDetailRepository.save(positionDetail);
              }
      
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29

      即使用XA事务来回滚或提交,并制造一个异常进行测试。使用注解或者使用代码的形式来指定事务。

    • 修改application.properties主配置文件,指定数据源等配置文件:

      #指定Sharding-JDBC配置文件的名称
      spring.profiles.active=sharding-database
      
      • 1
      • 2

    此时运行结果有异常,事务会进行回滚,所以表及其子表都没有数据。

    7.数据库编排治理

    • 配置集中化:越来越多的运行时实例,使得散落的配置难以管理,配置不同步导致的问题十分严重。将配置集中于配置中心,可以更加有效的进行管理。
    • 配置动态化:配置修改后的分发,是配置中心可以提供的另一个重要能力。它可支持数据源、表与分片及读写分离策略的动态切换。
    • 配置中心数据结构,配置中心在定义的命名空间的config下,可通过修改节点来实现对于配置的动态管理。

    总结:

    1. 强制路由,即自定义强制路由算法,实现HintShardingAlgorithm接口,在配置文件中指定该算法,需要时直接获取强制路由hint实例,并设置路由键即可。
    2. 数据脱敏,即使用配置文件配置需要脱敏的逻辑列、加密的算法和盐值等,将插入的列进行加密,Sharding-JDBC在数据库中将未加密的放在明文列,加密的放在密文列。可不配置明文列。
      encryptor,加密的算法分为AES、MD5和RC4。在写操作时,会对逻辑列进行加密操作,在读操作时,进行解密操作。
    3. 分布式事务
      2PC即协调者给参与者发送事务内容,参与者记入事务日志,可以提交则返回yes,不可以则返回no;yes则给每个参与者发送提交消息,no则发送回滚消息。
      3PC则是2PC的改进版本,将阶段一分为两个阶段,再加入超时机制。
      将反馈后的处理单独分为一个阶段。
      XA是基于两阶段提交协议的。
      常见的分布式事务解决方案有2PC、3PC、XA和使用MQ。
      分布式事务,即协调者发送precommit指令,测试各参与者是否可以正常运行;根据参与者反馈来发起事务预提交或中断(即写入事务日志),最后再根据参与者的反馈进行最后的提交或中断。
  • 相关阅读:
    SpringBoot如何集成Mybatis呢?
    【深入理解TcaplusDB技术】Tmonitor后台一键安装
    JavaScript 59 JavaScript 常见错误
    腾讯云服务器简介_CVM优势_常见问题解答
    python-文件和异常
    enum枚举的使用
    matlab神经网络求解最优化,matlab神经网络应用设计
    Python仪表板巨人之战 Streamlit vs Dash vs Voilà vs Panel
    c++_learning-模板元编程
    JVM调优工具锦囊:JDK自带工具与Arthas线上分析工具对比
  • 原文地址:https://blog.csdn.net/weixin_49076273/article/details/126911569