文章目录

  • 概述
  • 场景说明:读写分离
  • 操作步骤
    • 工程结构
    • Step1 自定义注解
    • Step2 数据源定义
    • Step3 配置文件配置数据源
    • Step4 数据源实例化DatasourceConfig
    • Step5 Mybatis中配置成动态数据源
    • Step6 ThreadLocal管理当前线程使用的数据源连接
    • Step7 切面
    • Step 8 核心方法,重写AbstractRoutingDataSource#determineCurrentLookupKey
  • 测试
    • 库表数据
    • Domain
    • Dao
    • Service
    • Controller
    • 启动Spring Boot 工程
  • 代码

概述

之前总结过一篇基于Spring的 数据库切换的文章:Spring-基于Spring使用自定义注解及Aspect实现数据库切换 ,新的项目一般都直接采用SpringBoot开发了,那我们也用Spring Boot来整一版吧。

用到的东西包含: Spring Boot + Mybatis + Druid + MySql8 + lombok 等

鉴于我们是整合了Spring Boot +Mybatis , 不清楚如何整合的可以先看下
Spring Boot2.x-07Spring Boot2.1.2整合Mybatis


场景说明:读写分离

简单说下适用场景【读写分离】:数据库切换通常情况是用在项目中存在主从数据库的情况,为了减轻主库的压力,因为主从是同步的,所以读的操作我们直接取从库的数据,主库只负责写的操作。从库可以使多个,当然了主库也可以是多个,看项目架构。 这个同多数据源还是有差别的,如何支持多数据源,后面单独开篇介绍下。

废话不多说,直接撸起来吧


操作步骤

核心还是重写Spring的AbstractRoutingDataSource抽象类的determineCurrentLookupKey方法。


工程结构


Step1 自定义注解

这里我们先约定,自定义注解只能标注在方法上,如果需要也能标注在类上(因为后面的判断会有Aspect判断会所不同)请参考 Spring-基于Spring使用自定义注解及Aspect实现数据库切换

package com.artisan.annotation;import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;import com.artisan.config.DataSources;/*** *    自定义注解,用于切换数据源,默认MASTER_DB* @author yangshangwei**/@Documented
@Retention(RUNTIME)
@Target({ METHOD })
public @interface RouteDataSource {String value() default DataSources.MASTER_DB;}

Step2 数据源定义

为了方便能够注解引用,直接用接口吧

package com.artisan.config;/***   数据源列表* @author yangshangwei**/
public interface DataSources {String MASTER_DB = "masterDB";String SLAVE_DB = "slaveDB";}

Step3 配置文件配置数据源

我们这里采用application.yml ,注意前缀,后面要用。

# datasource  Master   前缀为自定义的datasource-master
spring:datasource-master:driver-class-name: com.mysql.cj.jdbc.Driver # JDBC连接Mysql6以上com.mysql.cj.jdbc.Driver (服务端为Mysql8)url: jdbc:mysql://localhost:3306/master?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=falseusername: rootpassword: root# datasource Replication 前缀为自定义的datasource-slavedatasource-slave: driver-class-name: com.mysql.cj.jdbc.Driver # JDBC连接Mysql6以上com.mysql.cj.jdbc.Driver  (服务端为Mysql8)url: jdbc:mysql://localhost:3306/slave?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=falseusername: rootpassword: root

Step4 数据源实例化DatasourceConfig

通过@Configuration标注为配置类。被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法将会被AnnotationConfigApplicationContextAnnotationConfigWebApplicationContext类进行扫描,并用于构建bean定义,初始化Spring容器。

application.yml中定义的前缀,别搞错了。

package com.artisan.config;import javax.sql.DataSource;import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import com.alibaba.druid.pool.DruidDataSource;@Configuration
public class DatasourceConfig {//destroy-method="close":当数据库连接不使用的时候,将该连接重新放到数据池中@Bean(name=DataSources.MASTER_DB,destroyMethod="close")@ConfigurationProperties(prefix = "spring.datasource-master")public DataSource dataSource() {// 创建数据源return DataSourceBuilder.create().type(DruidDataSource.class).build();}@Bean(name=DataSources.SLAVE_DB,destroyMethod="close")@ConfigurationProperties(prefix = "spring.datasource-slave")public DataSource dataSourceSlave() {// 创建数据源return DataSourceBuilder.create().type(DruidDataSource.class).build();}}

Step5 Mybatis中配置成动态数据源

@Configuration 功能不多说了,如上。

@MapperScan 通过使用@MapperScan可以指定要扫描的Mapper类的包的路径,当然了也可以在Mapper接口上声明@Mapper , 当然是@MapperScan更方便了。

内部@Bean用到了DynamicDataSource 继承自AbstractRoutingDataSource,就是我们刚开始说的核心

package com.artisan.config;import java.util.HashMap;
import java.util.Map;import javax.sql.DataSource;import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;@Configuration
@MapperScan(basePackages = { "com.artisan.dao" }) // 扫描的mybatis接口类的包名
public class MybatisConfig {@Autowired@Qualifier(DataSources.MASTER_DB)private DataSource masterDB;@Autowired@Qualifier(DataSources.SLAVE_DB)private DataSource slaveDB;/*** 动态数据源*/@Bean(name = "dynamicDataSource")public DataSource dynamicDataSource() {DynamicDataSource dynamicDataSource = new DynamicDataSource();// 默认数据源dynamicDataSource.setDefaultTargetDataSource(masterDB);// 配置多数据源Map<Object, Object> dataSourceMap = new HashMap<Object, Object>();dataSourceMap.put(DataSources.MASTER_DB, masterDB);dataSourceMap.put(DataSources.SLAVE_DB, slaveDB);dynamicDataSource.setTargetDataSources(dataSourceMap);return dynamicDataSource;}@Bean@ConfigurationProperties(prefix = "mybatis")public SqlSessionFactoryBean sqlSessionFactoryBean() throws Exception {SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();// 配置数据源,关键配置sqlSessionFactoryBean.setDataSource(dynamicDataSource());// 解决配置到配置文件中通过*配置找不到mapper文件的问题。 如果不设置这一行,在配置文件中,只能使用数组的方式一个个的罗列出来,并且要指定具体的文件名sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));return sqlSessionFactoryBean;}}

application.yml配置文件中新增mybatis的如下配置

# mybatis
mybatis:# 映射文件的路径 , 这个切换数据源的场景下不能配置 * 通配符,有多个 逗号隔开,继续跟 classpath:mapper/XXX# mapper-locations: classpath:mapper/ArtisanMapper.xml  # 在MybatisConfig.java#sqlSessionFactoryBean方法中通过sqlSessionFactoryBean设置classpath:mapper/*.xml ,不然每次都要改这个地方,不好维护。# 类型别名包配置,只能指定具体的包,多个配置可以使用英文逗号隔开type-aliases-package: com.artisan.domain# Mybatis SQL语句控制台打印configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

Step6 ThreadLocal管理当前线程使用的数据源连接

package com.artisan.config;import lombok.extern.slf4j.Slf4j;/*** * 使用ThreadLocal管理当前线程使用的数据源连接* * @author yangshangwei**/
@Slf4j
public class DatasourceContextHolder {public static final String DEFAULT_DATASOURCE = DataSources.MASTER_DB;private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();/*** 设置数据源* @param dbType*/public static void setDB(String dbType) {contextHolder.set(dbType);log.info("切换到数据源{}", dbType);}/*** 获取数据源*/public static String getDB() {return contextHolder.get();}/*** 清除数据源*/public static void clearDB() {contextHolder.remove();}
}

Step7 切面

通过Aspect 来处理自定义注解的横切逻辑。

package com.artisan.aspect;import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;import com.artisan.annotation.RouteDataSource;
import com.artisan.config.DatasourceContextHolder;import java.lang.reflect.Method;/*** 通过切面对自定义切库注解的方法进行拦截,动态的选择数据源* * @author yangshangwei**/@Slf4j
@Aspect
@Component
public class DynamicDataSourceAspect {/*** 前置增强,方法执行前,通过JoinPoint访问连接点上下文的信息* * @param joinPoint*/@Before("@annotation(com.artisan.annotation.RouteDataSource)")public void beforeSwitchDataSource(JoinPoint joinPoint) {// 获取连接点的方法签名对象MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();// 获取方法Method method = methodSignature.getMethod();// 设置默认的数据源为Master,防止切库出现异常执行失败的情况String dataSource = DatasourceContextHolder.DEFAULT_DATASOURCE;// 判断方法上是否标注了@RouteDataSourceif (method.isAnnotationPresent(RouteDataSource.class)) {RouteDataSource routeDataSource = method.getDeclaredAnnotation(RouteDataSource.class);// 获取@RouteDataSource上的value的值dataSource = routeDataSource.value();}// 设置数据源DatasourceContextHolder.setDB(dataSource);log.info("setDB {}", dataSource);}/*** 后置增强,清空DatasourceContextHolder,防止threadLocal误用带来的内存泄露*/@After("@annotation(com.artisan.annotation.RouteDataSource)")public void afterSwitchDataSource() {// 方法执行完成后,清除threadlocal中持有的databaseDatasourceContextHolder.clearDB();log.info("清空DatasourceContextHolder...");}/**@Before("@annotation(com.artisan.annotation.RouteDataSource)")public void beforeSwitchDataSource(JoinPoint point) {// 获得当前访问的classClass<?> className = point.getTarget().getClass();// 获得访问的方法名String methodName = point.getSignature().getName();// 得到方法的参数的类型Class[] argClass = ((MethodSignature) point.getSignature()).getParameterTypes();String dataSource = DatasourceContextHolder.DEFAULT_DATASOURCE;try {// 得到访问的方法对象Method method = className.getMethod(methodName, argClass);// 判断是否存在@DS注解if (method.isAnnotationPresent(RouteDataSource.class)) {RouteDataSource annotation = method.getAnnotation(RouteDataSource.class);// 取出注解中的数据源名dataSource = annotation.value();}} catch (Exception e) {log.error("routing datasource exception, " + methodName, e);}// 切换数据源DatasourceContextHolder.setDB(dataSource);}**/
}

Step 8 核心方法,重写AbstractRoutingDataSource#determineCurrentLookupKey

根据上面的AOP拦截,通过DatasourceContextHolder.getDB()动态的取出在切面里设置(DatasourceContextHolder.setDB(dataSource))的数据源即可。

package com.artisan.config;import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;import lombok.extern.slf4j.Slf4j;@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource{@Overrideprotected Object determineCurrentLookupKey() {log.info("数据源为{}", DatasourceContextHolder.getDB());return DatasourceContextHolder.getDB();}
}

测试

库表数据

Master:

-- ----------------------------
-- Table structure for artisan
-- ----------------------------
DROP TABLE IF EXISTS `artisan`;
CREATE TABLE `artisan` (`id` int(11) NOT NULL AUTO_INCREMENT,`name` varchar(255) DEFAULT NULL,`sex` varchar(255) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;-- ----------------------------
-- Records of artisan
-- ----------------------------
INSERT INTO `artisan` VALUES ('1', 'master', '女');
INSERT INTO `artisan` VALUES ('2', 'master2', '男');

Slave:

-- ----------------------------
-- Table structure for artisan
-- ----------------------------
DROP TABLE IF EXISTS `artisan`;
CREATE TABLE `artisan` (`id` int(11) NOT NULL AUTO_INCREMENT,`name` varchar(255) DEFAULT NULL,`sex` varchar(255) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;-- ----------------------------
-- Records of artisan
-- ----------------------------
INSERT INTO `artisan` VALUES ('1', 'replication1', '女');
INSERT INTO `artisan` VALUES ('2', 'replication2', '男');

Domain

package com.artisan.domain;import lombok.Data;@Data
public class Artisan {private Long  id ;private String name ;private String sex;}

Dao

package com.artisan.dao;import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;import com.artisan.domain.Artisan;
/*** * @author yangshangwei*   * 增加@Mapper这个注解之后,Spring 启动时会自动扫描该接口,这样就可以在需要使用时直接注入 Mapper 了* * MybatisConfig中标注了@MapperScan , 所以这里的@Mapper不加也行*/@Mapper
public interface ArtisanMapper {Artisan selectArtisanById(@Param("id") int id );}

对应的sql映射文件 ,当然了也可以使用@Select注解的方式,更简便。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" ><!-- 当Mapper接口和XML文件关联的时候, namespace的值就需要配置成接口的全限定名称 -->
<mapper namespace="com.artisan.dao.ArtisanMapper"><select id="selectArtisanById"  resultType="Artisan"> select id , name ,sex from artisan where id= #{id}</select>
</mapper>

Service

接口及实现类

忽略这个方法名,忘改了。。。。事实上是根据Id获取某个Artisan.

package com.artisan.service;import com.artisan.domain.Artisan;public interface ArtisanService {Artisan getArtisanListFromMaster(int id);Artisan getArtisanListFromSlave(int id);
}

实现类

通过自定义注解设置主从库 ,默认是主库,@RouteDataSource(DataSources.MASTER_DB)可以省略

package com.artisan.service.impl;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import com.artisan.annotation.RouteDataSource;
import com.artisan.config.DataSources;
import com.artisan.dao.ArtisanMapper;
import com.artisan.domain.Artisan;
import com.artisan.service.ArtisanService;@Service
public class ArtisanServiceImpl implements ArtisanService {@Autowiredprivate ArtisanMapper artisanMapper;@Override@RouteDataSource(DataSources.MASTER_DB)public Artisan getArtisanListFromMaster(int id) {return artisanMapper.selectArtisanById(id);}@Override@RouteDataSource(DataSources.SLAVE_DB)public Artisan getArtisanListFromSlave(int id) {return artisanMapper.selectArtisanById(id);}}

Controller

package com.artisan.controller;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import com.artisan.domain.Artisan;
import com.artisan.service.ArtisanService;@RestController
public class ArtisanController {@Autowiredprivate ArtisanService artisanService ;@GetMapping("/getDataFromMaster")public Artisan getDataFromMaster(int id) {return artisanService.getArtisanListFromMaster(id);}@GetMapping("/getDataFromRep")public Artisan getDataFromRep(int id) {return artisanService.getArtisanListFromSlave(id);}}

启动Spring Boot 工程

为了验证功能,我们从主从库均是查询操作吧。

访问主库:
http://localhost:8080/getDataFromMaster?id=1

访问从库:
http://localhost:8080/getDataFromRep?id=2


为了方便用application.properties的童鞋,代码如下,验证通过

#master
spring.datasource-master.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource-master.url=jdbc:mysql://localhost:3306/master?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource-master.username=root
spring.datasource-master.password=root
spring.datasource-master.type=com.alibaba.druid.pool.DruidDataSource#slave
spring.datasource-slave.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource-slave.url=jdbc:mysql://localhost:3306/slave?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource-slave.username=root
spring.datasource-slave.password=root
spring.datasource-slave.type=com.alibaba.druid.pool.DruidDataSource#mybatis
#mybatis.mapper-locations=classpath:mapper/ArtisanMapper.xml
mybatis.type-aliases-package=com.artisan.domain

pom.xml

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.2.RELEASE</version><relativePath /> <!-- lookup parent from repository --></parent><groupId>com.artisan</groupId><artifactId>RoutingDataSource</artifactId><version>0.0.1-SNAPSHOT</version><name>RoutingDataSource</name><description>Artisan </description><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>1.3.2</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.10</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><optional>true</optional></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

代码

https://github.com/yangshangwei/RoutingDataSource

Spring Boot2.x-09 基于Spring Boot 2.1.2 + Mybatis使用自定义注解实现数据库切换相关推荐

  1. Spring Data JPA例子[基于Spring Boot、Mysql]

    关于Spring Data Spring社区的一个顶级工程,主要用于简化数据(关系型&非关系型)访问,如果我们使用Spring Data来开发程序的话,那么可以省去很多低级别的数据访问操作,如 ...

  2. spring配置与监听mysql_spring boot (8)mybatis配置监听,

    我用过两次, 1.一次是数据库以前数据可以为null,现在不想为null,然后不想改代码,所以在插入或修改数据的时候,查一次,把为null的数据变一下,字符串变"",数字变0或-1 ...

  3. Spring boot 少量字段不使用对象接收 自定义注解转成JSON格式

    一 自定义注解 /*** @author qujiawen* @version 1.0.0* @ClassName RequestJson.java* @Description TODO* @crea ...

  4. Spring Boot2.x-10 基于Spring Boot 2.1.2 + Mybatis 2.0.0实现多数据源,支持事务

    文章目录 概述 思路 步骤 Step1 多数据源配置文件applicaiton.yml Step2 初始化多个数据源 Step3 配置多个数据源 验证测试 支持事务 Step1 配置类中通过@Bean ...

  5. 基于 Spring Boot + Vue 实现的可视化拖拽编辑的大屏项目

    大家好,今天给小伙伴们分享一个基于 SpringBoot + Vue 实现的可视化拖拽编辑的大屏项目: 简介 这个是一个开源的一个BI平台,酷炫大屏展示,能随时随地掌控业务动态,让每个决策都有数据支撑 ...

  6. 基于Spring Boot+Cloud构建微云架构

    链接:my.oschina.net/u/3636867/blog/1802517 前言 首先,最想说的是,当你要学习一套最新的技术时,官网的英文文档是学习的最佳渠道.因为网上流传的多数资料是官网翻译而 ...

  7. 基于Spring Boot和Spring Cloud实现微服务架构学习--转

    原文地址:http://blog.csdn.net/enweitech/article/details/52582918 看了几周spring相关框架的书籍和官方demo,是时候开始总结下这中间的学习 ...

  8. 基于 Spring Boot 和 Spring Cloud 实现微服务架构

    前言 首先,最想说的是,当你要学习一套最新的技术时,官网的英文文档是学习的最佳渠道.因为网上流传的多数资料是官网翻译而来,很多描述的重点也都偏向于作者自身碰到的问题,这样就很容易让你理解和操作出现偏差 ...

  9. freemarker ftl模板_Spring Boot2 系列教程(十)Spring Boot 整合 Freemarker

    今天来聊聊 Spring Boot 整合 Freemarker. Freemarker 简介 这是一个相当老牌的开源的免费的模版引擎.通过 Freemarker 模版,我们可以将数据渲染成 HTML ...

最新文章

  1. python爬虫软件-8个最高效的Python爬虫框架,你用过几个?
  2. shopping car 2.0
  3. 黑马lavarel教程---4、csrf验证及相关
  4. python获取一个模块的路径_如何获取 Python 模块的路径
  5. places for finding a consulting job
  6. 浅谈:稀疏数组与二维数组之间的转换
  7. c语言程序中必不可少的,C语言程序设计(第3章程序控制语句)2
  8. 给 c# 程序员的十个重要提示
  9. 开源合规处理方法_经济高效的开源软件许可合规模型
  10. Linux bash shell递归函数
  11. dsp31段最佳调音图_车载dsp功放调音小经验分享!dsp调音31段EQ调音图与皇帝位时间延迟调整...
  12. 数据库mysql表常见字段大小_常用数据库的字段类型及大小
  13. L 2 聚焦和发散思维模式
  14. Android性能优化最佳实践,知乎上转疯了!
  15. 直流电机的PWM调速
  16. 今天来和大家一起分享一下好玩的游戏---美国农场主
  17. 手机百度浏览器ua标识在哪里_浏览器标识(ua)的那些事
  18. ERNIE-ViL: Knowledge Enhanced Vision-Language Representations Through Scene Graph
  19. linux 造字程序下载,truetype造字程序
  20. 如何在Excel中使用SQL语言?

热门文章

  1. opencv 入门 demo
  2. keras 多个显卡
  3. python编程书籍1020python编程书籍_代写INFT 1020作业、Database作业代做、Java课程作业代写、c++,Python编程作业代做...
  4. python字符串替换空格_python - 用pandas中的NaN替换空白值(空格)
  5. 95. Leetcode 1049. 最后一块石头的重量 II (动态规划-背包问题)
  6. 24. Leetcode 61. 旋转链表 (链表-基础操作类-旋转链表)
  7. Leetcode 83 删除排序链表中的重复元素 (每日一题 20210804)
  8. MATLAB从入门到精通系列之MATLAB维度获取size()函数详解
  9. MATLAB实战系列(四)-导入txt文件技巧大全
  10. Matlab 自带机器学习算法汇总