首页 文章 精选 留言 我的

精选列表

搜索[快速入门],共10000篇文章
优秀的个人博客,低调大师

迅雷快速下载 Android SDK相关

用AVD Manager升级SDK时,会在线升级,会很慢很慢。 当你看到temp目录下生成了个小文件时,记下文件名,然后在前面加上https://dl-ssl.google.com/android/repository/ ,然后去用迅雷下载,下载完放到 C:\Program Files (x86)\Android\android-sdk-windows\temp 启动AVD Manager 选择安装…… 如:https://dl-ssl.google.com/android/repository/docs-3.0_r01-linux.zip https://dl-ssl.google.com/android/repository/android-3.0_r01-linux.zip 本文转自曾祥展博客园博客,原文链接:http://www.cnblogs.com/zengxiangzhan/archive/2011/03/07/1973188.html ,如需转载请自行联系原作者

优秀的个人博客,低调大师

Android github 快速实现多人协作

前言:最近要做github多人协作,也就是多人开发。搜索了一些资料,千篇一律,而且操作麻烦。今天就整理一下,github多人协作的简单实现方法。 下面的教程不会出现:公钥、组织、team、pull request 1、首先小张在github上创建一个仓库,比如叫做:GlideDemo 2、 小张开始邀请小王 创建仓库后,然后开始添加 小王了。 注意在第三步的时候,要输入小王的github用户名。 3、小王接收小张的邀请 小王在github登录自己的账户,登录完成后,将在屏幕的右上角看到一个铃铛,双击铃铛。 小王在完成接受邀请后,就可以看到小张的项目仓库了。并且可以push代码到仓库。 4、小张查看所有的协作伙伴。

优秀的个人博客,低调大师

容器服务安全组快速指南

容器服务安全组规则 2月28号之后创建容器服务集群,默认创建的安全组已经做了加固,开放的规则如下 VPC安全组: 经典网络安全组(公网入方向和内网入方向): 注意 443端口和80端口可以根据自己的需求选择放开或者关闭。 ICMP规则建议保留,方便排查问题。有些工具也依赖ICMP 老集群的安全组规则 2月28之前创建的集群,安全组规则开的比较大,以经典网络安全组规则为例 如果希望收紧规则,可以参考前面安全组的配置。步骤如下 在内网入方向和公网入方向添加允许ICMP规则 如果直接访问VM的80端口和443端口,或者其他端口,增加内网和公网规则,放开此端口。务必确保放开所有你需要的端口,否则会导致服务不可访问。通过SLB访问的端口不需要放开。 删除地址段0.0.0.0端口-1/-1的公网入规则和内网入规则。 下面的内容按兴趣阅读 安全配置原则 每个集群一个安全组。

优秀的个人博客,低调大师

InheritableThreadLocal从入门到放弃

背景: 一个上线了很久但是请求量很低(平均每天一两次)的历史功能突然出现空指针报错:    我们翻开代码定位到对应的报错代码:    结合堆栈和代码可以确定是由于bdIdJobMap的值为null导致往bdIdEmployeeJobMap这个map中putAll的时候空指针了。 而bdIdJobMap又取自employeeJobMapThread.get(); 那么这个employeeJobMapThread又是何物?    哦豁,employeeJobMapThread居然是个InheritableThreadLocal。 梳理一下报错代码的上下文逻辑如下: 1. 首先在当前主线程中对InheritableThreadLocal类型变量employeeJobMapThread进行赋值 2. 把耗时操作提交到线程池中异步执行,在异步任务中去获取employeeJobMapThread的值(其中线程池配置的coreSize/maxSize均为4,queue大小为3000) 3. 在主线程中执行employeeJobMapThread.remove(),在异步任务完成之后没有执行employeeJobMapThread.remove() 4. 最后在异步任务中通过employeeJobMapThread获取到的值为null导致后续操作空指针  是否和最近的上线有关? 相信大家都有这样的共识:线上出现报错,首先怀疑是否和最近的上线有关系。 我们做的第一件事情也是排查了近期的上线功能,从上线的功能点和相关代码上来看都和这次报错的代码没什么关系, 因此初步排除了这个原因。所以接下来只能进一步了解代码来排查原因了。 要搞清楚当前报错的根因,毫无疑问肯定是要翻过InheritableThreadLocal这座小山啦。  简单聊下InheritableThreadLocal: 提起ThreadLocal,大家应该相对都比较熟悉了,比如存放登录用户信息到ThreadLocal变量中,然后在接口层可以比较方便的获取登录用户,帮助开发提效。 但是对于InheritableThreadLocal,有不少同学都不太了解。 挑重点来说,InheritableThreadLocal相比ThreadLocal多一个能力:在创建子线程Thread时,子线程Thread会自动继承父线程的InheritableThreadLocal信息到子线程中,进而实现在在子线程获取父线程的InheritableThreadLocal值的目的。 举个简单的栗子对比下InheritableThreadLocal和ThreadLocal: public class InheritableThreadLocalTest { private static final ThreadLocal<String> threadLocal = new ThreadLocal<>(); private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>(); public static void main(String[] args) { testThreadLocal(); testInheritableThreadLocal(); } /** * threadLocal测试 */ public static void testThreadLocal() { // 在主线程中设置值到threadLocal threadLocal.set("我是父线程threadLocal的值"); // 创建一个新线程并启动 new Thread(() -> { // 在子线程里面无法获取到父线程设置的threadLocal,结果为null System.out.println("从子线程获取到threadLocal的值: " + threadLocal.get()); }).start(); } /** * inheritableThreadLocal测试 */ public static void testInheritableThreadLocal() { // 在主线程中设置一个值到inheritableThreadLocal inheritableThreadLocal.set("我是父线程inheritableThreadLocal的值"); // 创建一个新线程并启动 new Thread(() -> { // 在子线程里面可以自动获取到父线程设置的inheritableThreadLocal System.out.println("从子线程获取到inheritableThreadLocal的值: " + inheritableThreadLocal.get()); }).start(); } } 执行结果:    可以看到子线程中可以获取到父线程设置的inheritableThreadLocal值,但不能获取到父线程设置的threadLocal值。 为什么InheritableThreadLocal能够做到这点呢? 是因为在父线程创建子线程Thread的时候,Thread的构造器内部会自动继承父线程的InheritableThreadLocal到子线程。 Thread源码这两处地方解释了原因:    init方法内部实现:     通过这个简单的介绍可以帮助对于InheritableThreadLocal不了解的同学有一个初步的了解,本文不是专门介绍InheritableThreadLocal的深入原理,所以就不展开聊了,大家感兴趣可以自己进一步探索。 验证InheritableThreadLocal+线程池: 前面介绍了InheritableThreadLocal可以自动把父线程的InheritableThreadLocal信息继承到子线程Thread中。 但是在业务项目中真正需要用到子线程的时候正经人谁自己new Thread,咱可是用线程池的。 当然了,就像文章开头说明的,这次报错的代码里面也用线程池来执行异步任务。 那么InheritableThreadLocal+线程池的组合会摩擦出什么样的火花呢? 我把这次报错的代码精简之后得到下面的示例(实际代码中往InheritableThreadLocal赋的值类型不是字符串,后面会提到): package com.dada.bd.data.service; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class InheritableThreadLocalWithThreadPoolTest { private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>(); private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 2, 2, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(3000), new ThreadPoolExecutor.CallerRunsPolicy() ); public static void main(String[] args) { testInheritableThreadLocalWithThreadPool(); threadPoolExecutor.shutdown(); } /** * inheritableThreadLocal+线程池测试 */ public static void testInheritableThreadLocalWithThreadPool() { // 1. 在主线程中设置一个值到inheritableThreadLocal inheritableThreadLocal.set("我是父线程inheritableThreadLocal的值"); // 2. 提交异步任务到线程池 threadPoolExecutor.execute(() -> { // 3. 在线程池-子线程里面可以获取到父线程设置的inheritableThreadLocal吗? System.out.println("从线程池-子线程获取到inheritableThreadLocal的值: " + inheritableThreadLocal.get()); }); // 4. 清除inheritableThreadLocal inheritableThreadLocal.remove(); } }    执行结果如图所示,可以看到在线程池里面也可以获取到父线程设置的inheritableThreadLocal值。 接下来我们来分析下InheritableThreadLocal+线程池的执行过程:   也就说只有在以下这两个场景下才会继承父线程的InheritableThreadLocal: 1.线程池当前线程数 < 核心线程数 2.线程池当前线程数 >= 核心线程数 && 队列已满 && 线程数 < 最大线程数(本次线上报错的代码使用的线程池设置的coreSize和maxSize一致,所以走不到该场景) 其他情况都是在复用线程池现有的Thread,自然也就不会继承父线程的InheritableThreadLocal。 我们提交多个异步任务到线程池来验证下: package com.dada.bd.data.service; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class InheritableThreadLocalWithThreadPoolTest { private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>(); private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 2, 2, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(3000), new ThreadPoolExecutor.CallerRunsPolicy() ); public static void main(String[] args) { testInheritableThreadLocalWithThreadPool("张三"); testInheritableThreadLocalWithThreadPool("李四"); testInheritableThreadLocalWithThreadPool("王五"); testInheritableThreadLocalWithThreadPool("赵六"); testInheritableThreadLocalWithThreadPool("孙七"); threadPoolExecutor.shutdown(); } /** * inheritableThreadLocal+线程池测试 */ public static void testInheritableThreadLocalWithThreadPool(String param) { // 1. 在主线程中设置一个值到inheritableThreadLocal inheritableThreadLocal.set(param); // 2. 提交异步任务到线程池 threadPoolExecutor.execute(() -> { // 3. 在线程池-子线程里面可以获取到父线程设置的inheritableThreadLocal吗? System.out.println("线程名: " + Thread.currentThread().getName() + ", 父线程设置的inheritableThreadLocal值: " + param + ", 子线程获取到inheritableThreadLocal的值: " + inheritableThreadLocal.get()); }); // 4. 清除inheritableThreadLocal inheritableThreadLocal.remove(); } }   执行结果用表格形式展示如下: 步骤 使用线程池 结果解释说明 提交第一个异步任务值为【张三】 创建Thread:pool-1-thread-1 自动继承父线程的InheritableThreadLocal值【张三】到pool-1-thread-1 提交第二个异步任务值为【李四】 创建Thread:pool-1-thread-2 自动继承父线程的InheritableThreadLocal值【李四】到pool-1-thread-2 提交第三个异步任务值为【王五】 复用Thread:pool-1-thread-1 没有自动继承父线程的InheritableThreadLocal值【王五】,所以拿到了第一个任务提交时Thread继承下来的值【张三】 提交第四个异步任务值为【赵六】 复用Thread:pool-1-thread-2 没有自动继承父线程的InheritableThreadLocal值【赵六】,所以拿到了第一个任务提交时Thread继承下来的值【李四】 可以看到InheritableThreadLocal+线程池的组合,会面临InheritableThreadLocal污染的问题,即异步任务可能取到其他父线程设置的InheritableThreadLocal值。 有同学会提到我们不是在代码里面加了inheritableThreadLocal.remove()来清除inheritableThreadLocal的吗?为什么没有清除掉呢? 这是因为此时我们清除的只是父线程的inheritableThreadLocal,而没有清除子线程的inheritableThreadLocal的缘故。  为什么InheritableThreadLocal污染对线上没有产生影响? 既然InheritableThreadLocal+线程池的组合,会存在InheritableThreadLocal污染的问题,那岂不是线上报错的这段代码也存在这个问题? 再次检查代码,确认历史代码的确存在这个问题, 但是,这个代码上线2年多为啥一直稳定运行且没有用户反馈过功能有问题?只有最近突然出现报错?一时之间脑袋懵懵的。    仔细检代码之后发现: 这段代码在父子线程之间通过InheritableThreadLocal类型变量employeeJobMapThread传递的值是【全量的<人员Id, 该人员基本信息>结构的map】,可以近乎看做是一个不变的常量,所以虽然异步任务会拿到污染的数据,也是正常可以用的,没有产生业务影响。 这种感觉怎么说呢,只能说感叹前人的智慧,把几乎不可能做到了可能~ 好了,到这里我们解释了为什么这段代码上线这么久一直没问题,因为代码确实有InheritableThreadLocal污染问题,但被污染了也不影响使用。。。所以从最终结果来看确实可以正常运行。     什么原因导致子线程获取到的InheritableThreadLocal值是null? 但是。。。说了这么多,还是不能解释为什么线上代码获取到的inheritableThreadLocal值会是null。 1.难道父线程设置的inheritableThreadLocal值可能会是null? 检查代码发现父线程设置的inheritableThreadLocal不可能为null,顶多会是空集合:     2.难道是线程池创建之后通过prestartAllCoreThreads初始化了核心线程,在执行异步任务的时候,都是复用的已有线程导致的? 检查了对应线程池的初始化代码,发现并没有初始化核心线程,也排除了这个可能。而且如果真的是该原因22年上线之后功能一定是有问题的,前面说过,该功能上线之后没人反馈过异常,所以也可以排除该原因。  • 该功能22年上线之间2年多一直没人反馈,大概率该功能之前很长时间是正常的,近期由于某个原因导致功能异常 • 虽然历史代码的用法存在子任务中获取到的InheritableThreadLocal被污染的问题,但是被污染的值也能用,不影响正确性 • 只要线程池中的线程初始化的时候继承了正确的InheritableThreadLocal值,后续就不会被清除掉,也就可以正常运行功能 从这些已知的信息来推断,可以推断出这段历史代码写法虽然有隐患,但是不是引发当前空指针的的原因。 3.剩下的只有一种可能:存在线程池的共用。 在执行这个报错的异步任务的时候,复用了某个已有的线程A,并且当时创建该线程A的时候,没有继承InheritableThreadLocal,进而导致后面复用该线程的时候,从InheritableThreadLocal获取到的值为null。 而只要是通过这段历史代码创建的线程一定是没问题的,所以一定是存在其他业务共用了这个线程池,并且这个业务优先执行进而初始化了线程池的线程,导致线程池的线程没有继承InheritableThreadLocal。 如下代码示例: package com.dada.bd.data.service; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class InheritableThreadLocalWithThreadPoolTest { private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>(); // 这里线程池core/max数量都只有2 private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 2, 2, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(3000), new ThreadPoolExecutor.CallerRunsPolicy() ); public static void main(String[] args) { // 先执行了不涉及InheritableThreadLocal的子任务初始化线程池线程 testAnotherFunction(); testAnotherFunction(); // 后执行了本次的历史代码,其涉及InheritableThreadLocal testInheritableThreadLocalWithThreadPool("张三"); testInheritableThreadLocalWithThreadPool("李四"); threadPoolExecutor.shutdown(); } /** * inheritableThreadLocal+线程池测试 */ public static void testInheritableThreadLocalWithThreadPool(String param) { // 1. 在主线程中设置一个值到inheritableThreadLocal inheritableThreadLocal.set(param); // 2. 提交异步任务到线程池 threadPoolExecutor.execute(() -> { // 3. 在线程池-子线程里面可以获取到父线程设置的inheritableThreadLocal吗? System.out.println("线程名: " + Thread.currentThread().getName() + ", 父线程设置的inheritableThreadLocal值: " + param + ", 子线程获取到inheritableThreadLocal的值: " + inheritableThreadLocal.get()); }); // 4. 清除inheritableThreadLocal inheritableThreadLocal.remove(); } /** * 模拟另一个独立的功能 */ public static void testAnotherFunction() { // 提交异步任务到线程池 threadPoolExecutor.execute(() -> { // 在线程池-子线程里面可以获取到父线程设置的inheritableThreadLocal吗? System.out.println("线程名: " + Thread.currentThread().getName() + ", 线程池-子线程摸个鱼"); }); } } 执行结果:    在项目里面搜一下看看,果真如此,有2出地方在用这个线程池,并且另外的一处代码中提交的异步任务不涉及inheritableThreadLocal:    示意图如下: 逻辑执行顺序为:创建线程池 - 执行功能A - 执行功能A - 执行功能B 其中: 功能A全流程均不涉及InheritableThreadLocal 功能B对应报错的代码,主线程设置InheritableThreadLocal并且在子线程使用    至此,线上报错的根因确定了:就是因为InheritableThreadLocal + 线程池共用导致。  扩展一下: 假如执行顺序是这样呢:创建线程池 - 执行功能B - 执行功能B - 执行功能A    结局居然是一切安好。  假如执行顺序是这样呢:创建线程池 - 执行功能B - 执行功能A - 执行功能B    发现了吗?如果应用启动之后功能B先执行并且初始化了线程池所有核心线程,那么一切正常,否则就可能报错。 也就是说功能B是否正常还看运气的,运气好就正常执行,运气不好就报空指针的错。  这你敢信?    小插曲: 这个问题的排查当中还遇到了2个小插曲: 插曲1: 最初怀疑是线程池复用导致的,但是在IDEA里面搜代码的时候粗心大意没有看到其他地方在复用线程池。 因此期间一度自我怀疑见鬼了,导致本来可以一两个小时确定根因的,结果饶了弯路多花了两个小时才确定根因。 所以说排查问题的时候每一步都要保持细心,得出的每一个结论都应该是证据确凿,理由充分, 否则会让自己兜兜转转浪费宝贵的时间。  插曲2: 排查代码的时候发现异步任务代码没有做任何的异常处理,这其实是很坑的。 有经验的同学应该知道,线程池里面提交异步任务如果没有做异常处理,出现异常的话不会有任何的日志信息。 本地运行的时候会打印到控制台,但是线上控制台的信息可不会记录到日志里面。 所以经常遇到异步任务执行结果不符合预期,但是线上没有任何相关日志就是这个原因。 我们这里有日志是因为使用的线程池是二次封装过的,里面对异步任务做了兜底的异常记录。  总结: 前面分析到了导致空指针的原因是线程池共用导致的老代码报错,而共用这个线程池的代码是新上线的功能引入的。 这就打脸了开头我们检查了上线的功能与此无关,实则有关。 只是我们评估复用线程池的影响时,很难想到会有这样的影响,通常我们会考虑: 1. 是否会影响共用该线程池的老功能响应时间边长 2. 是否存在父子任务共用线程池导致可能产生死锁 针对InheritableThreadLocal,我个人的建议是: 1. InheritableThreadLocal(其实ThreadLocal也一样)不适合应用于业务代码中,因为他们都是隐式的参数传递,而业务系统中好维护的代码应当是显式的参数传递(我们这个线上问题就采用该方式) 2. 框架类代码才是InheritableThreadLocal和ThreadLocal主要发光发热的地方,因为对应的研发水平通常较高,且代码经过严格测试验证,并且较少变动。而业务系统研发水平参差不齐,且经常会发生同步操作变异步等 3. 虽然InheritableThreadLocal不建议在业务代码中使用,但是我们还是需要掌握它,不为别的,只有掌握它的优缺点才能告诉自己和他人为什么应该在业务代码中放弃使用它  针对如何有效的应对业务研发遇到的一些“疑难杂症”,我的建议是: 1. 大胆提出合理的假设,小心谨慎进行验证 2. 没有充足理由,不要轻易下结论 3. 没有头绪时,休息一下,或找合适的人一起探讨,给自己打开新的思路  最后,愿天下没有故障,没有线上问题,没有bug。

优秀的个人博客,低调大师

OpenAI 函数调用 功能入门

作者:AI小火箭的HB 我是AI小火箭的HB,我探索和写作人工智能和语言交叉点的所有事物,范围从LLM,聊天机器人,语音机器人,开发框架,以数据为中心的潜在空间等。 范例 初步体验 OpenAI新增了“函数调用”功能,这是什么呢? 我们先调用API来体验下。 下面是发送到模型的 JSON 文档。此调用的目的是生成一个 JSON 文件,该文件可用于发送到发送电子邮件的 API。 您可以看到函数名称为 send_email,并定义了三个参数, to_address , subject 和 body ,即电子邮件正文。 用户请求为: Send Cobus from humanfirst ai an email asking for the monthly report? curl --location 'https://api.openai.com/v1/chat/completions' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer sk-xxxx' \ --data '{ "model": "gpt-3.5-turbo-0613", "messages": [ {"role": "user", "content": "Send Cobus from humanfirst ai an email asking for the monthly report?"} ], "functions": [ { "name": "send_email", "description": "Please send an email.", "parameters": { "type": "object", "properties": { "to_address": { "type": "string", "description": "To address for email" }, "subject": { "type": "string", "description": "subject of the email" }, "body": { "type": "string", "description": "Body of the email" } } } } ] }' 下面是返回的 JSON { "id": "chatcmpl-7TQuwzJpQAY470saQM2RPfxwF6DDE", "object": "chat.completion", "created": 1687249338, "model": "gpt-3.5-turbo-0613", "choices": [ { "index": 0, "message": { "role": "assistant", "content": null, "function_call": { "name": "send_email", "arguments": "{\n \"to_address\": \"cobus@humanfirst.ai\",\n \"subject\": \"Request for Monthly Report\",\n \"body\": \"Hi Cobus,\\n\\nI hope you're doing well. Could you please share the monthly report with me? It would be great to have it before the end of the week.\\n\\nThanks,\\n[Your Name]\"\n}" } }, "finish_reason": "function_call" } ], "usage": { "prompt_tokens": 86, "completion_tokens": 82, "total_tokens": 168 } } GPT模型会返回需要调用的函数名 send_email和对应的参数(放在arguments字段)。 { "to_address": "cobus@humanfirst.ai", "subject": "Request for Monthly Report", "body": "Hi Cobus,\n\nI hope you're doing well. Could you please share the monthly report with me? It would be great to have it before the end of the week.\n\nThanks,\n[Your Name]" } 这就非常有用,第三方的应用可以提供多个函数/服务(类似插件),GPT模型可以根据用户的指令自动选择不同的函数/服务。 现在再来看示例,就比较清晰了。 用途 根据官网文档,函数调用允许您更可靠地从模型中获取结构化数据。例如,您可以: 创建聊天机器人,通过调用外部 API 来回答问题(例如 ChatGPT 插件) 例如,定义像 send_email(to: string, body: string) 或 get_current_weather(location: string, unit: 'celsius' | 'fahrenheit') 这样的函数 将自然语言转换为 API 调用 例如,将“谁是我的顶级客户?”转换为 get_customers(min_revenue: int, created_before: string, limit: int) 并调用您的内部 API 从文本中提取结构化数据 例如,定义一个名为 extract_data(name: string, birthday: string) 或 sql_query(query: string) 的函数 函数调用的基本步骤顺序如下: 使用用户查询和函数参数中定义的一组函数调用模型。 模型可以选择调用函数;如果是这样,内容将是符合自定义架构的字符串化 JSON 对象(注意:模型可能会生成无效的 JSON 或幻觉参数)。 在代码中将字符串解析为 JSON,并使用提供的参数调用函数(如果存在)。 通过将函数响应追加为新消息来再次调用模型,并让模型将结果汇总回给用户。 AI小火箭 AI小火箭已经支持函数调用和gpt-3.5-turbo-16k、gpt-3.5-turbo-0613、gpt-3.5-turbo-16k-0613,大家可以去体验下。

优秀的个人博客,低调大师

Vue3开发入门

### 1、基础环境相关版本信息 ``` C:\Users\xxxxx>node -v v14.17.0 C:\Users\xxxxx>npm -v 7.18.1 ``` ### 2、新建vue3项目 ``` # a.切换到项目代码目录 cd /d D:\Development\Vue # b.新建项目 D:\Development\Vue>npm init @vitejs/app Need to install the following packages: @vitejs/create-app Ok to proceed? (y) √ Project name: ... study-admin √ Select a framework: » vue √ Select a variant: » vue-ts Scaffolding project in D:\Development\Vue\study-admin... Done. Now run: cd study-admin npm install npm run dev # c.安装依赖,运行项目,运行时会有一个报错,解决办法是手动执行node脚本安装spawn D:\Development\Vue>cd study-admin D:\Development\Vue\study-admin>npm install added 91 packages in 4s D:\Development\Vue\study-admin>npm run dev > dev > vite events.js:353 throw er; // Unhandled 'error' event ^ Error: spawn D:\Development\Vue\study-admin\node_modules\esbuild\esbuild.exe ENOENT at Process.ChildProcess._handle.onexit (internal/child_process.js:269:19) at onErrorNT (internal/child_process.js:467:16) at processTicksAndRejections (internal/process/task_queues.js:82:21) Emitted 'error' event on ChildProcess instance at: at Process.ChildProcess._handle.onexit (internal/child_process.js:275:12) at onErrorNT (internal/child_process.js:467:16) at processTicksAndRejections (internal/process/task_queues.js:82:21) { errno: -4058, code: 'ENOENT', syscall: 'spawn D:\\Development\\Vue\\study-admin\\node_modules\\esbuild\\esbuild.exe', path: 'D:\\Development\\Vue\\study-admin\\node_modules\\esbuild\\esbuild.exe', spawnargs: [ '--service=0.12.9', '--ping' ] } # 解决errno: -4058,code: 'ENOENT',spawn 相关报错 D:\Development\Vue\study-admin>node D:\Development\Vue\study-admin\node_modules\esbuild\install.js D:\Development\Vue\study-admin>npm run dev > dev > vite Pre-bundling dependencies: vue (this will be run only when your dependencies or config have changed) vite v2.3.7 dev server running at: > Local: http://localhost:3000/ > Network: use `--host` to expose ready in 693ms. ``` ### 3、修改vite启动参数相关配置,vite.config.ts > 参考文档:https://cn.vitejs.dev/config/ ``` import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // 需要安装 @types/node -> npm i @types/node -D import { resolve } from 'path' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': resolve(__dirname, 'src') // 设置 `@` 指向 `src` 目录 } }, server: { port: 4000, // 服务端口号 open: true, // 启动时自动打开浏览器 cors: true // 允许跨域访问 } }) ``` ### 4、集成vue-router路由模块 > 参考文档:https://next.router.vuejs.org/zh/introduction.html ``` # a.安装vue-router v4.x,只有v4版本支持vue3 D:\Development\Vue\study-admin>npm i vue-router@4 added 2 packages in 2s # b.创建 src/router/index.ts 文件 import { createRouter, createWebHashHistory, Router, RouteRecordRaw } from 'vue-router' import Login from '@/views/Login.vue' const routes: Array = [ { path: '/', name: 'Login', component: Login }, { path: '/admin', name: 'Admin', component: () => import('@/views/Admin.vue') // 懒加载 Admin 组件 } ] const router: Router = createRouter({ history: createWebHashHistory(), routes: routes }) export default router # c.在 src 下创建 views 目录,用来存储页面组件。在 views 目录下创建 Login.vue 、Admin.vue。 # Login.vue Login 登陆 # Admin.vue Admin # d.在 main.ts 文件中加载路由配置 import { createApp } from 'vue' import App from '@/App.vue' import router from './router/index' createApp(App).use(router).mount('#app') # e.修改App.vue,添加路由映射 APP Module ``` ### 5、集成UI框架Element Plus ``` # a.安装element-plus和vite-plugin-style-import npm i element-plus npm i vite-plugin-style-import -D # b.修改vite.config.ts配置按需加载 import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // 需要安装@types/node -> npm i @types/node -D import { resolve } from 'path' // 需要安装vite-plugin-style-import -> npm i vite-plugin-style-import -D import styleImport from 'vite-plugin-style-import'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [ vue(), // 按需载入 Element Plus styleImport({ libs: [ { libraryName: 'element-plus', esModule: true, ensureStyleFile: true, resolveStyle: (name) => { return `element-plus/lib/theme-chalk/${name}.css` }, resolveComponent: (name) => { return `element-plus/lib/${name}` } } ] }) ], resolve: { alias: { '@': resolve(__dirname, 'src') // 设置 `@` 指向 `src` 目录 } }, server: { port: 4000, // 设置服务启动端口号 open: true, // 设置服务启动时是否自动打开浏览器 cors: true // 允许跨域 } }) # c.在main.ts中加载element-plus import { createApp } from 'vue' import App from '@/App.vue' import router from './router/index' // 按需引入需要的element-plus组件 import { ElIcon, ElLoading, ElCard, ElButton } from 'element-plus' // 创建Vue3实例 const app = createApp(App) // 加载默认路由 app.use(router) // 加载element-plus组件 app.use(ElButton).use(ElCard).use(ElLoading).use(ElIcon) // 挂载到DOM app.mount('#app') # d.修改Login.vue将登陆链接改为element-plus的el-button组件,验证UI组件是否正确引入 Login 登陆 --> 登陆 ```

优秀的个人博客,低调大师

ShardingSphere-JDBC入门实战

前言 Apache ShardingSphere 是一套开源的分布式数据库解决方案组成的生态圈,它由 JDBC、Proxy 和 Sidecar(规划中)这 3 款既能够独立部署,又支持混合部署配合使用的产品组成;接下来的几篇文章将重点分析ShardingSphere-JDBC,从数据分片,分布式主键,分布式事务,读写分离,弹性伸缩等几个方面来介绍。 简介 ShardingSphere-JDBC定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架。整体架构图如下(来自官网): ShardingSphere-JDBC包含了众多的功能模块包括数据分片,分布式主键,分布式事务,读写分离,弹性伸缩等等;作为一个数据库中间件最核心的功能当属数据分片了,ShardingSphere-JDBC提供了很多分库分表的策略和算法,接下来看看具体是如何使用这些策略的; 数据分片 作为一个开发者我们希望中间件可以帮我们屏蔽底层的细节,让我们在面对分库分表的场景下,可以像使用单库单表一样简单;当然ShardingSphere-JDBC不会让大家失望,引入了分片数据源、逻辑表等概念; 分片数据源和逻辑表 逻辑表:逻辑表是相对物理表来说的,通常做分表处理,某一张表会被分成多张表,比如订单表被拆分成10张表,分别是t_order_0到t_order_9,而对应的逻辑表就是t_order,对于开发者来说只需要使用逻辑表即可; 分片数据源:对于分库来说,通常会有多个库,或者说是多个数据源,所以这些数据源需要被统一管理起来,引入了分片数据源的概念,常见的ShardingDataSource 有了以上两个最基本的概念当然还不够,还需要分库分表策略算法帮助我们做路由处理;但是这两个概念可以让开发者有一种使用单库单表的感觉,就像下面这样一个简单的实例: DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig, new Properties()); Connection conn = dataSource.getConnection(); String sql = "select id,user_id,order_id from t_order where order_id = 103"; PreparedStatement preparedStatement = conn.prepareStatement(sql); ResultSet set = preparedStatement.executeQuery(); 以上根据真实数据源列表,分库分表策略生成了一个抽象数据源,可以简单理解就是ShardingDataSource;接下来的操作和我们使用jdbc操作正常的单库单表没有任何区别; 分片策略算法 ShardingSphere-JDBC在分片策略上分别引入了分片算法、分片策略两个概念,当然在分片的过程中分片键也是一个核心的概念;在此可以简单的理解分片策略 = 分片算法 + 分片键;至于为什么要这么设计,应该是ShardingSphere-JDBC考虑更多的灵活性,把分片算法单独抽象出来,方便开发者扩展; 分片算法 提供了抽象分片算法类:ShardingAlgorithm,根据类型又分为:精确分片算法、区间分片算法、复合分片算法以及Hint分片算法; 精确分片算法:对应PreciseShardingAlgorithm类,主要用于处理 = 和 IN的分片; 区间分片算法:对应RangeShardingAlgorithm类,主要用于处理 BETWEEN AND, >, <, >=, <= 分片; 复合分片算法:对应ComplexKeysShardingAlgorithm类,用于处理使用多键作为分片键进行分片的场景; Hint分片算法:对应HintShardingAlgorithm类,用于处理使用 Hint 行分片的场景; 以上所有的算法类都是接口类,具体实现交给开发者自己; 分片策略 分片策略基本和上面的分片算法对应,包括:标准分片策略、复合分片策略、Hint分片策略、内联分片策略、不分片策略; 标准分片策略:对应StandardShardingStrategy类,提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法,PreciseShardingAlgorithm是必须的,RangeShardingAlgorithm可选的; public final class StandardShardingStrategy implements ShardingStrategy { private final String shardingColumn; private final PreciseShardingAlgorithm preciseShardingAlgorithm; private final RangeShardingAlgorithm rangeShardingAlgorithm; } 复合分片策略:对应ComplexShardingStrategy类,提供ComplexKeysShardingAlgorithm分片算法; public final class ComplexShardingStrategy implements ShardingStrategy { @Getter private final Collection<String> shardingColumns; private final ComplexKeysShardingAlgorithm shardingAlgorithm; } 可以发现支持多个分片键; Hint分片策略:对应HintShardingStrategy类,通过 Hint 指定分片值而非从 SQL 中提取分片值的方式进行分片的策略;提供HintShardingAlgorithm分片算法; public final class HintShardingStrategy implements ShardingStrategy { @Getter private final Collection<String> shardingColumns; private final HintShardingAlgorithm shardingAlgorithm; } 内联分片策略:对应InlineShardingStrategy类,没有提供分片算法,路由规则通过表达式来实现; 不分片策略:对应NoneShardingStrategy类,不分片策略; 分片策略配置类 在使用中我们并没有直接使用上面的分片策略类,ShardingSphere-JDBC分别提供了对应策略的配置类包括: StandardShardingStrategyConfiguration ComplexShardingStrategyConfiguration HintShardingStrategyConfiguration InlineShardingStrategyConfiguration NoneShardingStrategyConfiguration 实战 有了以上相关基础概念,接下来针对每种分片策略做一个简单的实战,在实战前首先准备好库和表; 准备 分别准备两个库:ds0、ds1;然后每个库分别包含两个表:t_order0,t_order1; CREATE TABLE `t_order0` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL, `order_id` bigint(20) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 准备真实数据源 我们这里有两个数据源,这里都使用java代码的方式来配置: // 配置真实数据源 Map<String, DataSource> dataSourceMap = new HashMap<>(); // 配置第一个数据源 BasicDataSource dataSource1 = new BasicDataSource(); dataSource1.setDriverClassName("com.mysql.jdbc.Driver"); dataSource1.setUrl("jdbc:mysql://localhost:3306/ds0"); dataSource1.setUsername("root"); dataSource1.setPassword("root"); dataSourceMap.put("ds0", dataSource1); // 配置第二个数据源 BasicDataSource dataSource2 = new BasicDataSource(); dataSource2.setDriverClassName("com.mysql.jdbc.Driver"); dataSource2.setUrl("jdbc:mysql://localhost:3306/ds1"); dataSource2.setUsername("root"); dataSource2.setPassword("root"); dataSourceMap.put("ds1", dataSource2); 这里配置的两个数据源都是普通的数据源,最后会把dataSourceMap交给ShardingDataSourceFactory管理; 表规则配置 表规则配置类TableRuleConfiguration,包含了五个要素:逻辑表、真实数据节点、数据库分片策略、数据表分片策略、分布式主键生成策略; TableRuleConfiguration orderTableRuleConfig = new TableRuleConfiguration("t_order", "ds${0..1}.t_order${0..1}"); orderTableRuleConfig.setDatabaseShardingStrategyConfig( new StandardShardingStrategyConfiguration("user_id", new MyPreciseSharding())); orderTableRuleConfig.setTableShardingStrategyConfig( new StandardShardingStrategyConfiguration("order_id", new MyPreciseSharding())); orderTableRuleConfig.setKeyGeneratorConfig(new KeyGeneratorConfiguration("SNOWFLAKE", "id")); 逻辑表:这里配置的逻辑表就是t_order,对应的物理表有t_order0,t_order1; 真实数据节点:这里使用行表达式进行配置的,简化了配置;上面的配置就相当于配置了: db0 ├── t_order0 └── t_order1 db1 ├── t_order0 └── t_order1 数据库分片策略:这里的库分片策略就是上面介绍的五种类型,这里使用的StandardShardingStrategyConfiguration,需要指定分片键和分片算法,这里使用的是精确分片算法; public class MyPreciseSharding implements PreciseShardingAlgorithm<Integer> { @Override public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Integer> shardingValue) { Integer index = shardingValue.getValue() % 2; for (String target : availableTargetNames) { if (target.endsWith(index + "")) { return target; } } return null; } } 这里的shardingValue就是user_id对应的真实值,每次和2取余;availableTargetNames可选择就是{ds0,ds1};看余数和哪个库能匹配上就表示路由到哪个库; 数据表分片策略:指定的**分片键(order_id)**和分库策略不一致,其他都一样; 分布式主键生成策略:ShardingSphere-JDBC提供了多种分布式主键生成策略,后面详细介绍,这里使用雪花算法; 配置分片规则 配置分片规则ShardingRuleConfiguration,包括多种配置规则:表规则配置、绑定表配置、广播表配置、默认数据源名称、默认数据库分片策略、默认表分片策略、默认主键生成策略、主从规则配置、加密规则配置; 表规则配置 tableRuleConfigs:也就是上面配置的库分片策略和表分片策略,也是最常用的配置; 绑定表配置 bindingTableGroups:指分⽚规则⼀致的主表和⼦表;绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将⼤⼤提升; 广播表配置 broadcastTables:所有的分⽚数据源中都存在的表,表结构和表中的数据在每个数据库中均完全⼀致。适⽤于数据量不⼤且需要与海量数据的表进⾏关联查询的场景; 默认数据源名称 defaultDataSourceName:未配置分片的表将通过默认数据源定位; 默认数据库分片策略 defaultDatabaseShardingStrategyConfig:表规则配置可以设置数据库分片策略,如果没有配置可以在这里面配置默认的; 默认表分片策略 defaultTableShardingStrategyConfig:表规则配置可以设置表分片策略,如果没有配置可以在这里面配置默认的; 默认主键生成策略 defaultKeyGeneratorConfig:表规则配置可以设置主键生成策略,如果没有配置可以在这里面配置默认的;内置UUID、SNOWFLAKE生成器; 主从规则配置 masterSlaveRuleConfigs:用来实现读写分离的,可配置一个主表多个从表,读面对多个从库可以配置负载均衡策略; 加密规则配置 encryptRuleConfig:提供了对某些敏感数据进行加密的功能,提供了⼀套完整、安全、透明化、低改造成本的数据加密整合解决⽅案; 数据插入 以上准备好,就可以操作数据库了,这里执行插入操作: DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig, new Properties()); Connection conn = dataSource.getConnection(); String sql = "insert into t_order (user_id,order_id) values (?,?)"; PreparedStatement preparedStatement = conn.prepareStatement(sql); for (int i = 1; i <= 10; i++) { preparedStatement.setInt(1, i); preparedStatement.setInt(2, 100 + i); preparedStatement.executeUpdate(); } 通过以上配置的真实数据源、分片规则以及属性文件创建分片数据源ShardingDataSource;接下来就可以像使用单库单表一样操作分库分表了,sql中可以直接使用逻辑表,分片算法会根据具体的值就行路由处理; 经过路由最终:奇数入ds1.t_order1,偶数入ds0.t_order0;以上使用了最常见的精确分片算法,下面继续看一下其他几种分片算法; 分片算法 上面的介绍的精确分片算法中,通过PreciseShardingValue来获取当前分片键值,ShardingSphere-JDBC针对每种分片算法都提供了相应的ShardingValue,具体包括: PreciseShardingValue RangeShardingValue ComplexKeysShardingValue HintShardingValue 区间分片算法 用在区间查询的时候,比如下面的查询SQL: select * from t_order where order_id>2 and order_id<9 以上两个区间值2、9会直接保存到RangeShardingValue中,这里没有指定user_id用来做库路由,所以会访问两个库; public class MyRangeSharding implements RangeShardingAlgorithm<Integer> { @Override public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Integer> shardingValue) { Collection<String> result = new LinkedHashSet<>(); Range<Integer> range = shardingValue.getValueRange(); // 区间开始和结束值 int lower = range.lowerEndpoint(); int upper = range.upperEndpoint(); for (int i = lower; i <= upper; i++) { Integer index = i % 2; for (String target : availableTargetNames) { if (target.endsWith(index + "")) { result.add(target); } } } return result; } } 可以发现会检查区间开始和结束中的每个值和2取余,是否都能和真实的表匹配; 复合分片算法 可以同时使用多个分片键,比如可以同时使用user_id和order_id作为分片键; orderTableRuleConfig.setDatabaseShardingStrategyConfig( new ComplexShardingStrategyConfiguration("order_id,user_id", new MyComplexKeySharding())); orderTableRuleConfig.setTableShardingStrategyConfig( new ComplexShardingStrategyConfiguration("order_id,user_id", new MyComplexKeySharding())); 如上在配置分库分表策略时,指定了两个分片键,用逗号隔开;分片算法如下: public class MyComplexKeySharding implements ComplexKeysShardingAlgorithm<Integer> { @Override public Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<Integer> shardingValue) { Map<String, Collection<Integer>> map = shardingValue.getColumnNameAndShardingValuesMap(); Collection<Integer> userMap = map.get("user_id"); Collection<Integer> orderMap = map.get("order_id"); List<String> result = new ArrayList<>(); // user_id,order_id分片键进行分表 for (Integer userId : userMap) { for (Integer orderId : orderMap) { int suffix = (userId+orderId) % 2; for (String s : availableTargetNames) { if (s.endsWith(suffix+"")) { result.add(s); } } } } return result; } } Hint分片算法 在一些应用场景中,分片条件并不存在于 SQL,而存在于外部业务逻辑;可以通过编程的方式向 HintManager 中添加分片条件,该分片条件仅在当前线程内生效; // 设置库表分片策略 orderTableRuleConfig.setDatabaseShardingStrategyConfig(new HintShardingStrategyConfiguration(new MyHintSharding())); orderTableRuleConfig.setTableShardingStrategyConfig(new HintShardingStrategyConfiguration(new MyHintSharding())); // 手动设置分片条件 int hitKey1[] = { 2020, 2021, 2022, 2023, 2024 }; int hitKey2[] = { 3020, 3021, 3022, 3023, 3024 }; DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig, new Properties()); Connection conn = dataSource.getConnection(); for (int i = 1; i <= 5; i++) { final int index = i; new Thread(new Runnable() { @Override public void run() { try { HintManager hintManager = HintManager.getInstance(); String sql = "insert into t_order (user_id,order_id) values (?,?)"; PreparedStatement preparedStatement = conn.prepareStatement(sql); // 分别添加库和表分片条件 hintManager.addDatabaseShardingValue("t_order", hitKey1[index - 1]); hintManager.addTableShardingValue("t_order", hitKey2[index - 1]); preparedStatement.setInt(1, index); preparedStatement.setInt(2, 100 + index); preparedStatement.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } } }).start(); } 以上实例中,手动设置了分片条件,分片算法如下所示: public class MyHintSharding implements HintShardingAlgorithm<Integer> { @Override public Collection<String> doSharding(Collection<String> availableTargetNames, HintShardingValue<Integer> shardingValue) { List<String> shardingResult = new ArrayList<>(); for (String targetName : availableTargetNames) { String suffix = targetName.substring(targetName.length() - 1); Collection<Integer> values = shardingValue.getValues(); for (int value : values) { if (value % 2 == Integer.parseInt(suffix)) { shardingResult.add(targetName); } } } return shardingResult; } } 不分片 配置NoneShardingStrategyConfiguration即可: orderTableRuleConfig.setDatabaseShardingStrategyConfig(new NoneShardingStrategyConfiguration()); orderTableRuleConfig.setTableShardingStrategyConfig(new NoneShardingStrategyConfiguration()); 这样数据会插入每个库每张表,可以理解为广播表 分布式主键 面对多个数据库表需要有唯一的主键,引入了分布式主键功能,内置的主键生成器包括:UUID、SNOWFLAKE; UUID 直接使用UUID.randomUUID()生成,主键没有任何规则;对应的主键生成类:UUIDShardingKeyGenerator; SNOWFLAKE 实现类:SnowflakeShardingKeyGenerator;使⽤雪花算法⽣成的主键,⼆进制表⽰形式包含 4 部分,从⾼位到低位分表为:1bit 符号位、41bit 时间戳位、10bit ⼯作进程位以及 12bit 序列号位;来自官网的图片: 扩展 实现接口:ShardingKeyGenerator,实现自己的主键生成器; public interface ShardingKeyGenerator extends TypeBasedSPI { Comparable<?> generateKey(); } 实战 使用也很简单,直接使用KeyGeneratorConfiguration即可,配置对应的算法类型和字段名称: orderTableRuleConfig.setKeyGeneratorConfig(new KeyGeneratorConfiguration("SNOWFLAKE", "id")); 这里使用雪花算法生成器,对应生成的字段是id;结果如下: mysql> select * from t_order0; +--------------------+---------+----------+ | id | user_id | order_id | +--------------------+---------+----------+ | 589535589984894976 | 0 | 0 | | 589535590504988672 | 2 | 2 | | 589535590718898176 | 4 | 4 | +--------------------+---------+----------+ 分布式事务 ShardingSphere-JDBC使用分布式事务和使用本地事务没什么区别,提供了透明化的分布式事务;支持的事务类型包括:本地事务、XA事务和柔性事务,默认是本地事务; public enum TransactionType { LOCAL, XA, BASE } 依赖 根据具体使用XA事务还是柔性事务,需要引入不同的模块; <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-transaction-xa-core</artifactId> </dependency> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-transaction-base-seata-at</artifactId> </dependency> 实现 ShardingSphere-JDBC提供了分布式事务管理器ShardingTransactionManager,实现包括: XAShardingTransactionManager:基于 XA 的分布式事务管理器; SeataATShardingTransactionManager:基于 Seata 的分布式事务管理器; XA 的分布式事务管理器具体实现包括:Atomikos、Narayana、Bitronix;默认是Atomikos; 实战 默认的事务类型是TransactionType.LOCAL,ShardingSphere-JDBC天生面向多数据源,本地模式其实是循环提交每个数据源的事务,不能保证数据的一致性,所以需要使用分布式事务,具体使用也很简单: //改变事务类型为XA TransactionTypeHolder.set(TransactionType.XA); DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig, new Properties()); Connection conn = dataSource.getConnection(); try { //关闭自动提交 conn.setAutoCommit(false); String sql = "insert into t_order (user_id,order_id) values (?,?)"; PreparedStatement preparedStatement = conn.prepareStatement(sql); for (int i = 1; i <= 5; i++) { preparedStatement.setInt(1, i - 1); preparedStatement.setInt(2, i - 1); preparedStatement.executeUpdate(); } //事务提交 conn.commit(); } catch (Exception e) { e.printStackTrace(); //事务回滚 conn.rollback(); } 可以发现使用起来还是很简单的,ShardingSphere-JDBC会根据当前的事务类型,在提交的时候判断是走本地事务提交,还是使用分布式事务管理器ShardingTransactionManager进行提交; 读写分离 对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库和从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。 主从配置 在上面章节介绍分片规则的时候,其中有说到主从规则配置,其目的就是用来实现读写分离的,核心配置类:MasterSlaveRuleConfiguration: public final class MasterSlaveRuleConfiguration implements RuleConfiguration { private final String name; private final String masterDataSourceName; private final List<String> slaveDataSourceNames; private final LoadBalanceStrategyConfiguration loadBalanceStrategyConfiguration; } name:配置名称,当前使用的4.1.0版本,这里必须是主库的名称; masterDataSourceName:主库数据源名称; slaveDataSourceNames:从库数据源列表,可以配置一主多从; LoadBalanceStrategyConfiguration:面对多个从库,读取的时候会通过负载算法进行选择; 主从负载算法类:MasterSlaveLoadBalanceAlgorithm,实现类包括:随机和循环; ROUND_ROBIN:实现类RoundRobinMasterSlaveLoadBalanceAlgorithm RANDOM:实现类RandomMasterSlaveLoadBalanceAlgorithm 实战 分别给ds0和ds1准备从库:ds01和ds11,分别配置主从同步;读写分离配置如下: List<String> slaveDataSourceNames0 = new ArrayList<String>(); slaveDataSourceNames0.add("ds01"); MasterSlaveRuleConfiguration masterSlaveRuleConfiguration0 = new MasterSlaveRuleConfiguration("ds0", "ds0", slaveDataSourceNames0); shardingRuleConfig.getMasterSlaveRuleConfigs().add(masterSlaveRuleConfiguration0); List<String> slaveDataSourceNames1 = new ArrayList<String>(); slaveDataSourceNames1.add("ds11"); MasterSlaveRuleConfiguration masterSlaveRuleConfiguration1 = new MasterSlaveRuleConfiguration("ds1", "ds1", slaveDataSourceNames1); shardingRuleConfig.getMasterSlaveRuleConfigs().add(masterSlaveRuleConfiguration1); 这样在执行查询操作的时候会自动路由到从库,实现读写分离; 总结 本文重点介绍了ShardingSphere-JDBC的数据分片功能,这也是所有数据库中间件的核心功能;当然分布式主键、分布式事务、读写分离等功能也是必不可少的;同时ShardingSphere还引入了弹性伸缩的功能,这是一个非常亮眼的功能,因为数据库分片本身是有状态的,所以我们在项目启动之初都固定了多少库多少表,然后通过分片算法路由到各个库表,但是业务的发展往往超乎我们的预期,这时候如果想扩表扩库会很麻烦,目前看ShardingSphere官网弹性伸缩处于alpha开发阶段,非常期待此功能。 参考 https://shardingsphere.apache.org/document/current/cn/overview/ 感谢关注 可以关注微信公众号「回滚吧代码」,第一时间阅读,文章持续更新;专注Java源码、架构、算法和面试。

优秀的个人博客,低调大师

Git 从入门到跑路

作者:吴小龙同学 1 今天是左小白入职新公司的第一天,这是左小白第二家公司,第一家公司工作 2 年,左小白后面已经能独立做整个项目,本想毕业就来这家公司,现在能力自我感觉也不错,是该涨波工资了,于是他找到领导,希望能涨 2 K,领导巴拉巴拉说了很多,就是没说涨工资,左小白心寒,这是宁愿再招人也不愿意给老员工涨薪! 第二天,左小白就开始进入项目,他发现新公司版本控制是用的 Git,之前是 SVN,问了旁边的同事,跟他说,Git 和 SVN 没什么区别的,会 git add和git commit就够了,左小白心想,SVN 他已经用的很溜,Git 那岂不是信手拈来,拿起键盘就干起来了。 2 左小白首先安装 Git,git config好了邮箱和姓名,然后git clone项目,就看起了代码,熟悉项目。可能因为左小白是新人,组长先分给他简单的任务,登录页面实现,这对于左小白完全是杀鸡用牛刀,不到两个小时,他就完成了,自测也 OK,可以提交代码了。 修改涉及到页面 login_activity.xml、LoginActivity.java、LoginModel.java、ApiStores.java 总共四个文件,按照同事说的,先 add 再 commit,git add ApiStores.java、git add LoginActivity.java、git add LoginModel.java、git add login_activity.xml,草(一种植物),终于 add 完了,再git commit,填上提交信息“Login”,以为这样就 OK 了,没想到这幕被组长正巧看到,左小白完全察觉到组长站在身后,大概工作太认真。 3 组长:小白,你这样提交就 OK 了吗? 左小白:嗯,是的,我之前 SVN,这样就好了啊。 组长:不,你要了解 Git 和 SVN 还是有些不同,你要了解下工作区、暂存区、本地仓库、远程仓库。 左小白:什么…… 组长:况且,你这这样一个 add 是不是有点费劲,现在就 4 个修改,看你都 add 很久,有 10 个 20 个甚至 100 个文件,怎么办,一个个 add 不得死人啊。 左小白:那应该怎么 add 啊? 组长:你起来,我操作一遍给你看下。 4 只见组长一顿操作,“我先把你提交回退掉,演示一遍给你看看”,等等,回退,那我今天代码岂不是白敲,组长估计看出了我疑惑,他接着说,你的代码还在,你看下我首先用的是git status .看下有哪些修改: git status . On branch master Your branch is up to date with 'origin/master'. Untracked files: (use "git add <file>..." to include in what will be committed) ApiStores.java LoginActivity.java LoginModel.java login_activity.xml nothing added to commit but untracked files present (use "git add" to track) 看到有 4 个文件修改,接下来看好了,我是用git add . 命令,而不是你那样一个个 add,这个.代码当前目录所有修改和新建,另外你 commit 是提交到本地仓库,明白了吗? 5 左小白这时有点懵逼,原来 Git 这么复杂,他此时很想锤旁边的同事,是他说 Git 和 SVN 差不多,害他在领导面前出丑。他现在有 3 个疑问: 1、工作区、暂存区、本地仓库、远程仓库都是啥? 2、他刚刚提交好了,组长一顿操作是如何回退的? 3、他如果删除了文件,怎么 add?他是没敢问组长,太丢人了。 6 等组长走了,左小白就带着这三个疑问去寻求答案,首先工作区、暂存区、本地仓库、远程仓库都是啥?一顿搜索引擎,看了几篇文章,大概是明白了。 原来新增,更改,删除文件这些动作,就是发生在工作区,当我们使用命令git add,那么这些更新就会出现在暂存区中。而本地仓库和远程仓库又是什么鬼?原来执行git commit,代码是提交到本地仓库,再执行git push,才把代码提交到远程仓库,这和 SVN 还是有很大的区别的,怪不得组长问我 git commit就提交了?我太菜了。 问题来了,我如果git add,我又不想 add 了,要怎么操作?亦或是git commit,我想撤销,怎么做?这也是第二个疑惑“他刚刚提交好了,组长一顿操作是如何回退的?”,带着这个疑惑,左小白又是一顿搜索引擎,搜了好十几页才得到答案,某度真是辣鸡。 如果git add后想撤销,可以通过命令git reset HEAD <file>,如果git commit后想撤销,可以通过命令git reset HEAD^,牛逼啊,今天学到知识了,左小白觉得还不够,再整个图,加深记忆。 当小白画完这个图,一看时间已经来到 22:30 了,心想赶紧溜,狗命要紧,第三个疑问明天再整吧。 坐了一个小时的地铁,小白终于到家了,洗洗刷个抖音,满足地睡去了。 7 叮咚叮咚……7 点的闹钟响了…… 起床,咦,内内怎么湿乎乎的,好像昨晚梦到女神,还跟她**,OMG。 又是一个小时的地铁,来到公司,左小白还想着“他如果删除了文件,怎么 add?” git add -u 提交所有被删除和修改的文件到数据暂存区。 git add . 提交所有修改的和新建的数据暂存区。 git add -A 提交所有被删除、被替换、被修改和新增的文件到数据暂存区。 左小白测试了一下,发现git add .和git add -A功能是一样的,不管了。 8 组长:小白,你登录功能是做好了吧。 左小白:嗯,做好了。 组长:自测没问题的话,把代码提交上来吧,然后找项目经理 review 下。 左小白:好的。 之前组长已经帮左小白 add 好了,小白就直接 commit,然后 push 成功了。微信上他就找项目经理,帮忙 review 下代码,大约 20 分钟后,项目经理来消息了,丢了一个文档,小白一头雾水,有点不好意思,但代码还得上库,鼓起勇气问道:“这是什么?” 项目经理:这是代码提交规范,你的这个提交信息“Login”不符合规范,按照“[模块][bug/feature]描述”改下。 好吧,这给小白带来了难点,他代码都已经上去了,还怎么改提交消息?难道要把代码 Abandoned 重新提交?那就试试吧!当左小白执行git status .,提示根本就没有文件修改,这行不通啊,怎么办?怎么办? 9 就在左小白焦头烂额不知道怎么办的时候,他居然发现页面上可以直接改这个提交消息,握草,so easy? 改完了,小白又找项目经理 review 代码,过了 10 分钟,项目经理回复道:“注释,注释,你代码啥注释都没有吧,适当地加点”。 问题又回到了“我代码都提交了,还怎么修改?”左小白实在没头绪,只能问问身边的同事 A。 左小白:A 哥,现在登录功能做好,代码上传了,项目经理 review 需要我加点注释,我代码都提交了,还能怎么改啊? 同事 A:你可以把之前的提交 Abandoned 点,使用命令git reset --hard。 左小白:啊?具体怎么操作啊? 同事 A:你还是搜下git reset --hard吧。 “MD,就不能教教我,你吊”左小白心里暗骂,但是还是去搜了git reset --hard,这个命令是可以回退版本,意思我回到之前修改的版本,就能继续提交了。 首先左小白通过git log .,看下提交记录: git log commit 85be09a7962a03815e5248a7b845297bd5467938 (HEAD -> master, origin/master, origin/HEAD) Author: WuXiaolong <wuxiaolong.me@qq.com> Date: Sat Apr 18 17:43:29 2020 +0800 Login commit 6b071d867e230479892a86beedc0484c8accac94 Author: 吴小龙同學 <wuxiaolong.me@gmail.com> Date: Wed Dec 25 22:16:36 2019 +0800 Update README.md 看到第一笔就是自己修改的,左小白要回退到上一笔: git reset --hard 6b071d867e230479892a86beedc0484c8accac94 等等,我的修改怎么没了,握草,不会吧,MD,想锤死身边的同事,欲哭无泪,不想搞了…… 10 天台上,左小白一个人,地上有一堆香烟头,左小白郁闷不已,工作 2 年多了,还是这么菜!越想越难受,又狂抽两支烟,这时组长来消息了,“登录代码上库了吗”,“马上上库”。 回到工位,左小白把登录代码重新写了一遍,花了一个小时,这回,他直接加了注释,完成了,左小白赶紧提交了代码,又去找项目经理 review,“小白啊,代码不要出现中文,改下吧,统一放到 string.xml,方便维护”,“好的,我马上改”。 去你大爷的,又要改,就不能一次把要改的点说完吗!左小白内心一万个草泥马。 没办法,左小白还是使用命令git reset --hard,这回聪明了,把修改内容保存了,不一会,他修改好了,又去找项目经理 review。 项目经理:小白,你座位在哪里?我来找你。 左小白有点慌,还是要改? 左小白:我在 44 楼 H。 “你是左小白,我是 XXX” “X 经理,好” 没想到,这么快,项目经理就来了。 项目经理:小白,你应该是才来公司不久是吧。 左小白:是的,今天是第三天。 项目经理:不错啊,马上就开始做项目了,发给你规范文档看了吗? 左小白:看了(其实没细看)。 项目经理:这个项目很多人参与,人多了就得严格把控代码质量。 左小白:明白。 项目经理:今天 review 你的登录代码,已经写的很不错了,提个建议,这两处逻辑是不是有问题,if 语句是不是不会走进去。 左小白看了看,握草,确实不会走进去,又得改。 左小白:是的,我把 if 删掉吧。 项目经理:好的,你改下就没其他问题了。 当左小白再次要 Abandoned 时,项目经理表示很疑惑,“小白,你干嘛要 Abandoned” 左小白:我要回退版本,修改代码啊。 项目经理:不不不,你可以直接修改,提交的时候使用git commit --amend。 左小白:这么神奇! 左小白不禁大叫起来,这个命令真是救星啊。 项目经理:你修改完成,正常git add,提交用git commit --amend,这样就在原本提交基础上修改了,之前难道都用git reset --hard回退? 左小白:是的,每次git reset --hard都会多一笔提交记录……真的耶,git commit --amend在以前的基础修改了。 项目经理:小白,知道你初来乍到,遇到问题要多问问同事哈。 左小白:好的,谢谢 X 经理。 11 哎,今天差点把左小白玩废了,下班回去感觉整个人都瘫了,明天就五一放假了,要不大宝贱走一波,放松一下。 12 正当左小白要去大宝贱走一波,看到部门群来了条重要通知:“公司方针改变,xx 项目将于 6 月中旬上线,为了按时上线,五一全员上班,无特殊事宜,不准请假。” WTF? 左小白看了时间,将近 12 点了,还要不要大宝贱,纠结了一会,还是算了吧,明天去上班。 洗澡,随着身体的一阵颤抖,什么大宝贱已经变的索然无味。 13 跟往常一样,又是一个多小时的地铁,到了公司,拿起键盘就是干,一顿复制粘贴,这一天完成了商品列表功能,商品详情,左小白感觉已经适应了新公司的节奏了。 临近下班,项目经理跟大家说:大家下班前,要把今天写的代码都提交下,明天起,我们要拉个 dev 分支,在这个分支上开发。 分支?什么是分支,左小白心里犯嘀咕,不管了,先下班,明天再说吧。 14 又是新的一天,左小白刚坐下工位,组长过来了,问:大家今天要拉 dev 分支,有什么疑问吗?左小白很明显看出大家对分支是有疑问了,于是主动发问:组长,什么是分支?为什么要分支? 组长回答:我们现在用的主分支,就是 master 分支,之前一直在主分支开发没有什么影响,现在项目马上进入收尾阶段,需要有个稳定的分支可以随时发版本,也就是 master 分支,dev 分支我们会一直在上面开发,明白吗? 呃……明白!其实左小白并不明白,怎么创建分支?又怎么把 dev 分支代码合入 master 分支,小白没敢继续问。 组长继续说:应该好多人对分支还没有概念,大家来小白位置,我演示一遍给大家看看。 15 因为远程已经拉出来 dev 分支,大家要做的就是本地创建一个分支 dev,和远程 dev 分支关联起来就可以了,首先我们来看下小白本地有哪些分支: git branch -a * master remotes/origin/HEAD -> origin/master remotes/origin/dev remotes/origin/master 这里星号意思当前指向哪个分支,那接下来我们创建 dev 分支和远程 dev 关联起来: git checkout -b dev origin/dev Branch 'dev' set up to track remote branch 'dev' from 'origin'. Switched to a new branch 'dev' 这样我们就创建了 dev 分支,可以用命令git branch -vv查看分支关联关系: git branch -vv * dev 85be09a [origin/dev] Login master 85be09a [origin/master] Login 接下来,提交代码就跟之前一样提交,master 现在已经是稳定的版本,如果需要将 dev 分支代码合入 master 上,我会通知你们的,好了,应该没有什么不明白的吧,大家干活。 经过组长这么一讲解,左小白又愉快敲起了代码。 又是一天,累但很充实的一天。 16 组长:小白,今天你完成的 epub 阅读功能要合入主分支。 左小白:好的。 左小白还不知道要怎么合并分支,搜了搜,先要切回主分支,然后使用命令git merge进行合并: $ git merge dev Auto-merging config.xml CONFLICT (content): Merge conflict in config.xml Automatic merge failed; fix conflicts and then commit the result. 这是成功了吗?等等,好像有文件冲突了啊。 17 左小白还想着如何解决分支合并冲突问题,一到公司就被经理叫到会议室,“卧槽,不会出什么事吧,我也就代码没有提交啊”,小白当时害怕极了。 “是这样的,说下昨晚开会客户的决策……没什么重要事,不要紧张兮兮的”,经理看出来我很紧张,停了下继续说:“客户觉得我们效率低,担心完不成,希望安排人去现场支持,看得出小白是个很努力的人,想安排你去支援,可以吗?” “是去哪里?” “深圳。” “这么远啊?” “不要担忧,飞机住宿都是可以报销的,你先去,后面陆续安排其他同事去”,嘀嘀咕咕说了很多。 “好吧!” 其实左小白内心是拒绝的。 “什么时间去啊?” “今天是周五,你马上买机票,下周一就过去”。 18 回到座位,左小白下载了航旅纵横 APP,看了周一去深圳的航班,6 点一班,15 点一班,那只能买 6 点了,意味着早上 4 点要起床去机场,这么早,怎么去机场还是问题,滴滴?想想就很难受。 周六还去公司加班了,周日睡了一天,吵闹的 4 点闹钟开始起床,试试滴滴,还真有人接单。40 分钟车程到了机场,匆匆忙忙过了安检。 到了深圳就开干,一刻没歇,冲突的问题还得解决,上次命令git merge进行合并报错如下: $ git merge dev Auto-merging config.xml CONFLICT (content): Merge conflict in config.xml Automatic merge failed; fix conflicts and then commit the result. 左小白先用命令 git status 查看状态: git status On branch master You have unmerged paths. (fix conflicts and run "git commit") Unmerged paths: (use "git add <file>..." to mark resolution) both modified: config.xml no changes added to commit (use "git add" and/or "git commit -a") 打开 config.xml 文件: <<<<<<< HEAD:config.xml <div id="footer">contact : email.support@github.com</div> ======= <div id="footer"> please contact us at support@github.com </div> >>>>>>> dev:config.xml 卧槽,好复杂,不想看了!却默默打开了搜索引擎,搜索关键字“Git”、“分支”、“冲突”,原来这表示 <<<<<<< 到======= 部分指 master 分支所在的位置,=======到>>>>>>>部分指 dev 分支所在的位置 。 为了解决冲突,必须选择使用由 ======= 分割的两部分中的一个,或者也可以自行合并这些内容。 当解决了所有文件里的冲突之后,对每个文件使用 git add 命令来将其标记为冲突已解决,最后git commit来提交,搞定。 19 还有 5 分钟就要迟到了,急忙往公司赶,准备上楼时看到一位高挑妹纸疾风而过,好漂亮啊,小白愣住了,这不就是小白心中的女神啊,这是哪个公司啊,小白心想。妈妈呀,要迟到了,电梯怎么还不来,爬楼吧,最后还是迟到了 3 秒,都怪那个妹纸,要找她算帐! 回到工位,又看到了那妹纸,还做她旁边?“难道她是我们公司,没见过啊?” 下午 3 时,小白为一个 bug,正没有头绪,焦头烂额,突然被一个人敲了下肩,小白气不打一处来,要发火了,只见那妹纸对她笑着说:“请问下,我git add已经提交了,我还要改点代码,如果再 add 是不是会成两条提交记录啊” “嗯,是的”,左小白很确定地说。 “一次 bug 修复我只想有一个提交记录,有办法吗” “这么强迫症,难道你是处女座?” “这都被你猜到了”,妹纸有点急了。 “你可以使用命令git reset HEAD^从本地仓库撤销到工作区,这样就可以继续修改,之前提交的那条记录会没有的。” "本地仓库?工作区?"妹纸一脸懵逼。 “工作区:你现在修改区域;通过命令git add把代码放到暂存区,使用命令git commit提交代码并不是真的提上来了,而是放在本地仓库,git push才真的提交到远程仓库,我这样简单解释“工作区”、“暂存区”、“本地仓库”、“远程仓库”四个概念可以理解吗?”我问妹纸。 “让我消化下……也就是说 Git 是不受网络影响,随时随地可以提交代码,反正也是先提交到本地仓库”。 “可以啊,都能延伸了啊”我给妹纸竖了大拇指。 “话虽如此,可我还是不敢撤销,万一操作坏了!” 好吧,左小白手把手帮妹纸操作一遍,妹纸投来敬仰的眼光,左小白内心乐开了花。 20 晚上 10 点,终于可以下班了。 “我请你吃夜宵吧,今天帮我一个大忙”,妹纸向左小白说。 “好啊。” “我今天第一次来,周围还不熟悉……” “没事,离我们不远有家串串店很好吃,我们去吃吧,哦,你可以吃辣吗?” “可以啊,我可无辣不欢。” 路程大概 10 分钟,左小白 20 多年来还很少单独跟妹纸相处,不禁有些紧张起来,为了不尴尬,左小白找着话题尬聊起来。 “哎呀,我居然还不知道你的名字呢。” “右小爱。” “我左小白,我们名字似乎有点般配啊,哈哈” …… “小爱,你是我们公司吗,怎么没见过你啊?” “不是,我来苏州无为科技有限公司。” “我是无锡思锐科技有限公司的,无锡知道吗,就是旁边的城市。” “知道的,这个项目缺人,我刚毕业,啥不会,公司让我来凑数的。” “没有啦,看你代码很溜,还以为你工作好几年了呢。” 吃串串时,两人你一句我一句瞎聊着,右小爱就是深圳人,高考没考好,就选了苏州大学,目前实习。 21 "小白,我git add加到暂存区,我想撤销怎么搞?就是从暂存区撤销到工作区怎么搞?"小爱问到。 “命令git reset HEAD <file>” “果然可以。” “小白,经理说这个 bug 不用改了,我工作区修改的代码可以不要了,难道要把我修改的代码再改回去?” “不用,命令git checkout <file>即可。” 过了一会,小爱很开心地对小白说:“你真厉害。” “小爱,看你写了一天代码,有什么成果啊?” “解了 5 个 bug 了。” “你可以看你写了多少代码的。” “这么神奇?怎么看啊?” “ git diff --shortstat "@{0 day ago}"查看今天你写了多少行代码。" “牛啊,小白!你知道这么多,都是怎么学的?” “我之前有看过吴小龙同学写的博客,公号 吴小龙同学 后台回复 Git 可以获取 Git 完整资料。” “好的,我这就去公号 吴小龙同学 后台回复 Git 获取资料。” “牛逼,还有 Git 思维导图,我平时用到的命令都有,啧啧,原来你教我都是来自吴小龙同学啊。” “嘿嘿。” 22 有了小爱“陪伴”时光好像过的格外的快,转眼三个月过去了,左小白和右小爱从一开始都不知道对方名字,到慢慢熟悉,到后面直接称兄道弟,可小爱实习期到了,小爱当然是要在深圳留在她爸妈身边的,因此她要先回苏州,办理离职。 “小白,你今后打算啊。” “不知道啊,可能要留在无锡吧。” 小爱没有继续说话,这一天都没有跟左小白说句话。 自从小爱回去了,左小白整天提不起精神来,总感觉是缺少了什么,不行,左小白喜欢上右小爱了。 第三天右小爱回到了深圳,小白想向小爱表白,但是又很担心,万一不答应,不要最后兄弟都没得做,在这种纠结中,小白发烧了。 很明显感觉她有些不高兴。 小白急了,豁出去吧。 要晕,要晕,小白假装晕了过去。 只见小爱轻轻地点点了头。 小爱狠狠捶了小白胸口一下。 第二天,左小白提了离职申请,从此左小白和右小爱过上了没羞没臊的幸福生活。

优秀的个人博客,低调大师

JVM-入门了解篇

引言:任何软件语言的设计都有对应的硬件设备作为参考,同理我们理解对应知识结构或设计思想也可以参考对应的体系模型进行理解。 计算机体系结构: 计算机处理数据过程: (1)提取阶段:由输入设备把原始数据或信息输入给计算机存储器存起来 (2)解码阶段:根据CPU的指令集架构(ISA)定义将数值解译为指令 (3)执行阶段:再由控制器把需要处理或计算的数据调入运算器 (4)最终阶段:由输出设备把最后运算结果输出 本质就是CPU处理数据并且返回 CPU = 控制器+运算器+存储器 什么问题需要JVM来解决? 如果你在线上遇到了OOM,你是否会束手无策。 线上卡顿是否可能是因为频繁Full GC造成的。 新项目上线,服务器数量以及配置不足,对于性能的扩展只能靠服务器的增加,而不能通过JVM的 调优达到实现服务器性能的突破。 面试经常会问到JVM的一些问题,但是当面试官问到你实际的落地点时,你就会茫然不知所措,没 有条理性,或者答非所问。 JVM是什么? JVM:Java Virtual Machine (Java虚拟机),特性:Write Once Run Anywhere 一次写入跨平台运行。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册