MySQL 读写分离之 ShardingSphere
ShardingSphere-Proxy
ShardingShpere-Proxy 是透明化的数据库代理,封装了分库分表的底层实现,将自己伪装成一个数据库,兼容任何 MySQL 协议的访问客户端操作数据。它可以与 ShardingSphere-JDBC 混合使用,也能单独作为数据库底层与用户之间的啮合层,说白了就是伪装成数据库,作为数据库对应用的代理库。详情参考ShardingSphere官方文档

一、安装部署
ShardingShpere-Proxy 作为数据库中间件可以使用安装包部署也可以使用 docker 容器部署,现在使用 docker 容器部署代理。
1. 拉取镜像
docker pull apache/shardingsphere-proxy
2. 创建目录
mkdir -p /usr/local/src/shardingsphere-proxy/logs \ /usr/local/src/shardingsphere-proxy/ext-lib \ /usr/local/src/shardingsphere-proxy/config
3. 创建代理容器
docker run \ --restart=always \ --privileged=true \ -e PORT=3307 \ -p 3307:3307 --name shardingsphere-proxy \ -v /usr/local/src/shardingsphere-proxy/logs:/opt/shardingsphere-proxy/logs \ -v /usr/local/src/shardingsphere-proxy/ext-lib:/opt/shardingsphere-proxy/ext-lib \ -v /usr/local/src/shardingsphere-proxy/config:/opt/shardingsphere-proxy/conf \ -d apache/shardingsphere-proxy:latest
二、配置
1. 数据库驱动
把 MySQL 的驱动包 mysql-connector-java-8.0.27.jar 放在目录 /usr/local/src/shardingsphere-proxy/logs 下。驱动包可以从 maven 仓库拷贝。
2. 代理服务配置
server.yaml
创建 server.yaml 文件,命令:vim server.yaml 写入以下内容。
1)shardingsphere-proxy 5.0 server.yaml 配置
rules: - !AUTHORITY users: # 用于登录计算节点的用户名,授权主机和密码的组合。格式:<username>@<hostname>:<password>,hostname 为 % 或空字符串表示不限制授权主机 - root@%:123456 provider: type: ALL_PRIVILEGES_PERMITTED # 存储节点数据授权的权限提供者类型,缺省值为 ALL_PRIVILEGES_PERMITTED
2)shardingsphere-proxy 4.1.1 server.yml 配置
orchestration: orchestration_ds: orchestrationType: registry_center,config_center,distributed_lock_manager instanceType: zookeeper serverLists: 1.14.194.150:2181 namespace: orchestration props: overwrite: true retryIntervalMilliseconds: 5000 timeToLiveSeconds: 60 maxRetries: 30 operationTimeoutMilliseconds: 50000 authentication: users: root: password: root sharding: password: sharding authorizedSchemas: partition_db props: max.connections.size.per.query: 1 acceptor.size: 16 # The default value is available processors count * 2. executor.size: 16 # Infinite by default. proxy.frontend.flush.threshold: 128 # The default value is 128. # LOCAL: Proxy will run with LOCAL transaction. # XA: Proxy will run with XA transaction. # BASE: Proxy will run with B.A.S.E transaction. proxy.transaction.type: LOCAL proxy.opentracing.enabled: false proxy.hint.enabled: false query.with.cipher.column: true sql.show: true allow.range.query.with.inline.sharding: false
config-sharding.yaml
创建 config-datasource.yaml,命令:vim config-datasource.yaml 写入以下内容。
1)shardingsphere-proxy 5.0 config-sharding.yaml 配置
schemaName: partition_db dataSourceCommon: username: root password: 123456 connectionTimeoutMilliseconds: 30000 idleTimeoutMilliseconds: 60000 maxLifetimeMilliseconds: 1800000 maxPoolSize: 50 minPoolSize: 1 maintenanceIntervalMilliseconds: 30000 dataSources: sharding00: url: jdbc:mysql://1.14.194.150:3306/sharding00?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=utf-8&useSSL=false sharding01: url: jdbc:mysql://1.14.30.31:3306/sharding01?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=utf-8&useSSL=false sharding02: url: jdbc:mysql://1.117.105.119:3306/sharding02?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=utf-8&useSSL=false rules: - !SHARDING tables: user: actualDataNodes: sharding0$->{0..2}.user_$->{0..1} tableStrategy: standard: shardingColumn: id shardingAlgorithmName: user_inline keyGenerateStrategy: column: id keyGeneratorName: snowflake defaultDatabaseStrategy: standard: shardingColumn: id shardingAlgorithmName: database_inline defaultTableStrategy: none: shardingAlgorithms: database_inline: type: INLINE props: algorithm-expression: sharding0$->{(id % 3)} user_inline: type: INLINE props: algorithm-expression: user_$->{(id % 2)} keyGenerators: snowflake: type: SNOWFLAKE props: worker-id: 1 # SNOWFLAKE 策略参数:机器标识ID,取值范围[0,1024) max.tolerate.time.difference.milliseconds: 5 # 最大容忍回拨时间 max.vibration.offset: 1 # 低并发下解决 ID 奇偶分片的问题
2)shardingsphere-proxy 4.1.1 config-sharding.yaml 配置
schemaName: partition_db dataSources: sharding00: url: jdbc:mysql://1.14.194.150:3306/sharding00?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: 123456 connectionTimeoutMilliseconds: 30000 idleTimeoutMilliseconds: 60000 maxLifetimeMilliseconds: 1800000 maxPoolSize: 65 sharding01: url: jdbc:mysql://1.14.30.31:3306/sharding01?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: 123456 connectionTimeoutMilliseconds: 30000 idleTimeoutMilliseconds: 60000 maxLifetimeMilliseconds: 1800000 maxPoolSize: 65 sharding02: url: jdbc:mysql://1.117.105.119:3306/sharding02?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: 123456 connectionTimeoutMilliseconds: 30000 idleTimeoutMilliseconds: 60000 maxLifetimeMilliseconds: 1800000 maxPoolSize: 65 shardingRule: tables: user: actualDataNodes: sharding0$->{0..2}.user_$->{0..1} databaseStrategy: inline: shardingColumn: id algorithmExpression: sharding0$->{(id % 3)} tableStrategy: inline: # 行表达式分片策略 shardingColumn: id # 分片列名称 algorithmExpression: user_$->{(id % 2)} # 分片算法行表达式,需符合groovy语法 keyGenerator: column: id # 主键列 type: SNOWFLAKE # 生成策略 UUID SNOWFLAKE props: # 可选配置 worker-id: 1 # SNOWFLAKE 策略参数:机器标识ID,取值范围[0,1024) max.tolerate.time.difference.milliseconds: 5 # 最大容忍回拨时间 max.vibration.offset: 1 # 低并发下解决 ID 奇偶分片的问题 defaultTableStrategy: none:
ShardingSphere-JDBC
ShardingSphere-JDBC 是一个轻量级 Java 框架,在 JDBC 层提供额外服务,使用客户端直连,而不是中间件转发的方式有效降低性能损耗。兼容各种主流的 ORM 框架,支持任意实现 JDBC 规范的数据库,也就是应用端对数据库的代理,可以理解为“JDBC plus” ^v^.

ShardingSphere-JDBC 可以和 ShardingSphere-Proxy 实现混合架构,采用注册中心统一配置分片策略。

一、数据分片
数据分片也就是所谓的分库分表。ShardingSphere 使用虚拟表与物理表建立关联,通过分片规则把数据在各分库、分表上进行分发、聚合,用户只需要对虚拟表进行操作即可。这里在已经搭建好的 MySQL 集群上做单表的分库分表操作,并向集群写入百万行数据。需要注意的是,这里使用 sharding-jdbc-spring-boot-starter:4.1.1 版本,ShardingSphere 3.x、4.x、5.x 各大版本之间的差异非常大,而且官网的文档更新不及时,文档混乱,很容易踩坑。
1. 添加依赖
还是使用 druid 作为连接池,这里使用 druid 依赖,也可以使用 druid-spring-boot-starter 作为依赖,但是在启动类上要添加 @SpringBootApplication(exclude = {DruidDataSourceAutoConfigure.class}) 排除 druid 数据源自动配置。为什么要排除?因为这里使用 ShardingSphere-JDBC 创建 druid 数据源,后面的配置中会有体现。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.12.0</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.8</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-boot-starter</artifactId> <version>4.1.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
2. 主要配置
1)sharding-sphere-datasource.yml
这个配置文件配置了数据源、连接池、分库分表规则等。
######################################################################################################################### # 数据源通用配置 druid 连接池 # 官方文档:https://github.com/alibaba/druid/wiki/%E9%A6%96%E9%A1%B5 ######################################################################################################################### default-datasource-config: &default-datasource-config # 通用配置锚链接 maxActive: 64 # 连接池最大连接数 minIdle: 32 # 连接池最小连接数 initialSize: 32 # 连接池初始连接数 maxWait: 10000 # 获取连接时最大等待时间,单位:毫秒 validationQuery: select 'x' # 用来检测连接是否有效的sql,要求是一个查询语句 ValidationQueryTimeout: 30 # 检测连接是否有效的超时时间,单位:秒 minEvictableIdleTimeMillis: 300000 # 连接保持空闲而不被驱逐的最小时间,单位:毫秒 timeBetweenEvictionRunsMillis: 60000 # 检测连接的间隔时间,单位:毫秒 type: com.alibaba.druid.pool.DruidDataSource # 连接池类型,默认为 HikariCP driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动 testWhileIdle: true # 申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 testOnBorrow: false # 申请连接时执行validationQuery检测连接是否有效 testOnReturn: false # 归还连接时执行validationQuery检测连接是否有效 useGlobalDataSourceStat: true # 合并多个DruidDataSource的监控数据 filters: wall, slf4j, stat # 配置监控统计拦截的filters,监控统计用的 Stat,日志用的 slf4j,防火墙用的 wall filter: stat: mergeSql: true # 打开 mergeSql 合并多个数据源的统计信息 slow-sql-millis: 3000 # 记录慢 SQL ######################################################################################################################### # 数据源配置 ######################################################################################################################### spring: shardingsphere: datasource: names: db00, db01, db02 # 数据源名称列表 db00: name: datasource00 # druid 数据源名称 url: jdbc:mysql://1.14.194.150:3306/shardingdb?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: 123456 <<: *default-datasource-config db01: name: datasource01 url: jdbc:mysql://1.14.30.31:3306/shardingdb?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: 123456 <<: *default-datasource-config db02: name: datasource02 url: jdbc:mysql://1.117.105.119:3306/shardingdb?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: 123456 <<: *default-datasource-config ######################################################################################################################### # 分片策略配置,这里只配置单表分库分表,使用行表达式分片策略,官方文档有很多配置策略,但是文档很乱,容易踩坑 # 官方文档地址:https://shardingsphere.apache.org/document/4.1.1/cn/manual/sharding-jdbc/configuration/config-yaml/ # 关于 SNOWFLAKE 算法参数的文档: # max.tolerate.time.difference.milliseconds:https://blog.csdn.net/g6u8w7p06dco99fq3/article/details/109610311 # max.vibration.offset 和 work-id:https://blog.csdn.net/zhutao_java/article/details/107320356 # work id 生成算法:https://blog.csdn.net/m0_37208669/article/details/104704418 ######################################################################################################################### sharding: default-data-source-name: db01 # 默认数据源 default-database-strategy: # 默认分库策略 inline: # 行表达式分片策略 sharding-column: id # 分片列名称 algorithm-expression: db0$->{(id % 3)} # 分片算法行表达式,需符合groovy语法 tables: # 表分片策略 user: # 逻辑表名称 actual-data-nodes: db0$->{0..2}.user_$->{0..1} # 数据节点规则 table-strategy: # 分表策略 inline: # 行表达式分片策略 sharding-column: id # 分片列名称 algorithm-expression: user_$->{(id % 2)} # 分片算法行表达式,需符合groovy语法 keyGenerator: # 主键生成策略 column: id # 主键列 type: SNOWFLAKE # 生成策略 UUID SNOWFLAKE props: # 可选配置 worker-id: ${workerId} # SNOWFLAKE 策略参数:机器标识ID,取值范围[0,1024) max.tolerate.time.difference.milliseconds: 5 # 最大容忍回拨时间 max.vibration.offset: 1 # 低并发下解决 ID 奇偶分片的问题 props: sql: show: false # 是否开启SQL显示
另外,对于 SNOWFLAKE 分片策略的参数 work-id 可以自定义通过算法计算分布式系统中的唯一机器 ID。可以参考雪花算法ID
@Component public class SystemConfig { static { /* 这个值可以基于 Redis 计算出分布式下的ID,这里是写死的 */ System.setProperty("workerId", String.valueOf(256)); } }
2)jpa.yml
这个配置文件配置了 JPA 相关参数。
spring: jpa: hibernate: ddl-auto: update properties: hibernate: event: merge.entity_copy_observer: allow database: mysql generate-ddl: true # show_sql: true # 打印SQL # format_sql: true # use_sql_comments: true dialet: MySQL5InnoDBDialect globally_quoted_identifiers: false
3)druid 监控配置
这里配置了 druid 的数据源监控,基本监控功能都可以实现。
package com.sxdear.sharding.sphere.partition.config; import com.alibaba.druid.filter.Filter; import com.alibaba.druid.filter.stat.StatFilter; import com.alibaba.druid.support.http.StatViewServlet; import com.alibaba.druid.support.http.WebStatFilter; import com.alibaba.druid.support.spring.stat.DruidStatInterceptor; import com.sxdear.sharding.sphere.partition.ShardingspherePartitionApplication; import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.aop.support.JdkRegexpMethodPointcut; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; import javax.servlet.Servlet; /** <p style="color:rgb(0,255,0);">TODO</p> **/ @Configuration public class DruidConfiguration extends WebStatFilter{ @Value("${default-datasource-config.filter.stat.slow-sql-millis}") private Long slowSqlMillis; @Bean public ServletRegistrationBean<Servlet> DruidStatViewServlet(){ ServletRegistrationBean<Servlet> servletRegistrationBean = new ServletRegistrationBean<>(new StatViewServlet(),"/druid/*"); servletRegistrationBean.addInitParameter("allow","127.0.0.1"); servletRegistrationBean.addInitParameter("deny",""); servletRegistrationBean.addInitParameter("loginUsername","admin"); servletRegistrationBean.addInitParameter("loginPassword","876v543xcvbwxecrvt67n8m9"); servletRegistrationBean.addInitParameter("resetEnable","false"); return servletRegistrationBean; } @Bean public FilterRegistrationBean<javax.servlet.Filter> druidStatFilter(){ FilterRegistrationBean<javax.servlet.Filter> filterRegistrationBean = new FilterRegistrationBean<>(new WebStatFilter()); filterRegistrationBean.addUrlPatterns("/*"); filterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); return filterRegistrationBean; } @Bean public Filter statFilter(){ StatFilter filter = new StatFilter(); filter.setSlowSqlMillis(slowSqlMillis); filter.setLogSlowSql(true); filter.setMergeSql(true); return filter; } @Bean public DruidStatInterceptor druidStatInterceptor() { return new DruidStatInterceptor(); } @Bean @Scope("prototype") public JdkRegexpMethodPointcut druidStatPointcut() { JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut(); pointcut.setPattern(ShardingspherePartitionApplication.class.getPackage().getName() + ".*"); return pointcut; } @Bean public DefaultPointcutAdvisor druidStatAdvisor(DruidStatInterceptor druidStatInterceptor, JdkRegexpMethodPointcut druidStatPointcut) { DefaultPointcutAdvisor defaultPointAdvisor = new DefaultPointcutAdvisor(); defaultPointAdvisor.setPointcut(druidStatPointcut); defaultPointAdvisor.setAdvice(druidStatInterceptor); return defaultPointAdvisor; } }
3. 实体类
这里使用单表(User 表)来模拟分库分表的场景,需要注意的是,在配置中已经配置了主键 ID 的策略,那么在把实体写入数据库时,主键的类型和生成策略需要相应调整。
1)User.java
package com.sxdear.sharding.sphere.partition.pojo; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; import org.hibernate.annotations.GenericGenerator; import javax.persistence.*; /** <p style="color:rgb(0,255,0);">TODO</p> **/ @Getter @Setter @ToString @Entity @NoArgsConstructor @Table(name = "user", indexes = {@Index(name = "name_index", columnList = "name")}) public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @Enumerated(value = EnumType.STRING) private Gender gender; private String address; private Integer age; private String hobby; public User (String name, Gender gender, String address, Integer age, String hobby) { this.name = name; this.gender = gender; this.address = address; this.age = age; this.hobby = hobby; } }
2)Gender.java
package com.sxdear.sharding.sphere.partition.pojo; /** <p style="color:rgb(0,255,0);">TODO</p> **/ public enum Gender { male, female; }
4. 踩坑记录
使用 shardingsphere-jdbc 做分库分表还是很方便的,主要做好配置就可以了。下面记录一下踩过的坑。。。。
1)druid 连接池配置无效
一开始是按照 druid 官方文档配置的一个单独的 application-druid.yml 配置文件,然后添加到 spring 主配置文件中,其中文件格式是 “spring.datasource.druid.*” 发现并没有用。看 shardingsphere 文档后需要把连接池的配置信息直接配置在数据源 datasource 中,作为 datasource 的扩展属性像 url/username/password 一样。可是这样一来多个数据源就会有很多重复配置,官方文档并没有提出如何解决。。。。
2)主键生成策略无效
这个就更坑爹了,跟着官方文档配置 keyGenerator 主键生成策略节点死活不起作用,用 JPA 在写入数据时报错 “ids for this class must be manually assigned before calling save()” 就是说在调用save()之前,必须手动分配此类的ID 。出现这个问题就是 keyGenerator 配置没起作用,在调用 save() 时没有生成主键 ID。
这个问题也有两个解决办法,一个就是手动配置主键 ID,自定义一个主键生成策略。
@Id @GeneratedValue(generator = "entityIdGenerator") @GenericGenerator(name = "entityIdGenerator", strategy = "com.sxdear.sharding.sphere.partition.util.SnowflakeIdUtil") private Long id;
还有一个就是根据上面的 sharding-sphere-datasource.yml 配置文件正确配置 keyGenerator,然后在主键添加相关注解。
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
需要注意的是,如果使用 @GeneratedValue(strategy = GenerationType.AUTO) 也可以,只要 keyGenerator 配置正确了就行,但是会在数据库自动生成 hibernate_sequence 表,这就很恶心了。原因就是 MySQL 本身不支持 sequence 序列。
二、读写分离
这里主需要修改 sharding-sphere-datasource.yml 配置文件就行了,还有就是 MySQL 集群已经做好了主从同步配置。
######################################################################################################################### # 数据源通用配置 druid 连接池 # 官方文档:https://github.com/alibaba/druid/wiki/%E9%A6%96%E9%A1%B5 ######################################################################################################################### default-datasource-config: &default-datasource-config # 通用配置锚链接 maxActive: 64 # 连接池最大连接数 minIdle: 2 # 连接池最小连接数 initialSize: 2 # 连接池初始连接数 maxWait: 10000 # 获取连接时最大等待时间,单位:毫秒 validationQuery: select 'x' # 用来检测连接是否有效的sql,要求是一个查询语句 ValidationQueryTimeout: 30 # 检测连接是否有效的超时时间,单位:秒 minEvictableIdleTimeMillis: 300000 # 连接保持空闲而不被驱逐的最小时间,单位:毫秒 timeBetweenEvictionRunsMillis: 60000 # 检测连接的间隔时间,单位:毫秒 type: com.alibaba.druid.pool.DruidDataSource # 连接池类型,默认为 HikariCP driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动 testWhileIdle: true # 申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 testOnBorrow: false # 申请连接时执行validationQuery检测连接是否有效 testOnReturn: false # 归还连接时执行validationQuery检测连接是否有效 useGlobalDataSourceStat: true # 合并多个DruidDataSource的监控数据 filters: wall, slf4j, stat # 配置监控统计拦截的filters,监控统计用的 Stat,日志用的 slf4j,防火墙用的 wall filter: stat: mergeSql: true # 打开 mergeSql 合并多个数据源的统计信息 slow-sql-millis: 3000 # 记录慢 SQL username: root # 用户名 password: 123456 # 密码 ######################################################################################################################### # 数据源配置 ######################################################################################################################### spring: shardingsphere: datasource: names: master, slave01, slave02, slave03 # 数据源名称列表 ################## 读写分离节点4306 master: name: master url: jdbc:mysql://1.14.194.150:3306/master?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=utf-8&useSSL=false <<: *default-datasource-config slave01: name: slave01 url: jdbc:mysql://1.14.194.150:4306/slave01?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=utf-8&useSSL=false <<: *default-datasource-config slave02: name: slave02 url: jdbc:mysql://1.14.30.31:4306/slave02?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=utf-8&useSSL=false <<: *default-datasource-config slave03: name: slave3 url: jdbc:mysql://1.117.105.119:4306/slave3?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=utf-8&useSSL=false <<: *default-datasource-config ######################################################################################################################### # 读写分离策略 ######################################################################################################################### masterslave: name: ds_ms masterDataSourceName: master slaveDataSourceNames: - slave01 - slave02 - slave03 props: sql: show: false # 是否开启SQL显示
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· DeepSeek “源神”启动!「GitHub 热点速览」
· 上周热点回顾(2.17-2.23)