-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.json
1 lines (1 loc) · 323 KB
/
index.json
1
[{"categories":["Mac"],"content":"这篇文章展示了 mac环境初始化.","date":"2024-07-18","objectID":"/iterm/","tags":["Mac"],"title":"mac 环境初始化","uri":"/iterm/"},{"categories":["Mac"],"content":"homebrew 安装 brew安装 ","date":"2024-07-18","objectID":"/iterm/:1:0","tags":["Mac"],"title":"mac 环境初始化","uri":"/iterm/"},{"categories":["Mac"],"content":"访达显示系统根目录 访达\u003e偏好设置\u003e边栏\u003e勾选“用户”\u003e左侧显示用户 ","date":"2024-07-18","objectID":"/iterm/:2:0","tags":["Mac"],"title":"mac 环境初始化","uri":"/iterm/"},{"categories":["Mac"],"content":"安装 iterm2 https://blog.csdn.net/wangzhongshun/article/details/122089389 iTerm2隐藏用户名和主机我设置了失效,不知道什么情况。 ","date":"2024-07-18","objectID":"/iterm/:3:0","tags":["Mac"],"title":"mac 环境初始化","uri":"/iterm/"},{"categories":["Tool","Git"],"content":"这篇文章展示了gitlab私有库.","date":"2024-07-12","objectID":"/git_child/","tags":["Tool","Git"],"title":"gitlab私有库","uri":"/git_child/"},{"categories":["Tool","Git"],"content":"一、设置go env 设置私有仓库的git地址 go env -w GOPRIVATE=\"git.example.com\" ","date":"2024-07-12","objectID":"/git_child/:1:0","tags":["Tool","Git"],"title":"gitlab私有库","uri":"/git_child/"},{"categories":["Tool","Git"],"content":"二、配置git {username}: 替换成gitlab账号名称 {token}: 替换成自己的 Personal Access Token token生成:gitlab → Preferences → Access Tokens,填写完Token名称和过期时间,勾选好全部的权限,生成即可 使用命令行 git config --global url.\"git.weyland-x.com\".insteadOf \"https://{username}:{token}@git.example.com\" 或者直接编辑~/.gitconfig [url \"[email protected]/\"] insteadof = https://{username}:{token}@git.example.com/ ","date":"2024-07-12","objectID":"/git_child/:2:0","tags":["Tool","Git"],"title":"gitlab私有库","uri":"/git_child/"},{"categories":["Tool","Git"],"content":"三、配置 .netrc 解决gitlab不能拉取子组库的问题 linux/mac下 在用户目录下编辑~/.netrc文件 machine git.example.com login {Username} password {Personal Access Token} windows下 在用户目录下创建 _netrc 文件 machine git.example.com login {Username} password {Personal Access Token} 自行替换username和token,与git配置中的相同 ","date":"2024-07-12","objectID":"/git_child/:3:0","tags":["Tool","Git"],"title":"gitlab私有库","uri":"/git_child/"},{"categories":["Tool"],"content":"这篇文章展示了分布式链路追踪.","date":"2024-07-11","objectID":"/trace/","tags":["Tool"],"title":"分布式链路追踪","uri":"/trace/"},{"categories":["Tool"],"content":"分布式链路追踪 ","date":"2024-07-11","objectID":"/trace/:0:0","tags":["Tool"],"title":"分布式链路追踪","uri":"/trace/"},{"categories":["Tool"],"content":"核心概念 Trace 是一个逻辑概念,表示一次(分布式)请求经过的所有局部操作(Span)构成的一条完整的有向无环图,其中所有的 Span 的 TraceId 相同。 Span 则是真实的数据实体模型,表示一次(分布式)请求过程的一个步骤或操作,代表系统中一个逻辑运行单元,Span 之间通过嵌套或者顺序排列建立因果关系。Span 数据在采集端生成,之后上报到服务端,做进一步的处理。其包含如下关键属性: Name:操作名称,如一个 RPC 方法的名称,一个函数名 StartTime/EndTime:起始时间和结束时间,操作的生命周期 ParentSpanId:父级 Span 的 ID Attributes:属性,一组 \u003cK,V\u003e 键值对构成的集合 Event:操作期间发生的事件 SpanContext:Span 上下文内容,通常用于在 Span 间传播,其核心字段包括 TraceId、SpanId ","date":"2024-07-11","objectID":"/trace/:1:0","tags":["Tool"],"title":"分布式链路追踪","uri":"/trace/"},{"categories":["Tool"],"content":"一般架构 在应用端需要通过侵入或者非侵入的方式,注入 Tracing Sdk,以跟踪、生成、传播和上报请求调用链路数据; Collect agent 一般是在靠近应用侧的一个边缘计算层,主要用于提高 Tracing Sdk 的写性能,和减少 back-end 的计算压力; 采集的链路跟踪数据上报到后端时,首先经过 Gateway 做一个鉴权,之后进入 kafka 这样的 MQ 进行消息的缓冲存储; 在数据写入存储层之前,我们可能需要对消息队列中的数据做一些清洗和分析的操作,清洗是为了规范和适配不同的数据源上报的数据,分析通常是为了支持更高级的业务功能,比如流量统计、错误分析等,这部分通常采用flink这类的流处理框架来完成; 存储层会是服务端设计选型的一个重点,要考虑数据量级和查询场景的特点来设计选型,通常的选择包括使用 Elasticsearch、Cassandra、或 Clickhouse 这类开源产品; 流处理分析后的结果,一方面作为存储持久化下来,另一方面也会进入告警系统,以主动发现问题来通知用户,如错误率超过指定阈值发出告警通知这样的需求等。 ","date":"2024-07-11","objectID":"/trace/:2:0","tags":["Tool"],"title":"分布式链路追踪","uri":"/trace/"},{"categories":["Tool"],"content":"开源实现 ","date":"2024-07-11","objectID":"/trace/:3:0","tags":["Tool"],"title":"分布式链路追踪","uri":"/trace/"},{"categories":["Tool"],"content":"SkyWalking 前置条件: 搭建skywalking需要三个镜像: 1.es 存储数据 2.oap 服务器 3.ui 界面 这里涉及到多个服务,采用docker-compose部署: version: '3.8' services: elasticsearch: # image: docker.elastic.co/elasticsearch/elasticsearch-oss:${ES_VERSION} image: elasticsearch:7.9.0 container_name: elasticsearch # --restart=always : 开机启动,失败也会一直重启; # --restart=on-failure:10 : 表示最多重启10次 restart: always ports: - \"9200:9200\" - \"9300:9300\" healthcheck: test: [ \"CMD-SHELL\", \"curl --silent --fail localhost:9200/_cluster/health || exit 1\" ] interval: 30s timeout: 10s retries: 3 start_period: 10s environment: - discovery.type=single-node # 锁定物理内存地址,防止elasticsearch内存被交换出去,也就是避免es使用swap交换分区,频繁的交换,会导致IOPS变高; - bootstrap.memory_lock=true # 设置时区 - TZ=Asia/Shanghai # - \"ES_JAVA_OPTS=-Xms512m -Xmx512m\" ulimits: memlock: soft: -1 hard: -1 oap: image: apache/skywalking-oap-server:8.9.1 container_name: oap restart: always # 设置依赖的容器 depends_on: elasticsearch: condition: service_healthy # 关联ES的容器,通过容器名字来找到相应容器,解决IP变动问题 links: - elasticsearch # 端口映射 ports: - \"11800:11800\" - \"12800:12800\" # 监控检查 healthcheck: test: [ \"CMD-SHELL\", \"/skywalking/bin/swctl ch\" ] # 每间隔30秒执行一次 interval: 30s # 健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败; timeout: 10s # 当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3 次。 retries: 3 # 应用的启动的初始化时间,在启动过程中的健康检查失效不会计入,默认 0 秒。 start_period: 10s environment: # 指定存储方式 SW_STORAGE: elasticsearch # 指定存储的地址 SW_STORAGE_ES_CLUSTER_NODES: elasticsearch:9200 SW_HEALTH_CHECKER: default SW_TELEMETRY: prometheus TZ: Asia/Shanghai # JAVA_OPTS: \"-Xms2048m -Xmx2048m\" ui: image: apache/skywalking-ui:8.9.1 container_name: skywalking-ui restart: always depends_on: oap: condition: service_healthy links: - oap ports: - \"8088:8080\" environment: SW_OAP_ADDRESS: http://oap:12800 TZ: Asia/Shanghai 执行成功后,docker会启动对应的容器 验证步骤: 打开localhost:8088 看下ui界面是否能访问 访问:http://localhost:9200/ 是否有数据返回,以验证es是否正常启动 部署 1.下载对应系统的skywalking的agent 2.执行如下命令: cd /path/to/skywalking \u0026\u0026 make build 3.执行成功会在bin文件夹下多出对应系统的可执行文件 不同的操作系统对应的可执行文件不同。例如,mac系统需选择skywalking-go-agent–darwin-amd64 4.打开Go项目,在main package中导入skywalking module package main import ( _ \"github.com/apache/skywalking-go\" ) 5.配置config.yaml文件 配置文件示例: agent: # Service name is showed in UI. service_name: ${SW_AGENT_NAME:Your_ApplicationName} # To obtain the environment variable key for the instance name, if it cannot be obtained, an instance name will be automatically generated. instance_env_name: SW_AGENT_INSTANCE_NAME # Sampling rate of tracing data, which is a floating-point value that must be between 0 and 1. sampler: ${SW_AGENT_SAMPLE:1} meter: # The interval of collecting metrics, in seconds. collect_interval: ${SW_AGENT_METER_COLLECT_INTERVAL:20} sampling: rate: 100 reporter: grpc: # The gRPC server address of the backend service. backend_service: ${SW_AGENT_REPORTER_GRPC_BACKEND_SERVICE:127.0.0.1:11800} # The maximum count of segment for reporting tracing data. max_send_queue: ${SW_AGENT_REPORTER_GRPC_MAX_SEND_QUEUE:5000} # The interval(s) of checking service and backend service check_interval: ${SW_AGENT_REPORTER_GRPC_CHECK_INTERVAL:20} # The authentication string for communicate with backend. authentication: ${SW_AGENT_REPORTER_GRPC_AUTHENTICATION:} # The interval(s) of fetching dynamic configuration from backend. cds_fetch_interval: ${SW_AGENT_REPORTER_GRPC_CDS_FETCH_INTERVAL:20} tls: # Whether to enable TLS with backend. enable: ${SW_AGENT_REPORTER_GRPC_TLS_ENABLE:false} # The file path of ca.crt. The config only works when opening the TLS switch. ca_path: ${SW_AGENT_REPORTER_GRPC_TLS_CA_PATH:} # The file path of client.pem. The config only works when mTLS. client_key_path: ${SW_AGENT_REPORTER_GRPC_TLS_CLIENT_KEY_PATH:} # The file path of client.crt. The config only works when mTLS. client_cert_chain_path: ${SW_AGENT_REPORTER_GRPC_TLS_CLIENT_CERT_CHAIN_PATH:} # Controls whether a client verifies the server's certificate chain and host name. insecure_skip_verify: ${SW_AGENT_REPORTER_GRPC_TLS_INSECURE_SKIP_VERIFY:false} log: # The type determines which logging type is currently use","date":"2024-07-11","objectID":"/trace/:3:1","tags":["Tool"],"title":"分布式链路追踪","uri":"/trace/"},{"categories":["Tool"],"content":"Ref: https://www.alibabacloud.com/help/zh/arms/tracing-analysis/untitled-document-1690516495864?spm=a2c63.p38356.0.0.59bc6bd1ao2W05#6f7d899246n3f https://github.com/alibabacloud-observability/skywalking-demo/tree/master/skywalking-go-demo/skywalking-go-agent-demo https://skywalking.apache.org/zh/2023-06-01-quick-start-with-skywalking-go-agent/ https://juejin.cn/post/7250317954993619000 ","date":"2024-07-11","objectID":"/trace/:3:2","tags":["Tool"],"title":"分布式链路追踪","uri":"/trace/"},{"categories":["Design"],"content":"这篇文章展示了领域驱动设计基本知识.","date":"2024-05-14","objectID":"/ddd/","tags":["Design"],"title":"领域驱动设计","uri":"/ddd/"},{"categories":["Design"],"content":"领域模型模式 目前企业级应用开发中,业务逻辑的组织方式主要是事务脚本模式。事务脚本按照业务处理 的过程组织业务逻辑,每个过程处理来自客户端的单个请求。客户端的每次请求都包含了一 定的业务处理逻辑,而程序则按照每次请求的业务逻辑进行划分。 事务脚本模式典型的就是 Controller→Service→Dao 这样的程序设计模式。Controller 封 装用户请求,根据请求参数构造一些数据对象调用 Service,Service 里面包含大量的业务 逻辑代码,完成对数据的处理,期间可能需要通过 Dao 从数据库中获取数据,或者将数据 写入数据库中。 领域模型模式和事务脚本模式不同。在领域模型模式下,业务逻辑围绕领域模型设计。 领域模型的对象则包含了对象的数据和计算逻辑 领域模型是合并了行为和数据的领域的对 象模型。通过领域模型对象的交互完成业务逻辑的实现,也就是说,设计好了领域模型对 象,也就设计好了业务逻辑实现。和事务脚本被称作贫血模型相对应的,领域模型也被称为 充血模型。 对于复杂的业务逻辑实现来说,用领域模型模式更有优势。特别是在持续的需求变更和业务 迭代过程中,把握好领域模型,对业务逻辑本身也会有更清晰的认识。使用领域模型增加新 的产品类型的时候,就不需要修改现有的代码,只需要扩展新的产品类和收入策略类就可以 了。 在需求变更过程中,如果一个需求和领域模型有冲突,和模型的定义以及模型间的交互逻辑 不一致,那么很有可能这个需求本身就是伪需求。很多看似合理的需求其实和业务的内在逻 辑是有冲突的,这样的需求也不会带来业务的价值,通过领域模型分析,可以识别出这样的 伪需求,使系统更好地保持一致性,也可以使开发资源投入到更有价值的地方去。 ","date":"2024-05-14","objectID":"/ddd/:1:0","tags":["Design"],"title":"领域驱动设计","uri":"/ddd/"},{"categories":["Design"],"content":"领域驱动设计 领域是一个组织所做的事情以及其包含的一切,通俗地说,就是组织的业务范围和做事方 式,也是软件开发的目标范围。比如对于淘宝这样一个以电子商务为主要业务的组织,C2C 电子商务就是它的领域。领域驱动设计就是从领域出发,分析领域内模型及其关系,进而设 计软件系统的方法。 如果我们说要对 C2C 电子商务这个领域进行建模设计,那么这个范围就太大了,不知 道该如何下手。所以通常的做法是把整个领域拆分成多个子域,比如用户、商品、订单、库 存、物流、发票等。强相关的多个子域组成一个界限上下文,界限上下文是对业务领域范围 的描述,对于系统实现而言,可以想象成相当于是一个子系统或者是一个模块。界限上下文 和子域共同组成组织的领域 不同的界限上下文,也就是不同的子系统或者模块之间会有各种的交互合作。如何设计这些 交互合作呢?DDD 使用上下文映射图来完成 在 DDD 中,领域模型对象也被称为实体,每个实体都是唯一的,具有一个唯一标识,一个 订单对象是一个实体,一个产品对象也是一个实体,订单 ID 或者产品 ID 是它们的唯一标 识。实体可能会发生变化,比如订单的状态会变化,但是它们的唯一标识不会变化。 实体设计是 DDD 的核心所在,首先通过业务分析,识别出实体对象,然后通过相关的业务 逻辑设计实体的属性和方法。这里最重要的,是要把握住实体的特征是什么,实体应该承担 什么职责,不应该承担什么职责,分析的时候要放在业务场景和界限上下文中,而不是想当 然地认为这样的实体就应该承担这样的角色。 事实上,并不是领域内的对象都应该被设计为实体,DDD 推荐尽可能将对象设计为值对 象。比如像住址这样的对象就是典型的值对象,也许建在住址上的房子可以被当做一个实 体,但是住址仅仅是对房子的一个描述,像这样仅仅用来做度量或描述的对象应该被设计为 值对象。 值对象的一个特点是不变性,一个值对象创建以后就不能再改变了。如果地址改变了,那就 是一个新地址,而一个订单实体则可能会经历创建、待支付、已支付、代发货、已发货、待 签收、待评价等各种变化。 领域实体和界限上下文包含了业务的主要逻辑,但是最终如何构建一个系统,如何将领域实 体对外暴露,开发出一个完整的系统。事实上,DDD 支持各种架构方案,比如典型的分层 架构: 领域实体被放置在领域层,通过应用层对领域实体进行包装,最终提供一组访问接口,通过 接口层对外开放。 六边形架构是 DDD 中比较知名的一种架构方式,领域模型通过应用程序封装成一个相对比 较独立的模块,而不同的外部系统则通过不同的适配器和领域模型交互,比如可以通过 HTTP 接口访问领域模型,也可以通过 Web Service 或者消息队列访问领域模型,只需要 为这些不同的访问接口提供不同的适配器就可以了。 领域驱动设计的技术体系内还有其他一些方法和概念,但是最核心的还是领域模型本身,通 过领域实体及其交互完成业务逻辑处理才是 DDD 的核心目标。至于是不是用了 CQRS,是 不是事件驱动,有没有事件溯源,并不是 DDD 的核心。 ","date":"2024-05-14","objectID":"/ddd/:2:0","tags":["Design"],"title":"领域驱动设计","uri":"/ddd/"},{"categories":["Design"],"content":"这篇文章展示了设计模式基本知识.","date":"2024-05-14","objectID":"/design_pattern/","tags":["Design"],"title":"设计模式","uri":"/design_pattern/"},{"categories":["Architecture"],"content":"这篇文章展示了分布式架构相关知识.","date":"2024-05-13","objectID":"/distributed/","tags":["Architecture"],"title":"分布式架构","uri":"/distributed/"},{"categories":["Architecture"],"content":"垂直伸缩与水平伸缩 为了应对高并发用户访问带来的系统资源消耗,一种解决办法是垂直伸缩。所谓的垂直伸缩 就是提升单台服务器的处理能力 垂直伸缩带来的价格成本和服务器的处理能力并不一定呈线性关系,也就是说,增加同样的 费用,并不能得到同样的计算能力。而且计算能力越强大,需要花费的钱就越多。 所谓的水平伸缩,指的是不去提升单机的处理能力,不使用更昂贵更快更厉害的硬件,而是 使用更多的服务器,将这些服务器构成一个分布式集群,通过这个集群,对外统一提供服务,以此来提高系统整体的处理能力。 ","date":"2024-05-13","objectID":"/distributed/:1:0","tags":["Architecture"],"title":"分布式架构","uri":"/distributed/"},{"categories":["Architecture"],"content":"互联网分布式架构演化 第一次分离,只需要把数据库、文件系统进行远程部署,进行 远程访问就可以了. 使用缓存改善性能 通过负载均衡服务器,将应用服务器部署为一个集群,添加更多的应用服务器去处 理用户的访问。 数据库的读写分离,将一个数据库通过数据复制的方式,分裂为两个 数据库,主数据库主要负责数据的写操作,所有的写操作都复制到从数据库上,保证从数据 库的数据和主数据库数据一致,而从数据库主要提供数据的读操作。 海量数据的存储,主要通过分布式数据库、分布式文件系统、NoSQL 数据库解决。直接在 数据库上查询已经无法满足这些数据的查询性能要求,还需要部署独立的搜索引擎提供查询 服务。同时减少数据中心的网络带宽压力,提供更好的用户访问延时,使用 CDN 和反向代 理提供前置缓存,尽快返回静态文件资源给用户。 为了使各个子系统更灵活易于扩展,则使用分布式消息队列将相关子系统解耦,通过消息的 发布订阅完成子系统间的协作。使用微服务架构将逻辑上独立的模块在物理上也独立部署, 单独维护,应用系统通过组合多个微服务完成自己的业务逻辑,实现模块更高级别的复用, 从而更快速地开发系统和维护系统。 ","date":"2024-05-13","objectID":"/distributed/:2:0","tags":["Architecture"],"title":"分布式架构","uri":"/distributed/"},{"categories":["Architecture"],"content":"这篇文章展示了负载均衡架构相关知识.","date":"2024-05-13","objectID":"/lb/","tags":["Architecture"],"title":"负载均衡架构","uri":"/lb/"},{"categories":["Architecture"],"content":"HTTP重定向负载均衡 HTTP 重定向负载均衡是一种比较简单的负载均衡技术实现。来自用户的 HTTP 请求到达负 载均衡服务器以后,负载均衡服务器根据某种负载均衡算法计算得到一个应用服务器的地 址,通过 HTTP 状态码 302 重定向响应,将新的 IP 地址发送给用户浏览器,用户浏览器收 到重定向响应以后,重新发送请求到真正的应用服务器,以此来实现负载均衡 HTTP 重定向负载均衡的优点是设计比较简单,但是它的缺点也比较明显,一方面用户完成 一次访问,就需要请求两次数据中心,一次请求负载均衡服务器,一次是请求应用服务器, 请求处理性能会受很大的影响。 另一个问题是因为响应要重定向到真正的应用服务器,所以需要把应用服务器的 IP 地址暴 露给外部用户,这样可能会带来安全性的问题。 ","date":"2024-05-13","objectID":"/lb/:1:0","tags":["Architecture"],"title":"负载均衡架构","uri":"/lb/"},{"categories":["Architecture"],"content":"DNS 负载均衡 当用户从浏览器 发起 HTTP 请求的时候,首先要到 DNS 域名服务器进行域名解析,解析得到 IP 地址以 后,用户才能够根据 IP 地址建立 HTTP 连接,访问真正的数据中心的应用服务器,这时候 就可以在 DNS 域名解析的时候进行负载均衡,也就是说,不同的用户进行域名解析的时 候,返回不同的 IP 地址,从而实现负载均衡。 域名解析直接得到应用服务器的 IP 地址,确实会存在安全性问 题。但是大型互联网应用通常并不直接通过 DNS 解析得到应用服务器 IP 地址,而是解析 得到负载均衡服务器的 IP 地址。也就是说,大型网互联网应用需要两次负载均衡,一次通 过 DNS 负载均衡,用户请求访问数据中心负载均衡服务器集群的某台机器,然后这台负载 均衡服务器再进行一次负载均衡,将用户请求分发到应用服务器集群的某台服务器上。通过 这种方式,应用服务器不需要用公网 IP 将自己暴露给外部访问者,避免了安全性问题。 ","date":"2024-05-13","objectID":"/lb/:2:0","tags":["Architecture"],"title":"负载均衡架构","uri":"/lb/"},{"categories":["Architecture"],"content":"反向代理负载均衡 用户请求到达数据中心以后,最先到达的就是反向代理服 务器。反向代理服务器查找本机是否有请求的资源,如果有就直接返回资源数据,如果没 有,就将请求发送给后面的应用服务器继续处理。事实上,发送请求给应用服务器的时候, 就可以进行负载均衡,将不同的用户请求分发到不同的服务器上面去。Nginx 这样的 HTTP 服务器就会同时提供反向代理与负载均衡功能。 作为互联网应用层的一个协议,HTTP 协议相对说来比较重,效率比较低,所以反向代理负 载均衡通常用在小规模的互联网系统上,只有几台或者十几台服务器的规模。 ","date":"2024-05-13","objectID":"/lb/:3:0","tags":["Architecture"],"title":"负载均衡架构","uri":"/lb/"},{"categories":["Architecture"],"content":"IP负载均衡 网络层负载均衡。它的主要工作原理是当用户的请求到达负载 均衡服务器以后,负载均衡服务器会对网络层的数据包的 IP 地址进行转换,修改 IP 地址, 将其修改为应用服务器的 IP 地址,然后把数据包重新发送出去,请求数据就会到达应用服 务器。 IP 负载均衡不需要在 HTTP 协议层工作,可以在操作系统内核直接修改 IP 数据包的地址, 因此,效率比应用层的反向代理负载均衡高得多。但是它依然有一个缺陷,不管是请求还是 响应的数据包,都要通过负载均衡服务器进行 IP 地址转换,才能够正确地把请求数据分发 到应用服务器,或者正确地将响应数据包发送到用户端程序。 ","date":"2024-05-13","objectID":"/lb/:4:0","tags":["Architecture"],"title":"负载均衡架构","uri":"/lb/"},{"categories":["Architecture"],"content":"数据链路层负载均衡 负载均衡服务器并不修改数据包的 IP 地址,而是修改数据链路层里的网卡 mac 地址,在数据链路层实现负载均衡。而应用服务器和负载均衡服务器都使用相同的虚拟 IP 地址,这样 IP 路由就不会受到影响,但是网卡会根据自己的 mac 地址,选择负载均衡服 务器发送到自己网卡的数据包,交给对应的应用程序去处理,处理结束以后,当把响应的数 据包发送到网络上的时候,因为 IP 地址没有修改过,所以这个响应会直接到达用户的浏览 器,而不会再经过负载均衡服务器。 链路层负载均衡避免响应数据再经过负载均衡服务器,因而可以承受较大的数据传输压力, 所以,目前大型互联网应用基本都使用链路层负载均衡。 ","date":"2024-05-13","objectID":"/lb/:5:0","tags":["Architecture"],"title":"负载均衡架构","uri":"/lb/"},{"categories":["Architecture"],"content":"这篇文章展示了高可用架构相关知识.","date":"2024-05-13","objectID":"/high_availability/","tags":["Architecture"],"title":"高可用架构","uri":"/high_availability/"},{"categories":["Architecture"],"content":"高可用的度量 一般说来,两个 9 表示系统基本可用,年度不可用时间小于 88 小时;3 个 9 是较高可 用,年度不可用时间小于 9 个小时;4 个 9 是具有自动恢复能力的高可用,年度不可用时 间小于 53 分钟;5 个 9 指极高的可用性,年度不可用时间小于 5 分钟 在互联网企业中,为了更好地管理系统的可用 性,界定好系统故障以后的责任,通常会用故障分进行管理。一般过程是,根据系统可用性 指标换算成一个故障分,这个故障分是整个系统的故障分 ","date":"2024-05-13","objectID":"/high_availability/:1:0","tags":["Architecture"],"title":"高可用架构","uri":"/high_availability/"},{"categories":["Architecture"],"content":"高可用的架构 ","date":"2024-05-13","objectID":"/high_availability/:2:0","tags":["Architecture"],"title":"高可用架构","uri":"/high_availability/"},{"categories":["Architecture"],"content":"冗余备份 冗余备份是说,提供同一服务的服务器要存在冗余,即任何服务都不能只有一台服务器,服 务器之间要互相进行备份,任何一台服务器出现故障的时候,请求可以发送到备份的服务器 去处理。这样,即使某台服务器失效,在用户看来,系统依然是可用的。 负载均衡可以实现系统的高可用。 负载均衡服务器通过心跳检测发现集群中某台应用服务器失效,然后负载均衡服务器就不将 请求分发给这台服务器,对用户而言,也就感觉不到有服务器失效,系统依然可用。 数据库主主复制,也是一种冗余备份。这个时候, 不只是数据库系统 RDBMS 互相进行冗余备份,数据库里的数据也要进行冗余备份,一份 数据存储在多台服务器里,保证当任何一台服务器失效,数据库服务依然可以使用。 ","date":"2024-05-13","objectID":"/high_availability/:2:1","tags":["Architecture"],"title":"高可用架构","uri":"/high_availability/"},{"categories":["Architecture"],"content":"失败隔离 将失败限制在一个较小的范围之内,使故障影响 范围不扩大。具体实现失败隔离的主要架构技术是消息队列。 另一方面,由于分布式消息队列具有削峰填谷的作用,所以在高并发的时候,消息的生产者 可以将消息缓冲在分布式消息队列中,消费者可以慢慢地从消息队列中去处理,而不会将瞬 时的高并发负载压力直接施加到整个系统上,导致系统崩溃。也就是将压力隔离开来,使消 息生产者的访问压力不会直接传递到消息的消费者,这样可以提高数据库等对压力比较敏感 的服务的可用性。 同时,消息队列还使得程序解耦,将程序的调用和依赖隔离开来,我们知道,低耦合的程序 更加易于维护,也可以减少程序出现 Bug 的几率。 ","date":"2024-05-13","objectID":"/high_availability/:2:2","tags":["Architecture"],"title":"高可用架构","uri":"/high_availability/"},{"categories":["Architecture"],"content":"限流降级 限流和降级也是保护系统高可用的一种手段。在高并发场景下,如果系统的访问量超过了系 统的承受能力,可以通过限流对系统进行保护。限流是指对进入系统的用户请求进行流量限 制,如果访问量超过了系统的最大处理能力,就会丢弃一部分的用户请求,保证整个系统可 用,保证大部分用户是可以访问系统的。这样虽然有一部分用户的请求被丢弃,产生了部分 不可用,但还是好过整个系统崩溃,所有的用户都不可用要好。 降级是保护系统的另一种手段。有一些系统功能是非核心的,但是它也给系统产生了非常大 的压力。 ","date":"2024-05-13","objectID":"/high_availability/:2:3","tags":["Architecture"],"title":"高可用架构","uri":"/high_availability/"},{"categories":["Architecture"],"content":"异地多活 为了解决这个问题,同时也为了提高系统的处理能力和改善用户体验,很多大型互联网应用 都采用了异地多活的多机房架构策略,也就是说将数据中心分布在多个不同地点的机房里, 这些机房都可以对外提供服务,用户可以连接任何一个机房进行访问,这样每个机房都可以 提供完整的系统服务,即使某一个机房不可使用,系统也不会宕机,依然保持可用。 异地多活的架构考虑的重点就是,用户请求如何分发到不同的机房去。这个主要可以在域名 解析的时候完成,也就是用户进行域名解析的时候,会根据就近原则或者其他一些策略,完 成用户请求的分发。另一个至关重要的技术点是,因为是多个机房都可以独立对外提供服 务,所以也就意味着每个机房都要有完整的数据记录。用户在任何一个机房完成的数据操 作,都必须同步传输给其他的机房,进行数据实时同步。 ","date":"2024-05-13","objectID":"/high_availability/:2:4","tags":["Architecture"],"title":"高可用架构","uri":"/high_availability/"},{"categories":["Architecture"],"content":"这篇文章展示了高性能相关知识.","date":"2024-05-13","objectID":"/high_performance/","tags":["Architecture"],"title":"高性能","uri":"/high_performance/"},{"categories":["Architecture"],"content":"性能指标 ","date":"2024-05-13","objectID":"/high_performance/:1:0","tags":["Architecture"],"title":"高性能","uri":"/high_performance/"},{"categories":["Architecture"],"content":"响应时间 从发出请求开始到收到最后响应数据所需要的时间。响应时间是系统最 重要的性能指标,最直接地反映了系统的快慢。 ","date":"2024-05-13","objectID":"/high_performance/:1:1","tags":["Architecture"],"title":"高性能","uri":"/high_performance/"},{"categories":["Architecture"],"content":"并发数 系统同时处理的请求数,这个数字反映了系统的负载压力情况。 ","date":"2024-05-13","objectID":"/high_performance/:1:2","tags":["Architecture"],"title":"高性能","uri":"/high_performance/"},{"categories":["Architecture"],"content":"吞吐量 单位时间内系统处理请求的数量,体现的是系统的处理能力。我们一般用每秒 HTTP 请求数 HPS、每秒事务数 TPS、每秒查询数 QPS 这样的一些指标来衡量 吞吐量、响应时间和并发数三者之间是有关联性的。并发数不变,响应时间足够快,那么单 位时间的吞吐量就会相应的提高。 ","date":"2024-05-13","objectID":"/high_performance/:1:3","tags":["Architecture"],"title":"高性能","uri":"/high_performance/"},{"categories":["Architecture"],"content":"性能计数器 服务器或者操作系统性能的一些指标数据,包括系统负载 System Load、对象和线程数、内存使用、CPU 使用、磁盘和网络 I/O 使用等指标,这些指标是系 统监控的重要参数,反映系统负载和处理能力的一些关键指标,通常这些指标和性能是强相 关的。这些指标很高,成为瓶颈,通常也预示着性能可能会出现问题。 ","date":"2024-05-13","objectID":"/high_performance/:1:4","tags":["Architecture"],"title":"高性能","uri":"/high_performance/"},{"categories":["Architecture"],"content":"性能测试 ","date":"2024-05-13","objectID":"/high_performance/:2:0","tags":["Architecture"],"title":"高性能","uri":"/high_performance/"},{"categories":["Architecture"],"content":"性能测试 性能测试是指以系统设计初期规划的性能指标为预期目标,对系统不断地施加压力,验证系 统在资源可接受的范围内是否达到了性能的预期目标。这个过程中,随着并发数的增加,吞 入量也在增加,但是响应时间变化不大。系统正常情况下的并发访问压力应该都在这个范围 内。 ","date":"2024-05-13","objectID":"/high_performance/:2:1","tags":["Architecture"],"title":"高性能","uri":"/high_performance/"},{"categories":["Architecture"],"content":"负载测试 负载测试则是对系统不断地施加并发请求,增加系统的压力,直到系统的某项或多项指标达 到安全临界值。这个过程中,随着并发数的增加,吞吐量只有小幅的增加,达到最大值后, 吞吐量还会下降,而响应时间则会不断增加。 ","date":"2024-05-13","objectID":"/high_performance/:2:2","tags":["Architecture"],"title":"高性能","uri":"/high_performance/"},{"categories":["Architecture"],"content":"压力测试 压力测试是指在超过安全负载的情况下,增加并发请求数,对系统继续施加压力,直到系统 崩溃,或者不再处理任何请求,此时的并发数就是系统的最大压力承受能力。这个过程中, 吞吐量迅速下降,响应时间迅速增加,到了系统崩溃点,吞吐量为 0,响应时间无穷大。 ","date":"2024-05-13","objectID":"/high_performance/:2:3","tags":["Architecture"],"title":"高性能","uri":"/high_performance/"},{"categories":["Architecture"],"content":"性能优化 ","date":"2024-05-13","objectID":"/high_performance/:3:0","tags":["Architecture"],"title":"高性能","uri":"/high_performance/"},{"categories":["Architecture"],"content":"用户体验优化 性能优化的最终目的是让用户有更好的性能体验,所以性能优化最直接的其实是优化用户体 验。同样 500 毫秒的响应时间,如果收到全部响应数据后才开始显示给用户,相比收到部 分数据就开始显示,对用户的体验就完全不一样。同样,在等待响应结果的时候,只显示一 个空白的页面和显示一个进度条,用户感受到的性能也是完全不同的。 第一层:数据中心优化 在全球各个主要区域都部署自己 的数据中心,就近为区域用户提供服务,加快响应速度。 第二层:硬件优化 事实上,即便使用水平伸缩,在分布式集群服务器内部,依然可以使用垂直伸缩,优化服务 器的硬件能力。有时候,硬件能力的提升,对系统性能的影响是非常巨大的。 第三层:操作系统优化 不同操作系统以及操作系统内的某些特性也会对软件性能有重要影响 第四层:虚拟机优化 特别是垃圾回收,可能会导致应用程序出现巨大的卡顿。 第五层:基础组件优化 如 Web 容器,数据库连接 池,MVC 框架等等。这些基础组件的性能也会对系统性能有较大影响。 第六层:架构优化 主要有缓存、消息队列、集群。 缓存:通过从缓存读取数据,加快响应时间,减少后端计算压力,缓存主要是提升读的性 能。 消息队列:通过将数据写入消息队列,异步进行计算处理,提升系统的响应时间和处理速 度,消息队列主要是提升写的性能。 集群:将单一服务器进行伸缩,构建成一个集群完成同一种计算任务,从而提高系统在高并 发压力时候的性能。各种服务器都可以构建集群,应用集群、缓存集群、数据库集群等等。 第七层:代码优化 通过各种编程技巧和设计模式提升代码的执行效率,也是我们最能控制的一个优化手段。 此外,还可以使用线程池、连接池等对象池化技术,复用资源,减少资源的创建。 ","date":"2024-05-13","objectID":"/high_performance/:3:1","tags":["Architecture"],"title":"高性能","uri":"/high_performance/"},{"categories":["Architecture"],"content":"这篇文章展示了缓存架构相关知识.","date":"2024-05-13","objectID":"/cache/","tags":["Architecture"],"title":"缓存架构","uri":"/cache/"},{"categories":["Architecture"],"content":"通读缓存 应用程序访问通读缓存获取数据的时候,如果通读缓存有应 用程序需要的数据,那么就返回这个数据;如果没有,那么通读缓存就自己负责访问数据 源,从数据源获取数据返回给应用程序,并将这个数据缓存在自己的缓存中。这样,下次应 用程序需要数据的时候,就可以通过通读缓存直接获得数据了。 ","date":"2024-05-13","objectID":"/cache/:1:0","tags":["Architecture"],"title":"缓存架构","uri":"/cache/"},{"categories":["Architecture"],"content":"CDN 即内容分发网络。CDN 只能缓存静态数据内容 ","date":"2024-05-13","objectID":"/cache/:1:1","tags":["Architecture"],"title":"缓存架构","uri":"/cache/"},{"categories":["Architecture"],"content":"反向代理缓存 既然所有的请求都需要通过反向代 理才能到达应用服务器,那么在这里加一个缓存,尽快将数据返回给用户,而不是发送给应 用服务器,这就是反向代理缓存。 ","date":"2024-05-13","objectID":"/cache/:1:2","tags":["Architecture"],"title":"缓存架构","uri":"/cache/"},{"categories":["Architecture"],"content":"旁路缓存 应用程序访问旁路缓存获取数据的时候,如果旁路缓存中有应 用程序需要的数据,那么就返回这个数据;如果没有,就返回空(null)。应用程序需要自 己从数据源读取数据,然后将这个数据写入到旁路缓存中。这样,下次应用程序需要数据的 时候,就可以通过旁路缓存直接获得数据了。 ","date":"2024-05-13","objectID":"/cache/:2:0","tags":["Architecture"],"title":"缓存架构","uri":"/cache/"},{"categories":["Architecture"],"content":"对象缓存 一种是本地缓存,缓存和应用程序在同一个进程中 启动,使用程序的堆空间存放缓存数据。本地缓存的响应速度快,但是缓存可以使用的内存 空间相对比较小,但是对于大型互联网应用所需要缓存的数据通以 T 计,这时候就要使用 远程的分布式缓存了。 ","date":"2024-05-13","objectID":"/cache/:2:1","tags":["Architecture"],"title":"缓存架构","uri":"/cache/"},{"categories":["Architecture"],"content":"缓存注意事项 好处: 缓存的数据通常存储在内存中,距离使用数据的应用也更近一点,因此相比从硬盘上获 取,或者从远程网络上获取,它获取数据的速度更快一点,响应时间更快,性能表现更 好。 缓存的数据通常是计算结果数据,比如对象缓存中,通常存放经过计算加工的结果对 象,如果缓存不命中,那么就需要从数据库中获取原始数据,然后进行计算加工才能得 到结果对象,因此使用缓存可以减少 CPU 的计算消耗,节省计算资源,同样也加快了处 理的速度。 通过对象缓存获取数据,可以降低数据库的负载压力;通过 CDN、反向代理等通读缓存 获取数据,可以降低服务器的负载压力。这些被释放出来的计算资源,可以提供给其他 更有需要的计算场景,比如写数据的场景,间接提高整个系统的处理能力。 问题: ","date":"2024-05-13","objectID":"/cache/:3:0","tags":["Architecture"],"title":"缓存架构","uri":"/cache/"},{"categories":["Architecture"],"content":"数据脏读 解决办法: 过期失效 失效通知: 应用程序更新数据源的数据,同时发送通知,将该数据从缓存中 清除。失效通知看起来数据更新更加及时,但是实践中,更多使用的还是过期失效。 如果缓存的数据没有热点,写入缓存的数据很难被重复读取,那么使用缓存就不是很 有必要了。 ","date":"2024-05-13","objectID":"/cache/:3:1","tags":["Architecture"],"title":"缓存架构","uri":"/cache/"},{"categories":["Architecture"],"content":"这篇文章展示了数据存储架构相关知识.","date":"2024-05-13","objectID":"/data_storage/","tags":["Architecture"],"title":"数据存储架构","uri":"/data_storage/"},{"categories":["Architecture"],"content":"数据库主从复制 将 MySQL 主数据库中的数据复制到从数据库中去 原理: 当应用程序客户端发送一条更新命令到主服务器数据库的时候,数据库会 把这条更新命令同步记录到 Binlog 中,然后由另外一个线程从 Binlog 中读取这条日志, 通过远程通讯的方式将它复制到从服务器上面去。 从服务器获得这条更新日志后,将其加入到自己的 Relay Log 中,然后由另外一个 SQL 执 行线程从 Relay log 中读取这条新的日志,并把它在本地的数据库中重新执行一遍,这样当 客户端应用程序执行一个 update 命令的时候,这个命令会同时在主数据库和从数据库上执 行,从而实现了主数据库向从数据库的复制,让从数据库和主数据库保持一样的数据。 通过数据库主从复制的方式,我们可以实现数据库读写分离。写操作访问主数据库,读操作 访问从数据库,使数据库具有更强大的访问负载能力,支撑更多的用户访问。 采用一主多从的方案,当某个从数据库宕机的时候,还可以将读操作迁移到其他从数据库 上,保证读操作的高可用。但如果主数据库宕机,系统就没法使用了,因此现实中,也会采 用 MySQL 主主复制的方案。也就是说,两台服务器互相备份,任何一台服务器都会将自 己的 Binlog 复制到另一台机器的 Relay Log 中,以保持两台服务器的数据一致。 使用主主复制需要注意的是,主主复制仅仅用来提升数据写操作的可用性,并不能用来提高 写操作的性能。任何时候,系统中都只能有一个数据库作为主数据库,也就是说,所有的应 用程序都必须连接到同一个主数据库进行写操作。只有当该数据库宕机失效的时候,才会将 写操作切换到另一台主数据库上。这样才能够保证数据库数据的一致性,不会出现数据冲 突。 此外,不管是主从复制还是主主复制,都无法提升数据库的存储能力,也就是说,不管增加 多少服务器,这些服务器存储的数据都是一样的。如果数据量太大,数据库无法存下这么多 的数据,通过数据库复制是无法解决问题的。 ","date":"2024-05-13","objectID":"/data_storage/:1:0","tags":["Architecture"],"title":"数据存储架构","uri":"/data_storage/"},{"categories":["Architecture"],"content":"数据库分片 数据库主从复制无法解决数据库的存储问题,但是数据库分片技术可以解 决。也就是说,将一张表的数据分成若干片,每一片都包含了数据表中一部分的行记录,然 后每一片存储在不同的服务器上,这样一张表就存储在多台服务器上了。 最简单的数据库分片存储可以采用硬编码的方式,在程序代码中直接指定一条数据库记录要 存放到哪个服务器上。但是硬编码方式的缺点比较明显。首先,如果要增加服务器,那么就必须修改分片逻辑代 码,这样程序代码就会因为非业务需求产生不必要的变更;其次,分片逻辑耦合在处理业务 逻辑的程序代码中,修改分片逻辑或者修改业务逻辑都可能使另一部分代码因为不小心的改 动而出现 Bug。 可以通过使用分布式关系数据库中间件解决这个问题,将数据的分片逻辑在中间件 中完成,对应用程序透明。 实践中,更常见的数据库分片算法是我们所熟悉的余数 Hash 算法,根据主键 ID 和服务器 的数目进行取模计算,根据余数连接相对应的服务器。 ","date":"2024-05-13","objectID":"/data_storage/:2:0","tags":["Architecture"],"title":"数据存储架构","uri":"/data_storage/"},{"categories":["Architecture"],"content":"关系数据库的混合部署 对于数据访问和存储压力不太大,对可用性要求也不太高的系统,也许部署在单一服务器上 的数据库就可以解决,所有的应用服务器都连接访问这一台数据库服务器。 如果访问量比较大,同时对数据可用性要求也比较高,那么就需要使用数据库主从复制技 术,将数据库部署在多台服务器上。 不同的业务数据库,其数据库存储的数据和访问压力也是不同的,比如用户数据库的数据量 和访问量就可能是类目数据库的几十倍,甚至上百倍。那么这时候就可以针对用户数据库进 行数据分片,而每个分片数据库还可以继续进行主从复制或者主主复制。 ","date":"2024-05-13","objectID":"/data_storage/:3:0","tags":["Architecture"],"title":"数据存储架构","uri":"/data_storage/"},{"categories":["Architecture"],"content":"Nosql数据库 NoSQL 数据库主要用来解决大规模分布式数据的存储 问题 NoSQL 数据库面临的挑战之一是数据一致性问题。如果数据分布存储在多台服务器组成的 集群上,那么当有服务器节点失效的时候,或者服务器之间网络通信故障的时候,不同用户 读取的数据就可能会不一致。 关于分布式存储系统有一个著名的 CAP 原理,CAP 原理说:一个提供数据服务的分布式系 统无法同时满足数据一致性(Consistency)、可用性(Availability)和分区耐受性 (Partition Tolerance)这三个条件。 CAP 原理是说,当网络分区失效发生的时候,我们要么取消操作,保证数据就是一致的, 但是系统却不可用;要么继续写入数据,但是数据的一致性就得不到保证了。 对于一个分布式系统而言,网络失效一定会发生,也就是说,分区耐受性是必须要保证的, 而对于互联网应用来说,可用性也是需要保证的,分布式存储系统通常需要在一致性上做一 些妥协和增强。 Apache Cassandra 解决数据一致性的方案是,在用户写入数据的时候,将一个数据写入 集群中的三个服务器节点,等待至少两个节点响应写入成功。用户读取数据的时候,从三个 节点尝试读取数据,至少等到两个节点返回数据,并根据返回数据的时间戳,选取最新版本 的数据。这样,即使服务器中的数据不一致,但是最终用户还是能得到一个一致的数据,这 种方案也被称为最终一致性。 ","date":"2024-05-13","objectID":"/data_storage/:4:0","tags":["Architecture"],"title":"数据存储架构","uri":"/data_storage/"},{"categories":["Architecture"],"content":"这篇文章展示了搜索引擎架构相关知识.","date":"2024-05-13","objectID":"/search_engine/","tags":["Architecture"],"title":"搜索引擎架构","uri":"/search_engine/"},{"categories":["Architecture"],"content":"搜索引擎倒排索引 事实上,互联网一方面是将全世界的人和网络应用联系起来,另一方面,也将全世界的网页 通过超链接联系起来,几乎每个网页都包含了一些其他网页的超链接,这些超链接互相链 接,就让全世界的互联网构成了一个大的网络。所以,搜索引擎只需要解析这些网页,得到 里面的超链接,然后继续下载这些超链接的网页,继续解析,这样就可以得到全世界的网页 了。 ","date":"2024-05-13","objectID":"/search_engine/:1:0","tags":["Architecture"],"title":"搜索引擎架构","uri":"/search_engine/"},{"categories":["Architecture"],"content":"搜索引擎结果排序 PageRank 算法认为,如果一个网页里包含了某个网页的超链接,那么就表示该网页认可 某个网页,或者说,该网页给某个网页投了一票。 PageRank 算法对于互联网网页排序效果很好,但是,对于那些用户生成内容(UGC)的 网站而言,如果想在这些网站内部进行搜索, PageRank 算法就没什么效果了。 那么,要相对这些站内搜索引擎的结果进行排序,就需要利用其它一些信息以及算法,比如 可以利用文章获得的点赞数进行排序,点赞越多,表示越获得其它用户的认可,越应该在搜 索结果中排在前面。利用点赞数排序,或者 PageRank 排序,都是利用内容中存在的推荐 信息排序,而这些推荐信息来自于广大参与其中的人,因此这些算法实现也被称作“集体智 慧编程”。 使用词频 TF 进行排序,词频表示某个词在该文档中出现的频繁程度,也代表 了这个词和该文档的相关程度 ","date":"2024-05-13","objectID":"/search_engine/:2:0","tags":["Architecture"],"title":"搜索引擎架构","uri":"/search_engine/"},{"categories":["Architecture"],"content":"这篇文章展示了微服务架构相关知识.","date":"2024-05-13","objectID":"/micro_service/","tags":["Architecture"],"title":"微服务架构","uri":"/micro_service/"},{"categories":["Architecture"],"content":"单体架构的困难和挑战 ","date":"2024-05-13","objectID":"/micro_service/:1:0","tags":["Architecture"],"title":"微服务架构","uri":"/micro_service/"},{"categories":["Architecture"],"content":"编译、部署困难 ","date":"2024-05-13","objectID":"/micro_service/:1:1","tags":["Architecture"],"title":"微服务架构","uri":"/micro_service/"},{"categories":["Architecture"],"content":"代码分支管理困难 因为单体应用非常庞大,所以代码模块也是由多个团队共同维护的。但最后还是要编译成一 个单体应用,统一发布。这就要求把各个团队的代码 merge 在一起,这个过程很容易发生 代码冲突。而 merge 的时候又是应用要发布的时候,发布过程本来就复杂,再加上代码 merge 带来的问题,各种情况纠缠在一起,极易出错。 ","date":"2024-05-13","objectID":"/micro_service/:1:2","tags":["Architecture"],"title":"微服务架构","uri":"/micro_service/"},{"categories":["Architecture"],"content":"数据库连接耗尽 对于一个巨型的应用而言,因为有大量的用户进行访问,所以必须把应用部署到大规模的服 务器集群上。然后每个应用都需要与数据库建立连接,大量的应用服务器连接到数据库,会 对数据库的连接产生巨大的压力,某些情况下甚至会耗尽数据库的连接。 ","date":"2024-05-13","objectID":"/micro_service/:1:3","tags":["Architecture"],"title":"微服务架构","uri":"/micro_service/"},{"categories":["Architecture"],"content":"新增业务困难 因为所有的业务都耦合在一个单一的大系统里,随着时间的发展,这个系统会变得非常的复 杂,想要维护这样一个系统是非常困难和复杂的。 ","date":"2024-05-13","objectID":"/micro_service/:1:4","tags":["Architecture"],"title":"微服务架构","uri":"/micro_service/"},{"categories":["Architecture"],"content":"发布困难 ","date":"2024-05-13","objectID":"/micro_service/:1:5","tags":["Architecture"],"title":"微服务架构","uri":"/micro_service/"},{"categories":["Architecture"],"content":"微服务框架原理 在面向服务的体系架构里面,服务提供者向注册中心注册自己的服务,而服务调用者到注册 中心去发现服务,发现服务以后,根据服务注册中心提供的访问接口和访问路径对服务发起 请求,由服务的提供者完成请求,返回结果给调用者。 ","date":"2024-05-13","objectID":"/micro_service/:2:0","tags":["Architecture"],"title":"微服务架构","uri":"/micro_service/"},{"categories":["Architecture"],"content":"微服务架构的落地实践 首先明确自己的需求:我们到底想用微服务达到什么样的目的?需求清晰了,再去考虑具体 要实现的价值,再根据价值构建我们的设计原则,根据原则寻找最佳实践,最后根据实践去 选择最合适的工具。 ","date":"2024-05-13","objectID":"/micro_service/:3:0","tags":["Architecture"],"title":"微服务架构","uri":"/micro_service/"},{"categories":["Architecture"],"content":"微服务间如何通讯 ","date":"2024-05-13","objectID":"/micro_service/:4:0","tags":["Architecture"],"title":"微服务架构","uri":"/micro_service/"},{"categories":["Architecture"],"content":"从通讯协议角度考虑 Rest Api RPC MQ ","date":"2024-05-13","objectID":"/micro_service/:4:1","tags":["Architecture"],"title":"微服务架构","uri":"/micro_service/"},{"categories":["Architecture"],"content":"从通讯模式角度考虑 一对一 一对多 同步 请求响应模式,最常见 – 异步 通知/请求异步响应 发布订阅/发布异步响应 ","date":"2024-05-13","objectID":"/micro_service/:4:2","tags":["Architecture"],"title":"微服务架构","uri":"/micro_service/"},{"categories":["Architecture"],"content":"如何选择 RPC框架 I/O、线程调度模型 序列化方式 多语言支持 ","date":"2024-05-13","objectID":"/micro_service/:4:3","tags":["Architecture"],"title":"微服务架构","uri":"/micro_service/"},{"categories":["Architecture"],"content":"服务发现 ","date":"2024-05-13","objectID":"/micro_service/:5:0","tags":["Architecture"],"title":"微服务架构","uri":"/micro_service/"},{"categories":["Architecture"],"content":"客户端发现 ","date":"2024-05-13","objectID":"/micro_service/:5:1","tags":["Architecture"],"title":"微服务架构","uri":"/micro_service/"},{"categories":["Architecture"],"content":"服务端发现 ","date":"2024-05-13","objectID":"/micro_service/:5:2","tags":["Architecture"],"title":"微服务架构","uri":"/micro_service/"},{"categories":["Architecture"],"content":"服务编排 ","date":"2024-05-13","objectID":"/micro_service/:6:0","tags":["Architecture"],"title":"微服务架构","uri":"/micro_service/"},{"categories":["Architecture"],"content":"kubernetes 六边形表示的是 node节点,即 worker节点。每个工作节点上运行了kubelet服务以及 docker服务。 kubelet就相当于是该节点运行时的一个总管,他会管理这个当前 node上面运行的所有服务 deployment相当于是一个部署,圆圈相当于是一个 pod.pod是k8s概念中的最小单元 pod里边是容器。有独立的 ip地址。pod里面的容器可以共享网络以及存储的。 虚线表示 service,service也有自己独立的 ip。可对多个 pod进行负载均衡。使用 label selector来定义。 master虚线框代表的是 api server,他提供了资源操作的唯一入口,并且提供了认证、授权和访问控制。control manager负责维护集群的状态, scheduler负责资源调度。按照预定的调度策略。把 pod调度给相应的 node节点上,etcd主要用于一致性存储,保存了集群状态等等的信息。 kubelet负责维护当前节点上的容器的生命周期,也负责维护当前的节点的 volume和网络的管理。 kube-proxy负责 service提供内部的服务发现和负载均衡 kube-DNS负责整个集群的 DNS服务 dashboard提供一些集群相关的数据的展示和操作。 集群外要访问集群内部服务时,k8s可以把服务端口直接暴露在当前的 node上。直接访问 node的 ip就可以关联到这个端口 设计理念 API设计原则(申明式) 控制机设计原则 网络 CNI Flannel、Calico、Weave Pod网络 scheduler-preselect(预选规则) NodiskConflict(挂载冲突) CheckNodeMemoryPressure NodeSelector FitResource Affinity scheduler-optimize-select(优选规则) SelectorSpreadPriority LeastRequestedPriority AffinityPriority pod通讯 同一个 pod内部通讯 用localhost地址访问彼此的端口。 同一个 node,不同 pod通讯 可通过 docker0网桥进行通讯,具体可通过访问pod的ip 不同 node,不同 pod通讯 通过 pod的 ip和 node的 ip关联 服务发现 kube-proxy(ClusterIp) kube-proxy(NodePort) kube-DNS ","date":"2024-05-13","objectID":"/micro_service/:6:1","tags":["Architecture"],"title":"微服务架构","uri":"/micro_service/"},{"categories":["Architecture"],"content":"CICD和 DevOps 持续集成 持续部署 ","date":"2024-05-13","objectID":"/micro_service/:7:0","tags":["Architecture"],"title":"微服务架构","uri":"/micro_service/"},{"categories":["Architecture"],"content":"这篇文章展示了异步架构相关知识.","date":"2024-05-13","objectID":"/async/","tags":["Architecture"],"title":"异步架构","uri":"/async/"},{"categories":["Architecture"],"content":"使用消息队列实现异步架构 同步架构是说,当应用程序调用服务的时候,当前程序需要阻塞等待服务 完成,返回服务结果后才能继续向下执行。 消息队列异步架构的主要角色包括消息生产者、消息队列和消息消费者。消息生产者通常就 是主应用程序,生产者将调用请求封装成消息发送给消息队列。此外还需要开发一个专门的 消息消费者程序,用来从消息队列中获取、消费消息,由消息消费者完成业务逻辑处理。 消息队列的职责就是缓冲消息,等待消费者消费。根据消息消费方式又分为点对点模式和发 布订阅模式两种。 在点对点模式中,多个消息生产者向消息队列发送消息,多个消息消费者消费消息,每个消 息只会被一个消息消费者消费。 在发布订阅模式中,开发者可以在消息队列中设置主题,消息生产者的消息按照主题进行发 送,多个消息消费者可以订阅同一个主题,每个消费者都可以收到这个主题的消息拷贝,然 后按照自己的业务逻辑分别进行处理计算。 发布订阅模式下,一个主题可以被重复订阅,所以如果需要扩展功能,可以在对当前的生产 者和消费者都没有影响的前提下,增加新的消费者订阅同一个主题即可。 ","date":"2024-05-13","objectID":"/async/:1:0","tags":["Architecture"],"title":"异步架构","uri":"/async/"},{"categories":["Architecture"],"content":"消息队列异步架构的好处 ","date":"2024-05-13","objectID":"/async/:2:0","tags":["Architecture"],"title":"异步架构","uri":"/async/"},{"categories":["Architecture"],"content":"改善写操作请求的响应时间 使用消息队列,生产者应用程序只需要将消息发送到消息队列之后,就可以继续向下执行 了,无需等待耗时的消息消费处理 ","date":"2024-05-13","objectID":"/async/:2:1","tags":["Architecture"],"title":"异步架构","uri":"/async/"},{"categories":["Architecture"],"content":"更容易进行伸缩 ","date":"2024-05-13","objectID":"/async/:2:2","tags":["Architecture"],"title":"异步架构","uri":"/async/"},{"categories":["Architecture"],"content":"削峰填谷 利用消息队列,我们可以将需要处理的消息放入消息队列,而消费者可以控制消费 速度,因此能够降低系统访问高峰时压力,而在访问低谷的时候还可以继续消费消息队列中 未处理的消息,保持系统的资源利用率。 ","date":"2024-05-13","objectID":"/async/:2:3","tags":["Architecture"],"title":"异步架构","uri":"/async/"},{"categories":["Architecture"],"content":"隔离失败 使用消息队列,生产者发送消息到消息队列后就继续自己后面的计算,消费者如果在处理消 息的过程中失败,不会传递给生产者,应用程序具有更高的可用性。 ","date":"2024-05-13","objectID":"/async/:2:4","tags":["Architecture"],"title":"异步架构","uri":"/async/"},{"categories":["Architecture"],"content":"降低耦合 耦合会使软件僵硬、笨拙、难以维护,而使用消息队列的异步架构可以降低调用 者和被调用者的耦合。调用者发送消息到消息队列,不需要依赖被调用者的代码和处理结 果,增加新的功能,也只需要增加新的消费者就可以了。 ","date":"2024-05-13","objectID":"/async/:2:5","tags":["Architecture"],"title":"异步架构","uri":"/async/"},{"categories":["Auth"],"content":"这篇文章展示了如何使用Authentik对接金蝶云星空.","date":"2024-04-17","objectID":"/k3cloud_login/","tags":["Auth","K3Cloud"],"title":"金蝶云星空对接 authentik实现 sso登录","uri":"/k3cloud_login/"},{"categories":["Auth"],"content":"编写 proxy 服务 端口开放为:8888 逻辑: 获取authentik返回的请求头 X-authentik-username获取用户的用户名 对接金蝶第三方登录生成html5跳转链接,并进行302 跳转 username := r.Header.Get(\"X-authentik-username\") req.Username = username l := logic.NewK3cloudLogic(r.Context(), svcCtx) resp, err := l.K3cloud(\u0026req) if err != nil { httpx.ErrorCtx(r.Context(), w, err) } else { w.Header().Set(\"Location\", resp.Message) w.WriteHeader(http.StatusFound) http.RedirectHandler(resp.Message, http.StatusFound) } ... func (l *K3cloudLogic) K3cloud(req *types.Request) (resp *types.Response, err error) { //拼接 userName := req.Username if userName == \"\" { return nil, errors.New(\"invalid param\") } dbId := l.svcCtx.Config.K3Cloud.DbId appId := l.svcCtx.Config.K3Cloud.AppId appSecret := l.svcCtx.Config.K3Cloud.AppSecret timestamp := time.Now().Unix() arr := []string{dbId, userName, appId, appSecret, fmt.Sprintf(\"%d\", timestamp)} sign := GetSignature(arr) // 签名 l.Debugf(\"generate sign param:%+v result:%v\", arr, sign) arg := domain.GenerateCloudSign{ DBID: dbId, Username: userName, AppId: appId, SignedData: sign, Timestamp: fmt.Sprintf(\"%d\", timestamp), LcId: l.svcCtx.Config.K3Cloud.LcId, OriginType: \"SimPas\", EntryRole: \"\", FormId: \"\", FormType: \"\", Pkid: \"\", OtherArgs: \"\", } argJson := arg.SerializeObject() argJsonBase64 := base64.StdEncoding.EncodeToString([]byte(argJson)) html5Url := l.svcCtx.Config.K3Cloud.BaseUrl + argJsonBase64 // html5入口链接 result := types.Response{} result.Message = html5Url l.Debugf(\"generate html5 url result:%v\", html5Url) return \u0026result, nil } func GetSignature(arr []string) string { // 将数组进行排序 sort.Strings(arr) // 将数组拼接成一个字符串 arrString := strings.Join(arr, \"\") // 对拼接后的字符串进行SHA1加密 sha1Hash := sha1.New() sha1Hash.Write([]byte(arrString)) sha1Bytes := sha1Hash.Sum(nil) // 将加密后的字节转换为十六进制字符串 signature := hex.EncodeToString(sha1Bytes) return signature } ","date":"2024-04-17","objectID":"/k3cloud_login/:1:0","tags":["Auth","K3Cloud"],"title":"金蝶云星空对接 authentik实现 sso登录","uri":"/k3cloud_login/"},{"categories":["Auth"],"content":"配置 authentik proxy provider 详见 authentik proxy provider部分 配置proxy的nginx,10.2.192.4:8888/from/authentik地址是编写 proxy服务地址 location / { # Put your proxy_pass to your application here, and all the other statements you'll need proxy_pass http://10.2.192.4:8888/from/authentik; proxy_set_header Host $host; ############################## # authentik-specific config ############################## auth_request /outpost.goauthentik.io/auth/nginx; error_page 401 = @goauthentik_proxy_signin; auth_request_set $auth_cookie $upstream_http_set_cookie; add_header Set-Cookie $auth_cookie; # translate headers from the outposts back to the actual upstream auth_request_set $authentik_username $upstream_http_x_authentik_username; auth_request_set $authentik_groups $upstream_http_x_authentik_groups; auth_request_set $authentik_email $upstream_http_x_authentik_email; auth_request_set $authentik_name $upstream_http_x_authentik_name; auth_request_set $authentik_uid $upstream_http_x_authentik_uid; proxy_set_header X-authentik-username $authentik_username; proxy_set_header X-authentik-groups $authentik_groups; proxy_set_header X-authentik-email $authentik_email; proxy_set_header X-authentik-name $authentik_name; proxy_set_header X-authentik-uid $authentik_uid; } ","date":"2024-04-17","objectID":"/k3cloud_login/:2:0","tags":["Auth","K3Cloud"],"title":"金蝶云星空对接 authentik实现 sso登录","uri":"/k3cloud_login/"},{"categories":["Linux"],"content":"这篇文章展示了基本的Docker常用命令.","date":"2024-04-09","objectID":"/docker/","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"服务相关 ","date":"2024-04-09","objectID":"/docker/:1:0","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"启动 docker 服务 systemctl start docker ","date":"2024-04-09","objectID":"/docker/:1:1","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"停止 docker 服务 systemctl stop docker ","date":"2024-04-09","objectID":"/docker/:1:2","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"重启 docker 服务 systemctl restart docker ","date":"2024-04-09","objectID":"/docker/:1:3","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"查看 docker 服务状态 systemctl status docker ","date":"2024-04-09","objectID":"/docker/:1:4","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"设置开机启动 docker 服务 systemctl enable docker ","date":"2024-04-09","objectID":"/docker/:1:5","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"镜像相关 ","date":"2024-04-09","objectID":"/docker/:2:0","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"搜索镜像 docker search nginx ","date":"2024-04-09","objectID":"/docker/:2:1","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"拉取镜像 docker pull nginx ","date":"2024-04-09","objectID":"/docker/:2:2","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"构建镜像 docker build -t my_image:1.0 . ","date":"2024-04-09","objectID":"/docker/:2:3","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"查看本地镜像 docker images docker images -q//查看本地镜像 id ","date":"2024-04-09","objectID":"/docker/:2:4","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"删除本地镜像 docker rmi mysql:5.7 docker rmi `docker images -q` //全部删除本地镜像 ","date":"2024-04-09","objectID":"/docker/:2:5","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"导出镜像 docker save -o image.tar target_image:tag ","date":"2024-04-09","objectID":"/docker/:2:6","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"导入镜像 docker load -i image.tar ","date":"2024-04-09","objectID":"/docker/:2:7","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"给镜像打标签 docker tag image_id new_image_name:tag ","date":"2024-04-09","objectID":"/docker/:2:8","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"容器相关 ","date":"2024-04-09","objectID":"/docker/:3:0","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"创建容器 docker run -d --name=my_container -p 8080:8080 tomcat:latest ","date":"2024-04-09","objectID":"/docker/:3:1","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"查看容器列表 docker ps docker ps -q //查看正在运行的容器ID列表 ","date":"2024-04-09","objectID":"/docker/:3:2","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"停止容器 docker stop container ","date":"2024-04-09","objectID":"/docker/:3:3","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"启动已停止的容器 docker start my_container ","date":"2024-04-09","objectID":"/docker/:3:4","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"删除容器 docker rm my_container ","date":"2024-04-09","objectID":"/docker/:3:5","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"进入容器 docker exec [OPTIONS] CONTAINER COMMAND [ARG...] 其中,OPTIONS 可以指定一些参数,CONTAINER 是容器的名称或 ID,COMMAND 是要执行的命令,ARG 是命令的参数。 举例: docker exec -it my_container sh -c \"echo Hello World \u0026\u0026 ls -l\" ","date":"2024-04-09","objectID":"/docker/:3:6","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"查看容器信息 docker inspect my_container ","date":"2024-04-09","objectID":"/docker/:3:7","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Linux"],"content":"数据拷贝 docker cp container_id:/app/html /mnt/ //将容器中 /app/html 目录拷贝到宿主机 /mnt/ 目录中 docker cp /mnt/dist container_id:/app/ //将宿主机 /mnt/dist 目录拷贝到容器的 /app 目录中 ","date":"2024-04-09","objectID":"/docker/:3:8","tags":["Docker","Linux"],"title":"Docker常用命令","uri":"/docker/"},{"categories":["Auth"],"content":"这篇文章展示了如何使用Authentik.","date":"2024-04-08","objectID":"/authentik-use/","tags":["Auth"],"title":"Authentik使用","uri":"/authentik-use/"},{"categories":["Auth"],"content":"Proxy Provider 代理前哨设置以下用户特定的标头: X-authentik-username当前登录用户的用户名 X-authentik-groups用户所属的组,用竖线分割 X-authentik-email当前登录用户的邮箱地址 X-authentik-name当前登录用户的全名 X-authentik-uid当前登录用户的哈希标识符 应用程序特定的标头 X-authentik-meta-outpost前哨名称 X-authentik-meta-provider提供者名称 X-authentik-meta-appslug X-authentik-meta-version版本 X-Forwarded-Host客户端发送原始 Host标头 附加: 此外,您可以设置additionalHeaders组或用户的属性来设置其他标头: additionalHeaders: X-test-header: test-value ","date":"2024-04-08","objectID":"/authentik-use/:1:0","tags":["Auth"],"title":"Authentik使用","uri":"/authentik-use/"},{"categories":["Auth"],"content":"退出登录 当您在没有有效 cookie 的情况下访问域时,会自动完成登录. 使用单应用程序模式时,导航至app.domain.tld/outpost.goauthentik.io/sign_out. 使用域级模式时,导航到auth.domain.tld/outpost.goauthentik.io/sign_out,其中 auth.domain.tld 是为提供商配置的外部主机。 要注销,请导航至/outpost.goauthentik.io/sign_out。 ","date":"2024-04-08","objectID":"/authentik-use/:1:1","tags":["Auth"],"title":"Authentik使用","uri":"/authentik-use/"},{"categories":["Auth"],"content":"转发认证 Nginx server { listen 443 ssl http2; server_name _; ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem; ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key; proxy_buffers 8 16k; proxy_buffer_size 32k; location / { # proxy_pass http://localhost:5000; # proxy_set_header Host $host; # proxy_set_header ... ############################## # authentik-specific config ############################## auth_request /outpost.goauthentik.io/auth/nginx; error_page 401 = @goauthentik_proxy_signin; auth_request_set $auth_cookie $upstream_http_set_cookie; add_header Set-Cookie $auth_cookie; # translate headers from the outposts back to the actual upstream auth_request_set $authentik_username $upstream_http_x_authentik_username; auth_request_set $authentik_groups $upstream_http_x_authentik_groups; auth_request_set $authentik_email $upstream_http_x_authentik_email; auth_request_set $authentik_name $upstream_http_x_authentik_name; auth_request_set $authentik_uid $upstream_http_x_authentik_uid; proxy_set_header X-authentik-username $authentik_username; proxy_set_header X-authentik-groups $authentik_groups; proxy_set_header X-authentik-email $authentik_email; proxy_set_header X-authentik-name $authentik_name; proxy_set_header X-authentik-uid $authentik_uid; } location /outpost.goauthentik.io { proxy_pass http://outpost.company:9000/outpost.goauthentik.io; # 确保此虚拟服务器的主机与您配置的外部 URL 匹配 proxy_set_header Host $host; proxy_set_header X-Original-URL $scheme://$http_host$request_uri; add_header Set-Cookie $auth_cookie; auth_request_set $auth_cookie $upstream_http_set_cookie; proxy_pass_request_body off; proxy_set_header Content-Length \"\"; } location @goauthentik_proxy_signin { internal; add_header Set-Cookie $auth_cookie; return 302 /outpost.goauthentik.io/start?rd=$request_uri; } } Nginx代理服务器 proxy_buffers 8 16k; proxy_buffer_size 32k; port_in_redirect off; location / { # Put your proxy_pass to your application here proxy_pass $forward_scheme://$server:$port; # Set any other headers your application might need # proxy_set_header Host $host; # proxy_set_header ... ############################## # authentik-specific config ############################## auth_request /outpost.goauthentik.io/auth/nginx; error_page 401 = @goauthentik_proxy_signin; auth_request_set $auth_cookie $upstream_http_set_cookie; add_header Set-Cookie $auth_cookie; # translate headers from the outposts back to the actual upstream auth_request_set $authentik_username $upstream_http_x_authentik_username; auth_request_set $authentik_groups $upstream_http_x_authentik_groups; auth_request_set $authentik_email $upstream_http_x_authentik_email; auth_request_set $authentik_name $upstream_http_x_authentik_name; auth_request_set $authentik_uid $upstream_http_x_authentik_uid; proxy_set_header X-authentik-username $authentik_username; proxy_set_header X-authentik-groups $authentik_groups; proxy_set_header X-authentik-email $authentik_email; proxy_set_header X-authentik-name $authentik_name; proxy_set_header X-authentik-uid $authentik_uid; } location /outpost.goauthentik.io { proxy_pass http://outpost.company:9000/outpost.goauthentik.io; # 确保此虚拟服务器的主机与您配置的外部 URL 匹配 proxy_set_header Host $host; proxy_set_header X-Original-URL $scheme://$http_host$request_uri; add_header Set-Cookie $auth_cookie; auth_request_set $auth_cookie $upstream_http_set_cookie; proxy_pass_request_body off; proxy_set_header Content-Length \"\"; } location @goauthentik_proxy_signin { internal; add_header Set-Cookie $auth_cookie; return 302 /outpost.goauthentik.io/start?rd=$request_uri; } ","date":"2024-04-08","objectID":"/authentik-use/:1:2","tags":["Auth"],"title":"Authentik使用","uri":"/authentik-use/"},{"categories":["Auth"],"content":"Oauth2 Provider ","date":"2024-04-08","objectID":"/authentik-use/:2:0","tags":["Auth"],"title":"Authentik使用","uri":"/authentik-use/"},{"categories":["Auth"],"content":"前期准备 配置 authentik的oauth2 provider 重定向URI填写的是 portainer的主页地址 配置 application 选择新增好的provider ","date":"2024-04-08","objectID":"/authentik-use/:2:1","tags":["Auth"],"title":"Authentik使用","uri":"/authentik-use/"},{"categories":["Auth"],"content":"配置portainer 我们使用 portainer作为演示 安装 portainer 这里我们使用 docker进行安装 docker volume create portainer_data docker run -d -p 9000:9000 --name=portainer --restart=unless-stopped -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce 登录portainer平台、 在浏览器中访问 http://localhost:9000 来登录 Portainer,首次使用,需要为 admin用户设置密码。 点击设置里的认证模块 配置对应的配置项 登出 portainer,出现 Login With Oauth 点击Login With Oauth,会跳转到 authentik页面进行授权,授权通过后就能登录到 portainer首页 ","date":"2024-04-08","objectID":"/authentik-use/:2:2","tags":["Auth"],"title":"Authentik使用","uri":"/authentik-use/"},{"categories":["Auth"],"content":"LDAP Provider ","date":"2024-04-08","objectID":"/authentik-use/:3:0","tags":["Auth"],"title":"Authentik使用","uri":"/authentik-use/"},{"categories":["Auth"],"content":"前期准备 新增 ldap flow 配置 authentik的 ldap provider、 application 配置 authentik ","date":"2024-04-08","objectID":"/authentik-use/:3:1","tags":["Auth"],"title":"Authentik使用","uri":"/authentik-use/"},{"categories":["Auth"],"content":"测试 ldapsearch ldapsearch -x -H ldap://ladp_addr:389 -D 'cn=akadmin,ou=users,dc=ldap,dc=goauthentik,dc=io' -b 'ou=users,dc=ldap,dc=goauthentik,dc=io' -w 'xxx@123' \"cn=akadmin\" jumpserver 我们使用 jumpserver作为演示ldap登录 注意 安装 jumpserver,详见docker安装 jumpserver 配置完之后就能用 authentik用户登录 jumpserver了 ","date":"2024-04-08","objectID":"/authentik-use/:3:2","tags":["Auth"],"title":"Authentik使用","uri":"/authentik-use/"},{"categories":["DB"],"content":"这篇文章展示了基本的Postgresql常用命令.","date":"2024-04-07","objectID":"/postgresql/","tags":["DB"],"title":"Postgresql常用命令","uri":"/postgresql/"},{"categories":["DB"],"content":"查看 pg版本 psql --version ","date":"2024-04-07","objectID":"/postgresql/:0:1","tags":["DB"],"title":"Postgresql常用命令","uri":"/postgresql/"},{"categories":["DB"],"content":"获取当前db中所有的表信息 select * from pg_tables; //获取名为 public的 schema的所有表 select tablename from pg_tables where schemaname='public' ","date":"2024-04-07","objectID":"/postgresql/:0:2","tags":["DB"],"title":"Postgresql常用命令","uri":"/postgresql/"},{"categories":["DB"],"content":"获取某个表结构信息 \\d authentik_core_user; ","date":"2024-04-07","objectID":"/postgresql/:0:3","tags":["DB"],"title":"Postgresql常用命令","uri":"/postgresql/"},{"categories":["DB"],"content":"获取某个表数据 SELECT * FROM authentik_core_user; ","date":"2024-04-07","objectID":"/postgresql/:0:4","tags":["DB"],"title":"Postgresql常用命令","uri":"/postgresql/"},{"categories":["DB"],"content":"外网不能访问数据库 ::: 1、修改postgresql.conf文件::: 在安装目录下data/postgresql.confi文件中将listen address改为* listen_addresses = '*' ::: 2、修改pg_hba.conf文件::: 在data/pg_hba.conf中设置 host all all 127.0.0.1/32 md5 host all all 0.0.0.0/0 md5 ::: 3、重启 postgres服务::: systemctl restart postgresql ","date":"2024-04-07","objectID":"/postgresql/:0:5","tags":["DB"],"title":"Postgresql常用命令","uri":"/postgresql/"},{"categories":["DB"],"content":"新建数据库 CREATE DATABASE dbname; ","date":"2024-04-07","objectID":"/postgresql/:0:6","tags":["DB"],"title":"Postgresql常用命令","uri":"/postgresql/"},{"categories":["Auth"],"content":"这篇文章展示了authentik安装说明.","date":"2024-04-02","objectID":"/authentik-deploy/","tags":["Auth"],"title":"Authentik安装","uri":"/authentik-deploy/"},{"categories":["Auth"],"content":"Docker安装 MacOs 下载 docker composer文件 curl -O https://goauthentik.io/docker-compose.yml 生成 password和 secret key到.env文件 echo \"PG_PASS=$(openssl rand -base64 36)\" \u003e\u003e .env echo \"AUTHENTIK_SECRET_KEY=$(openssl rand -base64 36)\" \u003e\u003e .env 如果只要开启错误日志,运行 echo \"AUTHENTIK_ERROR_REPORTING__ENABLED=true\" \u003e\u003e .env 邮件配置(可选不推荐) # SMTP Host Emails are sent to AUTHENTIK_EMAIL__HOST=localhost AUTHENTIK_EMAIL__PORT=25 # Optionally authenticate (don't add quotation marks to your password) AUTHENTIK_EMAIL__USERNAME= AUTHENTIK_EMAIL__PASSWORD= # Use StartTLS AUTHENTIK_EMAIL__USE_TLS=false # Use SSL AUTHENTIK_EMAIL__USE_SSL=false AUTHENTIK_EMAIL__TIMEOUT=10 # Email address authentik will send from, should have a correct @domain AUTHENTIK_EMAIL__FROM=authentik@localhost 配置端口号 COMPOSE_PORT_HTTP=80 COMPOSE_PORT_HTTPS=443 开启 docker compose pull docker compose up -d 初始化配置 http://\u003cyour server's IP or hostname\u003e/if/flow/initial-setup/ ","date":"2024-04-02","objectID":"/authentik-deploy/:1:0","tags":["Auth"],"title":"Authentik安装","uri":"/authentik-deploy/"},{"categories":["Auth"],"content":"二进制安装 确定 linux系统的架构 uname -m 如果显示x86_64则是 amd架构 不是则是 arm,也可通过 arch命令得到 2. 前期准备 安装依赖 安装 lib库 sudo yum update -y \u0026\u0026 sudo yum upgrade -y sudo yum install -y curl wget git gcc gcc-c++ sqlite-devel readline-devel ncurses-devel openssl tk-devel gdbm-d evel db4-devel xz-devel make glibc-devel bzip2-devel pkgconfig libffi-devel libpcap-devel zlib-devel xmlsec1 xmlsec1-openssl libmaxminddb postgresql-devel 确定 devel库是否开启 安装 python(要求版本 v3.12+) wget https://www.python.org/ftp/python/3.12.2/Python-3.12.2.tgz tar xzf Python-3.12.2.tgz cd Python-3.12.2 ./configure --enable-optimizations sudo make altinstall rm -rf Python-3.12.2.tgz Python-3.12.2 安装 node(要求版本 v18+) wget https://nodejs.org/dist/latest-v21.x/node-v21.7.1-linux-x64.tar.xz tar xf node-v21.7.1-linux-x64.tar.xz 配置环境变量 安装 go(要求 v1.22+) wget https://golang.org/dl/go1.22.1.linux-amd64.tar.gz sudo tar -C /usr/local -xzf go1.22.1.linux-amd64.tar.gz rm -rf go1.22.1.linux-amd64.tar.gz vim ~/.profile export GOROOT=/usr/local/go export PATH=$PATH:$GOROOT/bin source ~/.profile 安装 pip curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py python get-pip.py rm -rf get-pip.py 安装 py virtualenv pip install virtualenv black ruff codespell bandit poetry pycparser psycopg2 xmlsec1 无网模式需要设置pypi代理 安装 golangci-lint ","date":"2024-04-02","objectID":"/authentik-deploy/:2:0","tags":["Auth"],"title":"Authentik安装","uri":"/authentik-deploy/"},{"categories":["Auth"],"content":"下载https://github.com/golangci/golangci-lint/releases对应 rpm sudo rpm -ivh golangci-lint-1.57.2-linux-386.rpm 安装 website、 web和 python依赖 cd /opt/authentik make install 安装 postgreSql ","date":"2024-04-02","objectID":"/authentik-deploy/:2:1","tags":["Auth"],"title":"Authentik安装","uri":"/authentik-deploy/"},{"categories":["Auth"],"content":"下载路径:https://www.postgresql.org/ftp/source/v16.2/ tar -zxvf postgresql-16.2.tar.gz cd postgresql-16.2 ./configure make \u0026 make install 安装 redis wget http://download.redis.io/releases/redis-4.0.8.tar.gz tar xzvf redis-4.0.8.tar.gz cd redis-4.0.8 make cd src make install PREFIX=/usr/local/redis cd ../ mkdir /usr/local/redis/etc mv redis.conf /usr/local/redis/etc vi /usr/local/redis/etc/redis.conf //将daemonize no改成daemonize yes vi /etc/rc.local //在里面添加内容:/usr/local/redis/bin/redis-server /usr/local/redis/etc/redis.conf /usr/local/redis/bin/redis-server /usr/local/redis/etc/redis.conf cp /usr/local/redis/bin/redis-server /usr/local/bin/ cp /usr/local/redis/bin/redis-cli /usr/local/bin/ 设置环境变量PATH: export PATH=\"/home/admin/opt/authentik/.venv/bin\":$PATH export PATH=\"/home/admin/opt/authentik/lifecycle\":$PATH 启动 authentik服务 ak命令为 lifecycle目录下的可执行文件 ak server ak worker ","date":"2024-04-02","objectID":"/authentik-deploy/:2:2","tags":["Auth"],"title":"Authentik安装","uri":"/authentik-deploy/"},{"categories":["Auth"],"content":"使用 初始化管理员账号密码 打开初始化页面 https://{{your host}}/if/flow/initial-setup/,设置邮箱密码 访问管理后台 打开管理后台 https://{{your host}}/if/admin/#/administration/overview ","date":"2024-04-02","objectID":"/authentik-deploy/:3:0","tags":["Auth"],"title":"Authentik安装","uri":"/authentik-deploy/"},{"categories":["Linux"],"content":"这篇文章展示了基本的Kubectl常用命令.","date":"2024-04-01","objectID":"/kubectl/","tags":["Linux","K8s"],"title":"Kubectl常用命令","uri":"/kubectl/"},{"categories":["Linux"],"content":"配置及上下文 view kubectl config view # 显示合并后的 kubeconfig 配置 current-context kubectl config current-context # 显示当前的上下文 ","date":"2024-04-01","objectID":"/kubectl/:0:1","tags":["Linux","K8s"],"title":"Kubectl常用命令","uri":"/kubectl/"},{"categories":["Linux"],"content":"创建对象 kubectl create -f ./my-manifest.yaml # 创建资源 kubectl create -f https://git.io/vPieo # 使用 url 来创建资源 ","date":"2024-04-01","objectID":"/kubectl/:0:2","tags":["Linux","K8s"],"title":"Kubectl常用命令","uri":"/kubectl/"},{"categories":["Linux"],"content":"显示和查找资源 kubectl get services # 创建资源 kubectl get pods -o wide -n namespace # 列出所有 pod 并显示详细信息 kubectl create -f https://git.io/vPieo # 使用 url 来创建资源 ","date":"2024-04-01","objectID":"/kubectl/:0:3","tags":["Linux","K8s"],"title":"Kubectl常用命令","uri":"/kubectl/"},{"categories":["Linux"],"content":"scale资源 kubectl scale --replicas=3 rs/foo ","date":"2024-04-01","objectID":"/kubectl/:0:4","tags":["Linux","K8s"],"title":"Kubectl常用命令","uri":"/kubectl/"},{"categories":["Linux"],"content":"删除资源 kubectl delete pods,services -l name=myLabel ","date":"2024-04-01","objectID":"/kubectl/:0:5","tags":["Linux","K8s"],"title":"Kubectl常用命令","uri":"/kubectl/"},{"categories":["Linux"],"content":"这篇文章展示了基本的Linux常用命令.","date":"2024-03-19","objectID":"/command/","tags":["Linux"],"title":"Linux常用命令","uri":"/command/"},{"categories":["Linux"],"content":"删除多行 1.首先要显示对应的行数 :set nu 2.Esc退出 190,6233d 即[190 , 6233]都删除掉 ","date":"2024-03-19","objectID":"/command/:0:1","tags":["Linux"],"title":"Linux常用命令","uri":"/command/"},{"categories":["Linux"],"content":"清空文件 \u003e log.txt ","date":"2024-03-19","objectID":"/command/:0:2","tags":["Linux"],"title":"Linux常用命令","uri":"/command/"},{"categories":["Linux"],"content":"显示发行版本信息 lsb_release -v 显示版本信息。 -i 显示发行版的id。 -d 显示该发行版的描述信息。 -r 显示当前系统是发行版的具体版本号。 -c 发行版代号。 -a 显示上面的所有信息。 -h 显示帮助信息。 ","date":"2024-03-19","objectID":"/command/:0:3","tags":["Linux"],"title":"Linux常用命令","uri":"/command/"},{"categories":["Linux"],"content":"查看文件时间 Stat stat install.log Ls modification time(mtime,修改时间):当该文件的“内容数据”更改时,就会更新这个时间。内容数据指的是文件的内容,而不是文件的属性。 status time(ctime,状态时间):当该文件的”状态(status)”改变时,就会更新这个时间,举例来说,更改了权限与属性,就会更新这个时间。 access time(atime,存取时间):当“取用文件内容”时,就会更新这个读取时间。举例来说,使用cat去读取 ~/.bashrc,就会更新atime了。 ls -l --time=ctime install.log ","date":"2024-03-19","objectID":"/command/:0:4","tags":["Linux"],"title":"Linux常用命令","uri":"/command/"},{"categories":["Linux"],"content":"修改文件时间 Touch 同时修改文件的修改时间和访问时间 touch -d \"2010-05-31 08:10:30\" install.log 只修改文件的修改时间 touch -m -d \"2010-05-31 08:10:30\" install.log 只修改文件的访问时间 touch -a -d \"2010-05-31 08:10:30\" install.log ","date":"2024-03-19","objectID":"/command/:0:5","tags":["Linux"],"title":"Linux常用命令","uri":"/command/"},{"categories":["Linux"],"content":"开启 devel库 Linux vim /etc/yum.repos.d/rocky-devel.repo //确认是否开启 yum grouplist yum group install '开发工具' Ubuntu/Debian sudo apt update sudo apt install build-essential ","date":"2024-03-19","objectID":"/command/:0:6","tags":["Linux"],"title":"Linux常用命令","uri":"/command/"},{"categories":["Linux"],"content":"查看进程 lsof -i:端口号 netstat -nap | grep 端口号 ","date":"2024-03-19","objectID":"/command/:0:7","tags":["Linux"],"title":"Linux常用命令","uri":"/command/"},{"categories":["Linux"],"content":"查看磁盘使用情况 du -f ","date":"2024-03-19","objectID":"/command/:0:8","tags":["Linux"],"title":"Linux常用命令","uri":"/command/"},{"categories":["Linux"],"content":"抓包 tcpdump -D //列出可用的网卡列表 tcpdump -i eth0 //设置抓取的网卡名 tcpdump -i eth0 -w debug.cap //把捕获的包数据写入到文件中 tcpdump -A //以 ASCII 码方式显示每一个数据包 tcpdump -c 10 //设置抓取到多少个包后退出 tcpdump -w data.pcap port 80//将包数据写入文件 指定监听端口为 80 ","date":"2024-03-19","objectID":"/command/:0:9","tags":["Linux"],"title":"Linux常用命令","uri":"/command/"},{"categories":["Seo"],"content":"这篇文章展示了网站优化之 sitemap网站地图写法.","date":"2024-03-18","objectID":"/optimize_web/","tags":[" Seo"],"title":"网站优化","uri":"/optimize_web/"},{"categories":["Seo"],"content":"定义 网站地图(sitemap.xml)是一个网站的概览。搜索引擎在此文件中得到网站上存在的可抓取的网页,提交sitemap有利于搜索引擎的收录。 网站地图是一个网站的缩影,包含网站的内容地址,是根据网站的结构、框架、内容,生成的导航文件。网站地图分为三种文件格式:xml格式、html格式以及txt格式。xml格式和txt格式一般用于搜索引擎,为搜索引擎蜘蛛程序提供便利的入口到你的网站所有网页;html格式网站地图可以作为一个网页展示给访客,方便用户查看网站内容。 ","date":"2024-03-18","objectID":"/optimize_web/:0:1","tags":[" Seo"],"title":"网站优化","uri":"/optimize_web/"},{"categories":["Seo"],"content":"生成方法 Sitemap 0.90 是依据创意公用授权-相同方式共享的条款提供的,并被广泛采用,受 Google、Yahoo! 和 Microsoft 在内的众多厂商的支持。[网址](https://www. sitemaps.org/zh_CN/faq.html) \u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e \u003curlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd\"\u003e \u003curl\u003e \u003cloc\u003ehttp://www.zwp.com/\u003c/loc\u003e \u003cchangefreq\u003edaily\u003c/changefreq\u003e \u003c/url\u003e \u003curl\u003e \u003cloc\u003ehttp://www.zwp.com/wyblog?catid=43\u003c/loc\u003e \u003cchangefreq\u003edaily\u003c/changefreq\u003e \u003c/url\u003e \u003curl\u003e \u003cloc\u003ehttp://www.zwp.com/wyblog/?catid=167\u003c/loc\u003e \u003cchangefreq\u003edaily\u003c/changefreq\u003e \u003c/url\u003e \u003curl\u003e \u003cloc\u003ehttp://www.zwp.com/wyblog/?catid=170\u003c/loc\u003e \u003cchangefreq\u003edaily\u003c/changefreq\u003e \u003c/url\u003e \u003c/urlset\u003e ","date":"2024-03-18","objectID":"/optimize_web/:0:2","tags":[" Seo"],"title":"网站优化","uri":"/optimize_web/"},{"categories":["Seo"],"content":"提交步骤 生成网站地图: 网页版:http://www.xml-sitemaps.com/ 客户端:http://cn.sitemapx.com/ change frequency:指的是频率,地图的自动更新频率,默认每天(daily); last modification:是网站地图最后修改时间,默认使用服务器的响应(Use server’s response); priority:权重-可自动计算。 点击start开始生产。自动跳转到生成页面,稍等一段时间便可生成(时间和网站内容多少有关)。 它提供多种格式的网站地图文件下载(xml、xml.gz、ror.xml、html等),看你所提交的搜索引擎需要哪种格式的地图文件,就下载哪一个,或者直接打包全下载。 下载生成的地图文件sitemap.xml并上传至网站根目录 到站长平台提交网站地图。 注意 各搜索引擎推荐网站地图格式: Google:建议使用xml格式的网站地图 Google提交地址 Yahoo: 建议使用Txt格式的网站地图 Yahoo提交地址 Baidu:建议使用robots.txt提交html格式的网站地图 Baidu提交地址 在robots.txt爬虫协议中定义(Allow: /sitemap.hxml)robots.txt文档是表式允许蜘蛛网访问网站地图。爬虫协议写法参见:网站优化之robots.txt爬虫协议的写法。 ","date":"2024-03-18","objectID":"/optimize_web/:0:3","tags":[" Seo"],"title":"网站优化","uri":"/optimize_web/"},{"categories":["Redis"],"content":"这篇文章展示了Go-redis高级用法.","date":"2024-03-17","objectID":"/go-redis/","tags":["Redis"],"title":"Go-redis高级用法","uri":"/go-redis/"},{"categories":["Redis"],"content":"开启对 Cluster中 Slave Node的访问 在一个负载比较高的Redis Cluster中,如果允许对slave节点进行读操作将极大的提高集群的吞吐能力。 开启对Slave 节点的访问,受以下3个参数的影响 type ClusterOptions struct { ReadOnly bool RouteByLatency bool RouteRandomly bool ... } go-redis 选择节点的逻辑如下 func (c *ClusterClient) cmdSlotAndNode(cmd Cmder) (int, *clusterNode, error) { state, err := c.state.Get() if err != nil { return 0, nil, err } cmdInfo := c.cmdInfo(cmd.Name()) slot := cmdSlot(cmd, cmdFirstKeyPos(cmd, cmdInfo)) if c.opt.ReadOnly \u0026\u0026 cmdInfo != nil \u0026\u0026 cmdInfo.ReadOnly { if c.opt.RouteByLatency { node, err := state.slotClosestNode(slot) return slot, node, err } if c.opt.RouteRandomly { node := state.slotRandomNode(slot) return slot, node, nil } node, err := state.slotSlaveNode(slot) return slot, node, err } node, err := state.slotMasterNode(slot) return slot, node, err } 如果ReadOnly = true,只选择Slave Node 如果ReadOnly = true 且 RouteByLatency = true 将从slot对应的Master Node 和 Slave Node选择,选择策略为: 选择PING 延迟最低的节点 如果ReadOnly = true 且 RouteRandomly = true 将从slot对应的Master Node 和 Slave Node选择,选择策略为:随机选择 ","date":"2024-03-17","objectID":"/go-redis/:0:1","tags":["Redis"],"title":"Go-redis高级用法","uri":"/go-redis/"},{"categories":["Redis"],"content":"在集群模式下使用pipeline功能 Redis的pipeline功能的原理是 Client通过一次性将多条redis命令发往Redis Server,减少了每条命令分别传输的IO开销。同时减少了系统调用的次数,因此提升了整体的吞吐能力。 我们在主-从模式的Redis中,pipeline功能应该用的很多,但是Cluster模式下,估计还没有几个人用过。 我们知道 redis cluster 默认分配了 16384 个slot,当我们set一个key 时,会用CRC16算法来取模得到所属的slot,然后将这个key 分到哈希槽区间的节点上,具体算法就是:CRC16(key) % 16384。如果我们使用pipeline功能,一个批次中包含的多条命令,每条命令涉及的key可能属于不同的slot go-redis 为了解决这个问题, 分为3步 源码可以阅读 defaultProcessPipeline 将计算command 所属的slot, 根据slot选择合适的Cluster Node 将同一个Cluster Node 的所有command,放在一个批次中发送(并发操作) 接收结果 注意 注意:这里go-redis 为了处理简单,每个command 只能涉及一个key, 否则你可能会收到如下错误err CROSSSLOT Keys in request don't hash to the same slot go-redis不支持类似 MGET 命令的用法 package main import ( \"github.com/go-redis/redis\" \"fmt\" ) func main() { client := redis.NewClusterClient(\u0026redis.ClusterOptions{ Addrs: []string{\"192.168.120.110:6380\", \"192.168.120.111:6380\"}, ReadOnly: true, RouteRandomly: true, }) pipe := client.Pipeline() pipe.HGetAll(\"1371648200\") pipe.HGetAll(\"1371648300\") pipe.HGetAll(\"1371648400\") cmders, err := pipe.Exec() if err != nil { fmt.Println(\"err\", err) } for _, cmder := range cmders { cmd := cmder.(*redis.StringStringMapCmd) strMap, err := cmd.Result() if err != nil { fmt.Println(\"err\", err) } fmt.Println(\"strMap\", strMap) } } ","date":"2024-03-17","objectID":"/go-redis/:0:2","tags":["Redis"],"title":"Go-redis高级用法","uri":"/go-redis/"},{"categories":["Nginx"],"content":"这篇文章展示了 Nginx Error Page配置.","date":"2024-03-02","objectID":"/nginx_config/","tags":["Nginx"],"title":"Nginx Error Page配置","uri":"/nginx_config/"},{"categories":["Nginx"],"content":"语法 error_page code [ code... ] [ = | =answer-code ] uri |@named_location ","date":"2024-03-02","objectID":"/nginx_config/:0:1","tags":["Nginx"],"title":"Nginx Error Page配置","uri":"/nginx_config/"},{"categories":["Nginx"],"content":"示例 作用是当发生错误的时候能够显示一个预定义的uri error_page 502 503 /50x.html; location = /50x.html { root /usr/share/nginx/html; } 这样实际上产生了一个内部跳转(internal redirect),当访问出现502、503的时候就能返回50x.html中的内容,这里需要注意是否可以找到50x.html页面,所以加了个location保证找到你自定义的50x页面。 同时我们也可以自己定义这种情况下的返回状态吗,比如: error_page 502 503 =200 /50x.html; location = /50x.html { root /usr/share/nginx/html; } 这样用户访问产生502 、503的时候给用户的返回状态是200,内容是50x.html。 当error_page后面跟的不是一个静态的内容的话,比如是由proxyed server或者FastCGI/uwsgi/SCGI server处理的话,server返回的状态(200, 302, 401 或者 404)也能返回给用户。 error_page 404 = /404.php; location ~ \\.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } 也可以设置一个named location,然后在里边做对应的处理。 error_page 500 502 503 504 @jump_to_error; location @jump_to_error { proxy_pass http://backend; } 同时也能够通过使客户端进行302、301等重定向的方式处理错误页面,默认状态码为302。 error_page 403 http://example.com/forbidden.html; error_page 404 =301 http://example.com/notfound.html; 同时error_page在一次请求中只能响应一次,对应的nginx有另外一个配置可以控制这个选项:recursive_error_pages 默认为false,作用是控制error_page能否在一次请求中触发多次。 ","date":"2024-03-02","objectID":"/nginx_config/:0:2","tags":["Nginx"],"title":"Nginx Error Page配置","uri":"/nginx_config/"},{"categories":["Nginx"],"content":"Nginx 自定义404错误页面配置中有无等号的区别 error_page 404 /404.html 可显示自定义404页面内容,正常返回404状态码。 error_page 404 = /404.html 可显示自定义404页面内容,但返回200状态码。 error_page 404 /404.php 如果是动态404错误页面,包含 header 代码(例如301跳转),将无法正常执行。正常返回404代码。 error_page 404 = /404.php 如果是动态404错误页面,包含 header 代码(例如301跳转),加等号配置可以正常执行,返回php中定义的状态码。但如果php中定义返回404状态码,404状态码可以正常返回,但无法显示自定义页面内容(出现系统默认404页面),这种情况可以考虑用410代码替代( header(“HTTP/1.1 410 Gone”); 正常返回410状态码,且可正常显示自定义内容)。 注意 由于在nginx配置中,设置了limit_req的流量限制,导致许多请求返回503错误代码,在限流的条件下,为提高用户体验,希望返回正常Code 200,且返回操作频繁的信息: location /test { ... limit_req zone=zone_ip_rm burst=1 nodelay; error_page 503 =200 /dealwith_503?callback=$arg_callback; } location /dealwith_503{ set $ret_body '{\"code\": \"V00006\",\"msg\": \"操作太频繁了,请坐下来喝杯茶。\"}'; if ( $arg_callback != \"\" ) { return 200 'try{$arg_callback($ret_body)}catch(e){}'; } return 200 $ret_body; } ","date":"2024-03-02","objectID":"/nginx_config/:0:3","tags":["Nginx"],"title":"Nginx Error Page配置","uri":"/nginx_config/"},{"categories":["Nginx"],"content":"这篇文章展示了 Nginx配置.","date":"2024-03-02","objectID":"/nginx_error_page/","tags":["Nginx"],"title":"Nginx配置","uri":"/nginx_error_page/"},{"categories":["Nginx"],"content":"location 语法 规则 location [=|~|~*|^~] /uri/ { ····· } = 开头表示精确匹配 ^~ 开头表示uri以某个常规字符串开头,理解为匹配 url路径即可。nginx不对url做编码,因此请求为/static/20%/aa,可以被规则^~ /static/ /aa匹配到(注意是空格) ~ 开头表示区分大小写的正则匹配 ~* 开头表示不区分大小写的正则匹配 !~ 区分大小写不匹配的正则 !~* 不区分大小写不匹配的正则 / 通用匹配,任何请求都会匹配到 匹配顺序: 首先匹配 “=\",其次匹配 “^~”, 其次是按文件中顺序的正则匹配,最后是交给 “/” 通用匹配。 当有匹配成功时候,停止匹配,按当前匹配规则处理请求 ","date":"2024-03-02","objectID":"/nginx_error_page/:0:1","tags":["Nginx"],"title":"Nginx配置","uri":"/nginx_error_page/"},{"categories":["Nginx"],"content":"rewrite 语法 规则: last – 基本上都用这个 Flag break – 中止 Rewirte,不在继续匹配 redirect – 返回临时重定向的HTTP状态302 permanent – 返回永久重定向的HTTP状态301 可用来判断的表达式 -f 和 !-f 用来判断是否存在文件 -d 和 !-d 用来判断是否存在目录 -e 和 !-e 用来判断是否存在文件或目录 -x 和 !-x 用来判断文件是否可执行 可用来判断的全局变量 例:http://localhost:88/test1/test2/test.php $host:localhost $server_port:88 $request_uri:http://localhost:88/test1/test2/test.php $document_uri:/test1/test2/test.php $document_root:D:\\nginx/html $request_filename:D:\\nginx/html/test1/test2/test.php ","date":"2024-03-02","objectID":"/nginx_error_page/:0:2","tags":["Nginx"],"title":"Nginx配置","uri":"/nginx_error_page/"},{"categories":["Nginx"],"content":"redirect 语法 server { listen 80; server_name start.igrow.cn; index index.html index.php; root html; if ($http_host !~ “^star\\.igrow\\.cn$\u0026quot { rewrite ^(.*) http://star.igrow.cn$1 redirect; } } ","date":"2024-03-02","objectID":"/nginx_error_page/:0:3","tags":["Nginx"],"title":"Nginx配置","uri":"/nginx_error_page/"},{"categories":["Nginx"],"content":"防盗链语法 location ~* \\.(gif|jpg|swf)$ { valid_referers none blocked start.igrow.cn sta.igrow.cn; if ($invalid_referer) { rewrite ^/ http://$host/logo.png; } } ","date":"2024-03-02","objectID":"/nginx_error_page/:0:4","tags":["Nginx"],"title":"Nginx配置","uri":"/nginx_error_page/"},{"categories":["Nginx"],"content":"根据文件类型设置过期时间 location ~* \\.(js|css|jpg|jpeg|gif|png|swf)$ { if (-f $request_filename) { expires 1h; break; } } ","date":"2024-03-02","objectID":"/nginx_error_page/:0:5","tags":["Nginx"],"title":"Nginx配置","uri":"/nginx_error_page/"},{"categories":["Nginx"],"content":"禁止访问某个目录 location ~* \\.(txt|doc)${ root /data/www/wwwroot/linuxtone/test; deny all; } ","date":"2024-03-02","objectID":"/nginx_error_page/:0:6","tags":["Nginx"],"title":"Nginx配置","uri":"/nginx_error_page/"},{"categories":["Mysql"],"content":"这篇文章展示了Mysql用法.","date":"2024-02-17","objectID":"/mysql/","tags":["Mysql"],"title":"Mysql用法","uri":"/mysql/"},{"categories":["Mysql"],"content":"导出 mysqldump -uroot -pdbpasswd dbname test1 test2 test3\u003edb.sql ","date":"2024-02-17","objectID":"/mysql/:0:1","tags":["Mysql"],"title":"Mysql用法","uri":"/mysql/"},{"categories":["Mysql"],"content":"合并 表达式 COALESCE是一个函数, (expression_1, expression_2, …,expression_n)依次参考各参数表达式,遇到非null值即停止并返回该值。如果所有的表达式都是空值,最终将返回一个空值。使用COALESCE在于大部分包含空值的表达式最终将返回空值。 示例: select coalesce(success_cnt, 1) from tableA 当success_cnt 为null值的时候,将返回1,否则将返回success_cnt的真实值 select coalesce(success_cnt,period,1) from tableA 当success_cnt不为null,那么无论period是否为null,都将返回success_cnt的真实值(因为success_cnt是第一个参数),当success_cnt为null,而period不为null的时候,返回period的真实值。只有当success_cnt和period均为null的时候,将返回1。 ","date":"2024-02-17","objectID":"/mysql/:0:2","tags":["Mysql"],"title":"Mysql用法","uri":"/mysql/"},{"categories":["Mysql"],"content":"强制索引 force index select customer,count(1) c from upv_1 force index(idx_created) where created between \"2015-07-06\" and \"2015-07-07\" group by customer having c \u003e 15 order by c desc ","date":"2024-02-17","objectID":"/mysql/:0:3","tags":["Mysql"],"title":"Mysql用法","uri":"/mysql/"},{"categories":["Mysql"],"content":"重建索引 alter table T engine=InnoDB。 ","date":"2024-02-17","objectID":"/mysql/:0:4","tags":["Mysql"],"title":"Mysql用法","uri":"/mysql/"},{"categories":["Mysql"],"content":"排序 过滤 过滤 is_buy=0的值 SELECT * FROM mall.m_store_sort ORDER BY is_buy=0 ,is_buy ASC ","date":"2024-02-17","objectID":"/mysql/:0:5","tags":["Mysql"],"title":"Mysql用法","uri":"/mysql/"},{"categories":["Mysql"],"content":"索引策略 独立的列 索引列不能是表达式的一部分,也不能是函数的参数 前缀索引和索引选择性 索引的选择性: 不重复的索引值和数据表的记录总数的比值,索引的选择性越高查询效率越高。 对于BLOB/TEXT或者很长的VARCHAR类型的列,必须使用前缀索引。因为Mysql不允许索引这些列的完整的长度 诀窍在于要选择足够长的前缀以保证较高的选择性,同时又不能太长。 如何创建: ALTER TABLE sakila.city_demo ADD KEY (city(7)); 缺点: Mysql无法使用前缀索引做ORDER BY和GROUP BY,也无法使用前缀做覆盖扫描 多列索引 当出现服务器对多个索引做相交操作时(通常有多个AND条件),通常意味着需要一个包含所有相关列的多列索引,而不是多个独立的单列索引 选择合适的索引列的顺序 (适用于B-TREE索引) 当不需要考虑排序和分组时,将选择性最高的列放在前面通常是很好的,这时候的索引的作用只是用于优化where条件的查找 聚簇索引 聚簇索引并不是一种单独的索引类型,而是一种数据的存储方式 优点: 可以把相关数据保存在一起。例如实现电子邮件时,可以根据用户ID来聚集数据,这样只需要从磁盘读取少数的数据页就能获取某个用户的全部邮件。如果没有使用聚族索引,则每封邮件都可能导致一次磁盘I/O; 数据访问更快。聚族索引将索引和数据保存在同一个B-Tree中,因此从聚族索引中获取数据通常比在非聚族索引中查找更快。 使用覆盖索引扫描的查询可以直接使用节点中的主键值。 缺点: 聚簇数据最大限度的提高了I/O密集型应用的性能,但如果数据全部都放在内存中,则访问的顺序就没有那么重要了,聚簇索引也就没有那么优势了; 插入速度严重依赖于插入顺序。按照主键的顺序插入是加载数据到InnoDB表中速度最快的方式。但如果不是按照主键顺序加载数据,那么在加载完成后最好使用OPTIMIZE TABLE命令重新组织一下表。 更新聚簇索引列的代价很高,因为会强制InnoDB将每个被更新的行移动到新的位置。 基于聚簇索引的表在插入新行,或者主键被更新导致需要移动行的时候,可能面临“页分裂”的问题。当行的主键值要求必须将这一行插入到某个已满的页中时,存储引擎会将该页分裂成两个页面来容纳该行,这就是一次分裂操作。页分裂会导致表占用更多的磁盘空间。 聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候。 二级索引(非聚簇索引)可能比想象的要更大,因为在二级索引的叶子节点包含了引用行的主键列。 二级索引访问需要两次索引查找,而不是一次。 InnoDB的二级索引的叶子节点中存储的不是“行指针”。而是主键值,并以此作为指向行的“指针”。这样的策略减少了当出现行移动或者数据页分裂时二级索引的维护工作。 使用InnoDB时应该尽可能的按照按键顺序插入数据,并且尽可能的使用单调增加的聚簇键的值来插入新行。 覆盖索引 定义: 如果一个索引包含所有需要查询的字段的值,我们称之为覆盖索引。 优点: 索引条目通常远小于数据行大小,所以如果只需要读取索引,那MySQL就会极大地减少数据访问量。这对缓存的负载非常重要,因为这种情况下响应时间大部分花费在数据拷贝上。覆盖索引对于I/O密集型的应用也有帮助,因为索引比数据更小,更容易全部放入内存中(这对于MyISAM尤其正确,因为MyISAM能压缩索引以变得更小)。 因为索引是按照列值顺存储的(至少在单个页内是如此),所以对于I/O密集型的范围查询会比随机从磁盘读取每一行数据的I/O要少得多。对于某些存储引擎,例如MyISAM和Percona XtraDB,甚至可以通过OPTIMIZE命令使得索引完全顺序排列,这让简单的范围查询能使用完全顺序的索引访问。 一些存储引擎如MyISAM在内存中只缓存索引,数据则依赖于操作系统来缓存,因此要访问数据需要一次系统调用。这可能会导致严重的性能问题,尤其是那些系统调用占了数据访问中的最大开销的场景。 由于InnoDB的聚簇索引,覆盖索引对InnoDB表特别有用。InnoDB的二级索引在叶子节点中保存了行的主键值,所以如果二级主键能够覆盖查询,则可以避免对主键索引的二次查询。 MySQL只能使用B-Tree索引做覆盖索引 使用索引扫描来做排序 如果EXPLAIN出来的type列的值为“index”,则说明Mysql使用了索引扫描来做排序 只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向(倒序或正序)都一样时,MySQL才能够使用索引来对结果做排序。如果查询需要关联多张表,则只有当ORDER BY子句引用的字段全部为第一个表时,才能使用索引做排序。OEDER BY子句和查找型查询的限制是一样的:需要满足索引的最左前缀的要求;否则,MySQL都需要执行排序操作,而无法利用索引排序。 有一种情况下ORDER BY子句可以不满足索引的最左前缀的要求,就是前导列为常量的时候。如果WHERE子句或者JOIN子句中对这些列制定了常量,就可以“弥补“索引的不足。 即使OEDER BY子句不满足索引的最左前缀的要求,也可以用于查询排序,这是因为索引的第一列被指定为一个常数 前缀压缩索引 MyISAM使用前缀压缩来减少索引的大小,从而让更多的索引可以放入内存,这在某些情况下能极大的提高性能。默认值压缩字符串,但通过参数设置也可以对整数做压缩。 压缩使用更少的空间,待见是某些操作可能更慢。因为每个值的压缩前缀都依赖前面的值,所以MyISAM查找时无法在所言块中使用二分查找而只能从头开始扫描。正序的扫描速度还不错,但如果时倒序就是很好了。测试表明,对于cpu密集型应用,因为扫描需要随机查找,压缩索引使得MyISAM在索引查找上要慢好几倍。压缩索引的倒序扫描就更慢了。压缩索引需要在cpu内存资源与磁盘之间做平衡。压缩索引可能只需要十分之一大小的磁盘空间,如果是I/O密集型应用,对某些查询带来的好处会比成本多很多。 可以在create table语句中指定PACK_KEYS参数来控制索引压缩的方式。 冗余和重复索引 重复索引是指在相同的列上按照相同的顺序创建的相同类型的额索引。应该避免这样创建索引,发现以后也应该立即移除。 如果创建了索引(A,B),再创建索引(A)就是冗余索引,因为这只是前一个索引的前缀索引。 一般来说。增加索引将会导致INSERT/UODATE/DELETE等操作的速度变慢,特别是当新增索引后导致了内存瓶颈的时候 索引和锁 索引和锁可以让查询锁定更少的行。如果你的查询从不访问那些不需要访问的行,那么就会锁定更少的行,从两个方面来看这对性能都有好处。首先,虽然innodb的行锁效率很高,内存使用也很少,但是锁定行的时候仍然会带来额外的开销,其次,锁定超过需要的行会增加锁竞争,并减少并发性。 innodb只有在访问行的时候才会对其加锁,而索引能够减少innodb访问的行数,从而减少锁的数量。但只有当innodb在存储引擎能够过滤掉不需要的行时才有效。如果索引无法过滤掉无效的行,那么在innodb检索到数据并返回给服务器层以后,MySQL服务器才能应用WHERE子句。这时候,已经无法避免锁定行了:inno代表可以在服务器端过滤掉行后就释放锁,但是在早期的MySQL版本中,innodb只有在事务提交后才能释放锁。 ","date":"2024-02-17","objectID":"/mysql/:0:6","tags":["Mysql"],"title":"Mysql用法","uri":"/mysql/"},{"categories":["Mysql"],"content":"查询优化 优化数据访问 是否向数据库请求了不需要的数据 查询不需要的记录:加limit 多表关联时返回全部列 总是取出全部列 重复查询相同的数据:做数据缓存 查询执行的基础 客户端发送一条查询给服务器。 服务器先检查查询缓存,如果命中缓存,则立刻返回存储在缓存中的记过。否则进入下一阶段。 服务器进行SQL解析,预处理,再由优化器生成对应的执行计划。 MYSQL根据优化器生成的执行计划,调用存储引擎的API来执行查询。 将结果返回给客户端。 mysql客户端和服务器之间的通信协议 是“半双工”的,这意味着,在任何一个时刻要么是由服务器向客户端发送数据,要么是由客户端向服务器发送数据,这两个动作不能懂事发生。所以我们也无需将一个消息切成小块独立来发送。 这种协议让mysql通信简单快速,但也从很多地方限制了mysql。一个明显的限制是,这意味着没法进行流量控制。一旦一段开始发生消息,另一端要接受完整个消息才能响应它。这就像是来回抛球的游戏:在任何时刻,只有一个人能够控制球,而且只有控制球的人才能将消息抛回去(发送消息)。 客户端用一个单独的数据包将查询传给服务器。这也是为什么当查询语句很长的时候,参数max_allowed_packet就非常重要了。一旦客户端发送了请求,它能做的事情就只是等待结果了。 相反的,一般服务器响应给用户的数据通常很多,由多个数据包组成。当服务器开始响应客户端请求时,客户端必须完整地接受整个返回结果,而不能简单地只取前几条结果,然后让服务器停止发送数据。这种情况下,客户端若接受完整的结果,然后取前面几条需要的结果,或者接收完几调结果后就粗暴地断开链接,都不是好主意。这也是在必要的时候一定要在查询中加上limit限制的原因。 换一种方式解释这种行为:当客户端从服务器取数据时,看起来是一个拉数据的过程,但实际上是mysql在向客户端推送数据的过程。客户端不断地接收从服务器推送的数据,客户端也没法让服务器停下来。客户端像是“从消防水管喝水”。 多少连接mysql的库函数都可以获得全部结果集并缓存到内存里,还可以朱行获取需要的数据。默认一般是获得全部结果集并缓存在内存中。mysql通常需要等所有的数据都已经发送给客户端才能释放这条查询所占用的资源,所以接受全部结果并缓存通常可以减少服务器的压力,让查询能够早点结束、早点释放相应资源。 当使用多数链接mysql的库函数从mysql获取数据时,其结果看起来都像是从mysql服务器获取数据,二实际上都是从这个库函数的缓存获取数据。多数情况下没什么问题,但是如果需要范湖一个很大的结果集的时候,这样做并不好,因为库函数会花很多时间和内存来存储所有的结果集。如果能够尽早开始处理这些结果集,就能大大减少内存的消耗,这种情况下可以不适用缓存来记录结果而是直接处理。这样做的缺点是,对于服务器来说需要查询完成后才能释放资源,所以在客户端交互的整个过程中,服务器的资源都是被这个查询锁占用的。 查询缓存 在解析一个查询语句之前,如果查询缓存是打开的,那么mysql会优先检查这个查询是否命中查询缓存中的数据。这个检查是通过一个对大小写敏感的哈希查找实现的。查询和缓存中的查询即使只是一个字节不同,那也不会匹配缓存结果,这种情况下查询就会进入下一阶段的处理。 如果当前的查询恰好命中了查询缓存,那么在返回查询结果之前mysql会检查一次用户权限。这仍然是无需解析查询SQL语句的,因为在查询缓存中已经存放了当前查询需要访问的表信息。如果权限没有问题,mysql会跳过所有其他阶段,直接从缓存中拿到结果并返回给客户端。这种情况下,查询不会被解析,不用生成执行计划,不会被执行。 change buffer的使用场景 普通索引的所有场景,使用 change buffer 都可以起到加速作用吗? 因为 merge 的时候是真正进行数据更新的时刻,而 change buffer 的主要目的就是将记录的变更动作缓存下来,所以在一个数据页做 merge 之前,change buffer 记录的变更越多(也就是这个页面上要更新的次数越多),收益就越大。 因此,对于写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时change buffer 的使用效果最好。这种业务模型常见的就是账单类、日志类的系统。 ","date":"2024-02-17","objectID":"/mysql/:0:7","tags":["Mysql"],"title":"Mysql用法","uri":"/mysql/"},{"categories":["Mysql"],"content":"索引选择和实践 这两类索引在查询能力上是没差别的,主要考虑的是对更新性能的影响。所以,我建议你尽量选择普通索引。 尽量使用普通索引,然后把 change buffer 尽量开大,以确保这个“历史数据”表的数据写入速度。 ","date":"2024-02-17","objectID":"/mysql/:0:8","tags":["Mysql"],"title":"Mysql用法","uri":"/mysql/"},{"categories":["Mysql"],"content":"普通索引和唯一索引,应该怎么选择? 查询过程 对于普通索引来说,查找到满足条件的第一个记录 (5,500) 后,需要查找下一个记录,直到碰到第一个不满足 k=5 条件的记录。 对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。 ","date":"2024-02-17","objectID":"/mysql/:0:9","tags":["Mysql"],"title":"Mysql用法","uri":"/mysql/"},{"categories":["Mysql"],"content":"更新过程 当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InooDB 会将这些更新操作缓存在 changebuffer 中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。 change buffer,实际上它是可以持久化的数据。也就是说,change buffer 在内存中有拷贝,也会被写入到磁盘上。 什么条件下可以使用change buffer呢? 唯一索引的更新就不能使用 change buffer,实际上也只有普通索引可以使用。 change buffer 的大小,可以通过参数 innodb_change_buffer_max_size 来动态设置。这个参数设置为 50 的时候,表示 change buffer 的大小最多只能占用 buffer pool 的 50%。 如果要在这张表中插入一个新记录 (4,400) 的话,InnoDB 的处理流程是怎样的? 这个记录要更新的目标页在内存中。这时,InnoDB 的处理流程如下: 对于唯一索引来说,找到 3 和 5 之间的位置,判断到没有冲突,插入这个值,语句执行结束; 对于普通索引来说,找到 3 和 5 之间的位置,插入这个值,语句执行结束。 这个记录要更新的目标页不在内存中。这时,InnoDB 的处理流程如下: 对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值,语句执行结束; 对于普通索引来说,则是将更新记录在 change buffer,语句执行就结束了。 将数据从磁盘读入内存涉及随机 IO 的访问,是数据库里面成本最高的操作之一。changebuffer 因为减少了随机磁盘访问,所以对更新性能的提升是会很明显的。 Change buffer和redo log redo log 主要节省的是随机写磁盘的 IO 消耗(转成顺序写),而 change buffer 主要节省的则是随机读磁盘的 IO 消耗。 ","date":"2024-02-17","objectID":"/mysql/:0:10","tags":["Mysql"],"title":"Mysql用法","uri":"/mysql/"},{"categories":["Mysql"],"content":"索引下推 使用ICP的情况下,查询过程: 存储引擎读取索引记录(不是完整的行记录); 判断WHERE条件部分能否用索引中的列来做检查,条件不满足,则处理下一行索引记录(+1) 条件满足,使用索引中的主键去定位并读取完整的行记录(就是所谓的回表); 存储引擎把记录交给Server层,Server层检测该记录是否满足WHERE条件的其余部分。 ","date":"2024-02-17","objectID":"/mysql/:0:11","tags":["Mysql"],"title":"Mysql用法","uri":"/mysql/"},{"categories":["Mysql"],"content":"最左前缀原则 当你创建了一个联合索引,该索引的任何最左前缀都可以用于查询 mysql会一直向右匹配直到遇到范围查询(\u003e、\u003c、between、like)就停止匹配,比如a = 1 and b = 2 and c \u003e 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。 ","date":"2024-02-17","objectID":"/mysql/:0:12","tags":["Mysql"],"title":"Mysql用法","uri":"/mysql/"},{"categories":["Mysql"],"content":"前缀索引 使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。 我们在建立索引时关注的是区分度,区分度越高越好。因为区分度越高,意味着重复的键值越少。因此,我们可以通过统计索引上有多少个不同的值来判断要使用多长的前缀。 怎么确定前缀长度? 计算出列上有多少个不同的值 Select count(distinct email) as L from SUSer; 依次选取不同长度的前缀来看这个值 Select count(distinct left(email,4)) as L4 count(distinct left(email,5)) as L5 from SUSer; 要预先设定一个可以接受的损失比例,比如 5%。然后,在返回的 L4~L5 中,找出不小于 L * 95% 的值 前缀索引对覆盖索引的影响 使用前缀索引就用不上覆盖索引对查询性能的优化了,这也是你在选择是否使用前缀索引时需要考虑的一个因素。 ","date":"2024-02-17","objectID":"/mysql/:0:13","tags":["Mysql"],"title":"Mysql用法","uri":"/mysql/"},{"categories":["Mysql"],"content":"基于主键索引和普通索引的查询有什么区别? 如果语句是 select * from T where ID=500,主键查询方式,则只需要搜索 ID 这棵B+ 树; 如果语句是 select * from T where k=5,普通索引查询方式,则需要先搜索 k 索引树,得到 ID 的值为 500,再到 ID 索引树搜索一次。这个过程称为回表。 ","date":"2024-02-17","objectID":"/mysql/:0:14","tags":["Mysql"],"title":"Mysql用法","uri":"/mysql/"},{"categories":["Mysql"],"content":"索引的类型 主键索引(聚簇索引):主键索引的叶子节点存的是整行数据 非主键索引(二级索引):非主键索引的叶子节点内容是主键的值 哪些场景应该使用自增主键? 自增主键的插入数据模式,正符合了我们前面提到的递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。 有些业务的场景需求是这样的:1. 只有一个索引;2. 该索引必须是唯一索引。此时更适合用业务字段作为主键 ","date":"2024-02-17","objectID":"/mysql/:0:15","tags":["Mysql"],"title":"Mysql用法","uri":"/mysql/"},{"categories":["Mysql"],"content":"Mysql为什么有时候会选错索引? 优化器的逻辑 扫描行数是怎么判断的? MySQL 在真正开始执行语句之前,并不能精确地知道满足这个条件的记录有多少条,而只能根据统计信息来估算记录数。 MySQL 是怎样得到索引的基数的呢? InnoDB 默认会选择 N 个数据页,统计这些页面上的不同值,得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数。 当变更的数据行数超过1/M 的时候,会自动触发重新做一次索引统计。 在 MySQL 中,有两种存储索引统计的方式,可以通过设置参数 innodb_stats_persistent的值来选择: 设置为 on 的时候,表示统计信息会持久化存储。这时,默认的 N 是 20,M 是 10。 设置为 off 的时候,表示统计信息只存储在内存中。这时,默认的 N 是 8,M 是 16。 重新统计索引信息: analyze table t 索引选择异常和处理 采用 force index 强行选择一个索引。 我们可以考虑修改语句,引导 MySQL 使用我们期望的索引。 在有些场景下,我们可以新建一个更合适的索引,来提供给优化器做选择,或删掉误用的索引。 ","date":"2024-02-17","objectID":"/mysql/:0:16","tags":["Mysql"],"title":"Mysql用法","uri":"/mysql/"},{"categories":["秒杀"],"content":"这篇文章展示了秒杀.","date":"2024-02-17","objectID":"/miaosha/","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"主要挑战 瞬时高并发 高并发无法避开的热点数据问题 来自黑产的刷子流量 ","date":"2024-02-17","objectID":"/miaosha/:1:0","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"页面静态化 cdn加速 ","date":"2024-02-17","objectID":"/miaosha/:2:0","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"设计 ","date":"2024-02-17","objectID":"/miaosha/:3:0","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"秒杀按钮 js文件控制 定时器 ","date":"2024-02-17","objectID":"/miaosha/:4:0","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"读多写少 redis ","date":"2024-02-17","objectID":"/miaosha/:5:0","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"缓存问题 ","date":"2024-02-17","objectID":"/miaosha/:6:0","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"缓存击穿 缓存预热 分布式锁 ","date":"2024-02-17","objectID":"/miaosha/:6:1","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"缓存穿透 布隆过滤器: 适用于缓存更新很少的场景 将商品id加入缓存,设置超时时间尽量短 ","date":"2024-02-17","objectID":"/miaosha/:6:2","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"库存问题 ","date":"2024-02-17","objectID":"/miaosha/:7:0","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"预扣库存 数据库扣减库存 数据库的乐观锁:Stock\u003e0在更新 问题: 死锁问题 容易造成系统雪崩 redis扣减库存 非原子操作 Lua脚本扣减库存 ","date":"2024-02-17","objectID":"/miaosha/:7:1","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"分布式锁 ","date":"2024-02-17","objectID":"/miaosha/:8:0","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"Setnx加锁 ","date":"2024-02-17","objectID":"/miaosha/:8:1","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"Set加锁 lockKey requestId NX PX expireTime ","date":"2024-02-17","objectID":"/miaosha/:8:2","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"释放锁 通过requestId,只释放自己的锁,不允许释放别人加的锁 ","date":"2024-02-17","objectID":"/miaosha/:8:3","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"自旋锁 解决均匀分布的秒杀问题 ","date":"2024-02-17","objectID":"/miaosha/:8:4","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"Redission 在不同的节点上使用单个实例获取锁的方式去获得锁,且每次获取锁都有超时时间,如果请求超时,则认为该节点不可用。当应用服务成功获取锁的Redis节点超过半数(N/2+1,N为节点数)时,并且获取锁消耗的实际时间不超过锁的过期时间,则获取锁成功 ","date":"2024-02-17","objectID":"/miaosha/:8:5","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"mq异步处理 ","date":"2024-02-17","objectID":"/miaosha/:9:0","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"下单功能 消息丢失问题 Job,增加重试 消息发送表 重复消费问题 消息处理表 注意 下单和写消息处理表,需要在同一个事务当中,保证原子操作 垃圾消息问题 Job重试的时候,需要判断一下消息发送表该消息的发送次数是否达到最大限制 延迟消费问题 下单消息生产者先生成订单,向延迟队列发送消息 ","date":"2024-02-17","objectID":"/miaosha/:9:1","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"如何限流 ","date":"2024-02-17","objectID":"/miaosha/:10:0","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"对同一用户限流 ","date":"2024-02-17","objectID":"/miaosha/:10:1","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"对同一ip限流 ","date":"2024-02-17","objectID":"/miaosha/:10:2","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"对接口限流 ","date":"2024-02-17","objectID":"/miaosha/:10:3","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"加验证码 用户体验比较差 ","date":"2024-02-17","objectID":"/miaosha/:10:4","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["秒杀"],"content":"提高业务门槛 ","date":"2024-02-17","objectID":"/miaosha/:10:5","tags":["秒杀"],"title":"秒杀","uri":"/miaosha/"},{"categories":["Mysql"],"content":"这篇文章展示了Mysql查询优化用法.","date":"2024-02-16","objectID":"/mysql_optimize/","tags":["Mysql"],"title":"Mysql查询优化","uri":"/mysql_optimize/"},{"categories":["Mysql"],"content":"查询分页数据方法 使用数据库提供 SQL语句 select * from table_name limit 0,5 语句样式: MySQL中,可用如下方法: SELECT * FROM 表名称 LIMIT M,N 适应场景: 适用于数据量较少的情况(元组百/千级) 原因/缺点: 全表扫描,速度会很慢,且有的数据库结果集返回不稳定(如某次返回1,2,3,另外的一次返回2,1,3),Limit限制的是从结果集的M位置处取出N条输出,其余抛弃。 建立主键或唯一索引,利用索引 语句样式:可用如下方法:SELECT * FROM 表名称 WHERE pk_id \u003e (pageNum*10) LIMIT M 适应场景:适用于数据量多的情况(元组数上万) 原因:索引扫描,速度会很快。有朋友提出:因为数据查询出来并不是按照pk_id排序的,所以会有漏掉数据的情况,只能方法3 基于索引在排序 语句样式:可用如下方法: SELECT * FROM 表名称 WHERE pk_id \u003e (pageNum*10) ORDER BY pk_id ASC LIMIT M 适应场景: 适用于数据量多的情况(元组数上万),最好ORDER BY后的列对象是主键或唯一索引,使得ORDER BY操作能利用索引被消除但结果集是稳定的 原因:索引扫描,速度会很快。 基于索引使用prepare(第一个问号表示pageNum,第二个?表示每页元组数) 语句样式:MySQL中,可用如下方法: PREPARE stmt_name FROM SELECT * FROM 表名称 WHERE pk_id \u003e (?* ?) ORDER BY pk_id ASC LIMIT M 适应场景:大数据量 原因:索引扫描,速度会很快。prepare语句又比一般的查询语句快一点。 利用MySQL支持ORDER操作可以利用索引快速定位部分元组,避免全表扫描 比如: 读第1000到1019行元组(pk是主键/唯一键). SELECT * FROM your_table WHERE pk\u003e=1000 ORDER BY pk ASC LIMIT 0,20 利用\"子查询/连接+索引\"快速定位元组的位置,然后再读取元组。 道理同方法5。如(id是主键/唯一键,$page、$pagesize是变量): 利用子查询示例: SELECT * FROM your_table WHERE id \u003c= (SELECT id FROM your_table ORDER BY id desc LIMIT ($page-1)*$pagesize ORDER BY id desc LIMIT $pagesize 利用连接示例: SELECT * FROM your_table AS t1 JOIN (SELECT id FROM your_table ORDER BY id desc LIMIT ($page-1)*$pagesize AS t2 WHERE t1.id \u003c= t2.id ORDER BY t1.id desc LIMIT $pagesize; ","date":"2024-02-16","objectID":"/mysql_optimize/:1:0","tags":["Mysql"],"title":"Mysql查询优化","uri":"/mysql_optimize/"},{"categories":["Mysql"],"content":"优化方式 直接用limit start, count分页语句: 对limit分页问题的性能优化方法 利用表的覆盖索引来加速分页查询:我们都知道,利用了索引查询的语句中如果只包含了那个索引列(覆盖索引),那么这种情况会查询很快。 因为利用索引查找有优化算法,且数据就在查询索引上面,不用再去找相关的数据地址了,这样节省了很多时间。另外Mysql中也有相关的索引缓存,在并发高的时候利用缓存就效果更好了。 ","date":"2024-02-16","objectID":"/mysql_optimize/:1:1","tags":["Mysql"],"title":"Mysql查询优化","uri":"/mysql_optimize/"},{"categories":["Mysql"],"content":"短连接风暴 短连接模型存在一个风险,就是一旦数据库处理得慢一些,连接数就会暴涨。max_connections 参数,用来控制一个 MySQL 实例同时存在的连接数的上限,超过这个值,系统就会拒绝接下来的连接请求,并报错提示“Too many connections”。对于被拒绝连接的请求来说,从业务角度看就是数据库不可用。 第一种方法:先处理掉那些占着连接但是不工作的线程。 因此,如果是连接数过多,你可以优先断开事务外空闲太久的连接;如果这样还不够,再考虑断开事务内空闲太久的连接。 从数据库端主动断开连接可能是有损的,尤其是有的应用端收到这个错误后,不重新连接,而是直接用这个已经不能用的句柄重试查询。这会导致从应用端看上去,“MySQL 一直没恢复”。 第二种方法:减少连接过程的消耗。 在 MySQL 8.0 版本里,如果你启用–skip-grant-tables 参数,MySQL 会默认把 –skipnetworking 参数打开,表示这时候数据库只能被本地的客户端连接。可见,MySQL 官方对 skip-grant-tables 这个参数的安全问题也很重视。 ","date":"2024-02-16","objectID":"/mysql_optimize/:2:0","tags":["Mysql"],"title":"Mysql查询优化","uri":"/mysql_optimize/"},{"categories":["Mysql"],"content":"慢查询性能问题 ","date":"2024-02-16","objectID":"/mysql_optimize/:3:0","tags":["Mysql"],"title":"Mysql查询优化","uri":"/mysql_optimize/"},{"categories":["Mysql"],"content":"索引没有设计好; 最高效的做法就是直接执行 alter table 语句。 比较理想的是能够在备库先执行。假设你现在的服务是一主一备,主库 A、备库 B,这个方案的大致流程是这样的: 在备库 B 上执行 set sql_log_bin=off,也就是不写 binlog,然后执行 alter table 语句加上索引; 执行主备切换; 这时候主库是 B,备库是 A。在 A 上执行 set sql_log_bin=off,然后执行 alter table语句加上索引。 ","date":"2024-02-16","objectID":"/mysql_optimize/:3:1","tags":["Mysql"],"title":"Mysql查询优化","uri":"/mysql_optimize/"},{"categories":["Mysql"],"content":"SQL 语句没写好; 我们可以通过改写 SQL 语句来处理。MySQL 5.7 提供了 query_rewrite 功能,可以把输入的一种语句改写成另外一种模式。 ","date":"2024-02-16","objectID":"/mysql_optimize/:3:2","tags":["Mysql"],"title":"Mysql查询优化","uri":"/mysql_optimize/"},{"categories":["Mysql"],"content":"MySQL 选错了索引。 应急方案就是给这个语句加上 force index。 上线前,在测试环境,把慢查询日志(slow log)打开,并且把 long_query_time 设置成 0,确保每个语句都会被记录入慢查询日志; 在测试表里插入模拟线上的数据,做一遍回归测试; 观察慢查询日志里每类语句的输出,特别留意 Rows_examined 字段是否与预期一致。 你需要工具帮你检查所有的 SQL语句的返回结果。比如,你可以使用开源工具 pt-querydigest(https://www.percona.com/doc/percona-toolkit/3.0/pt-query-digest.html)。 ","date":"2024-02-16","objectID":"/mysql_optimize/:3:3","tags":["Mysql"],"title":"Mysql查询优化","uri":"/mysql_optimize/"},{"categories":["Mysql"],"content":"QPS突增问题 一种是由全新业务的 bug 导致的。假设你的 DB 运维是比较规范的,也就是说白名单是一个个加的。这种情况下,如果你能够确定业务方会下掉这个功能,只是时间上没那么快,那么就可以从数据库端直接把白名单去掉。 如果这个新功能使用的是单独的数据库用户,可以用管理员账号把这个用户删掉,然后断开现有连接。这样,这个新功能的连接不成功,由它引发的 QPS 就会变成 0。 如果这个新增的功能跟主体功能是部署在一起的,那么我们只能通过处理语句来限制。这时,我们可以使用上面提到的查询重写功能,把压力最大的 SQL 语句直接重写成\"select 1\"返回。 当然,这个操作的风险很高,需要你特别细致。它可能存在两个副作用: 如果别的功能里面也用到了这个 SQL 语句模板,会有误伤; 很多业务并不是靠这一个语句就能完成逻辑的,所以如果单独把这一个语句以 select 1的结果返回的话,可能会导致后面的业务逻辑一起失败。 所以,方案 3 是用于止血的,跟前面提到的去掉权限验证一样,应该是你所有选项里优先级最低的一个方案。 ","date":"2024-02-16","objectID":"/mysql_optimize/:4:0","tags":["Mysql"],"title":"Mysql查询优化","uri":"/mysql_optimize/"},{"categories":["Mysql"],"content":"这篇文章展示了Mysql配置用法.","date":"2024-02-15","objectID":"/mysql_config/","tags":["Mysql"],"title":"Mysql配置","uri":"/mysql_config/"},{"categories":["Mysql"],"content":"修改最大连接数 通过命令 使用数据库提供 SQL语句 set GLOBAL max_connections=100; show variables like \"max_connections\"; 及时生效,不需要重启服务,确保使用 root账号执行 修改 my.cnf vim /etc/my.cnf max_connections=100 /etc/init.d/mysqld restart 需要重启服务 ","date":"2024-02-15","objectID":"/mysql_config/:0:1","tags":["Mysql"],"title":"Mysql配置","uri":"/mysql_config/"},{"categories":["Mysql"],"content":"这篇文章展示了Mysql DDL.","date":"2024-02-14","objectID":"/mysql_ddl/","tags":["Mysql"],"title":"Mysql DDL","uri":"/mysql_ddl/"},{"categories":["Mysql"],"content":"基本语法 对数据库进行定义 create Database nba; drop Database nba; 对数据表进行定义 create table table_name; ","date":"2024-02-14","objectID":"/mysql_ddl/:0:1","tags":["Mysql"],"title":"Mysql DDL","uri":"/mysql_ddl/"},{"categories":["Mysql"],"content":"数据库约束 主键约束 作用: 唯一标识一条记录,不能重复,不能为空 一个数据表的主键只能有一个。主键可以是一个字段,也可以由多个字段复合组成。 外键 外键确保了表与表之间引用的完整性。 一个表中的外键对应另一张表的主键。外键可以重复也可以为空 唯一性约束 表明字段在表中的数值是唯一的 NOT NULL约束 表明该字段不应为空,必须有取值 DEFAULT 表明这个字段的默认值 CHECK约束 检查特定字段取值范围的有效性 ","date":"2024-02-14","objectID":"/mysql_ddl/:0:2","tags":["Mysql"],"title":"Mysql DDL","uri":"/mysql_ddl/"},{"categories":["Mysql"],"content":"设计原则 数据表的个数越少越好 数据表中的字段个数越少越好 数据表中联合主键的字段个数越少越好 使用主键和外键越多越好 关系越多,证明实体间的冗余度越低,利用度越高。 ","date":"2024-02-14","objectID":"/mysql_ddl/:0:3","tags":["Mysql"],"title":"Mysql DDL","uri":"/mysql_ddl/"},{"categories":["Mysql"],"content":"这篇文章展示了Mysql查询用法.","date":"2024-02-14","objectID":"/mysql_select/","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"查询顺序 关键字顺序不能颠倒 select ...from...where...group by...having select语句执行顺序 from \u003e where \u003e group by \u003e having \u003e select的字段 \u003e dist ","date":"2024-02-14","objectID":"/mysql_select/:1:0","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"数据过滤 ","date":"2024-02-14","objectID":"/mysql_select/:2:0","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"比较运算符 =、\u003c\u003e、\u003c、\u003c=、\u003e、\u003e=、!\u003c、BETWEEN、IS NULL ","date":"2024-02-14","objectID":"/mysql_select/:2:1","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"逻辑运算符 AND、 OR、 IN、 NOT ","date":"2024-02-14","objectID":"/mysql_select/:2:2","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"通配符 如果要让索引生效,则 LIKE后面不能以%开头。 ","date":"2024-02-14","objectID":"/mysql_select/:2:3","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"排序方式 FileSort:一般在内存中排序,占用 CPU较多。如果待排结果较大,会产生临时文件 I/O到磁盘进行排序,效率较低 Index:效率更高,索引可以保证数据的有序性 ","date":"2024-02-14","objectID":"/mysql_select/:2:4","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"SQL函数 ","date":"2024-02-14","objectID":"/mysql_select/:3:0","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"算术函数 函数名 定义 ABS() 取绝对值 MOD() 取余 ROUND() 四舍五入 ","date":"2024-02-14","objectID":"/mysql_select/:3:1","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"字符串函数 函数名 定义 CONTACT() 多个字符串拼接 LENGTH() 计算字段的长度,一个汉字算三个字符 CHAR_LENGTH() 计算字段的长度,汉字、数字、字母算一个字符 LOWER() 将字符串的字符转化为小写 UPPER() 将字符串的字符转化为大写 REPLACE() 替换函数 SUBSTRING() 截取字符串 ","date":"2024-02-14","objectID":"/mysql_select/:3:2","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"日期函数 函数名 定义 CURRENT_DATE() 取绝对值 CURRENT_TIME() 取余 CURRENT_TIMESTAMP() 四舍五入 EXTRACT() 抽取具体的年、月、日 DATE() 日期 YEAR() 年份 MONTH() 月份 DAY() 天数 HOUR() 时 MINUTE() 分 SECOND() 秒 ","date":"2024-02-14","objectID":"/mysql_select/:3:3","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"转换函数 函数名 定义 CAST() 数据类型转化 COALESCE() 返回第一个非空数值 ","date":"2024-02-14","objectID":"/mysql_select/:3:4","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"命名规范 关键字和函数和名称全部大写 数据库名、表名、字段名全部小写 SQL语句必须以分号结尾 ","date":"2024-02-14","objectID":"/mysql_select/:3:5","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"聚集函数 函数名 定义 COUNT() 总行数 MAX() 最大值 MIN() 最小值 SUM() 求和 AVG() 平均值 ","date":"2024-02-14","objectID":"/mysql_select/:3:6","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"WHERE和 HAVING区别 过滤分组使用 HAVING. 数据行使用 WHERE ","date":"2024-02-14","objectID":"/mysql_select/:3:7","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"子查询 ","date":"2024-02-14","objectID":"/mysql_select/:4:0","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"非关联子查询 子查询从数据表中查询了数据结果,如果这个数据结果只执行了一次,然后这个数据结果作为主查询的条件进行执行。 ","date":"2024-02-14","objectID":"/mysql_select/:4:1","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"Exist子查询 ","date":"2024-02-14","objectID":"/mysql_select/:4:2","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"集合比较子查询 IN 判断是否在集合中 ANY 需跟比较操作符一起使用 ALL 需跟比较操作符一起使用 SOME ANY的别名 ","date":"2024-02-14","objectID":"/mysql_select/:4:3","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"关联子查询 如果子查询需要执行多次,即采用循环的方式,先从外部查询开始,每次都传入子查询进行查询,然后再将结果反馈给外部 ","date":"2024-02-14","objectID":"/mysql_select/:4:4","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"in和 exist效率 SELECT* FROM A WHERE cc IN(SELECT cc FROM B) SELECT* FROM A WHERE cc EXIST(SELECT cc FROM B WHERE B.cc=A.cc) 如果对 cc列都建立了索引的情况下,需判断表 A和表 B的大小。 如果 A\u003eB大,那么 IN子查询的效率要比 EXIST子查询效率高。 ","date":"2024-02-14","objectID":"/mysql_select/:4:5","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"连接 ","date":"2024-02-14","objectID":"/mysql_select/:5:0","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"自连接 ","date":"2024-02-14","objectID":"/mysql_select/:5:1","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"笛卡尔积 ","date":"2024-02-14","objectID":"/mysql_select/:5:2","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"外连接 ","date":"2024-02-14","objectID":"/mysql_select/:5:3","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"非等值连接 ","date":"2024-02-14","objectID":"/mysql_select/:5:4","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"等值连接 ","date":"2024-02-14","objectID":"/mysql_select/:5:5","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"图 封装了底层和数据表的借口,简化一张表或者多张表的数据结果集 ","date":"2024-02-14","objectID":"/mysql_select/:6:0","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"创建视图 create view view_name As select column1, column2 from table; ","date":"2024-02-14","objectID":"/mysql_select/:6:1","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"修改视图 Alter view view_name As select column1, column2 from table; ","date":"2024-02-14","objectID":"/mysql_select/:6:2","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"删除视图 drop view view_name ","date":"2024-02-14","objectID":"/mysql_select/:6:3","tags":["Mysql"],"title":"Mysql查询","uri":"/mysql_select/"},{"categories":["Mysql"],"content":"这篇文章展示了Mysql Procedure.","date":"2024-02-13","objectID":"/mysql_procedure/","tags":["Mysql"],"title":"Mysql Procedure","uri":"/mysql_procedure/"},{"categories":["Mysql"],"content":"基本语法 创建存储过程 Create Procedure name BEGIN 执行语句 END 更新存储过程 Alter Procedure name BEGIN 执行语句 END 删除存储过程 Drop Procedure name ","date":"2024-02-13","objectID":"/mysql_procedure/:0:1","tags":["Mysql"],"title":"Mysql Procedure","uri":"/mysql_procedure/"},{"categories":["Mysql"],"content":"参数类型 参数类型 是否返回 In 否 Out 是 InOut 是 ","date":"2024-02-13","objectID":"/mysql_procedure/:0:2","tags":["Mysql"],"title":"Mysql Procedure","uri":"/mysql_procedure/"},{"categories":["Mysql"],"content":"流控制语句 BEGIN … END 语句 DECLARE 声明变量 SET 赋值语句 SELECT … INTO 为变量赋值 IF…THEN…ENDIF 条件判断语句 CASE多条件判断语句 LOOP、 LEAVE和 ITERATE:LOOP循环语句,使用LEAVE可以跳出循环,使用ITERATE则可以进入下一次循环 REPEAT…UNTIL…END REPEAT:循环语句 WHILE…DO…END WHILE:循环语句 ","date":"2024-02-13","objectID":"/mysql_procedure/:0:3","tags":["Mysql"],"title":"Mysql Procedure","uri":"/mysql_procedure/"},{"categories":["Mysql"],"content":"优缺点 优点 一次编译多次使用 提升 SQL的执行效率,减少开发工作量 安全性强 减少网络传输量 ","date":"2024-02-13","objectID":"/mysql_procedure/:0:4","tags":["Mysql"],"title":"Mysql Procedure","uri":"/mysql_procedure/"},{"categories":["Mysql"],"content":"缺点 可移植性差 调试困难 版本管理困难 不适合高并发的场景 ","date":"2024-02-13","objectID":"/mysql_procedure/:0:5","tags":["Mysql"],"title":"Mysql Procedure","uri":"/mysql_procedure/"},{"categories":["Mysql"],"content":"这篇文章展示了Mysql事务.","date":"2024-02-13","objectID":"/mysql_transaction/","tags":["Mysql"],"title":"Mysql事务","uri":"/mysql_transaction/"},{"categories":["Mysql"],"content":"ACID原则 A原子性 数据处理操作的基本单位 C一致性 当事务提交后,或者当事务发生回滚后,数据库的完整性约束不能被破坏 I隔离性 一个事务在提交之前,对其他事务都是不可见的 D持久性 事务提交之后对数据的修改是持久性的 ","date":"2024-02-13","objectID":"/mysql_transaction/:0:1","tags":["Mysql"],"title":"Mysql事务","uri":"/mysql_transaction/"},{"categories":["Mysql"],"content":"事务语句 Start Transaction或者 begin, 显式的开启事务 Commit 提交事务 Rollback或者 Rollback to 回滚事务 Savepoint 在事务中创建保存点,方便后续针对保存点进行回滚 Release savepoint 删除某个保存点 Set transaction 设置事务的隔离级别 ","date":"2024-02-13","objectID":"/mysql_transaction/:0:2","tags":["Mysql"],"title":"Mysql事务","uri":"/mysql_transaction/"},{"categories":["Mysql"],"content":"设置自动提交 set autocommit = 0;//关闭 set autocommit = 1;//开启 ","date":"2024-02-13","objectID":"/mysql_transaction/:0:3","tags":["Mysql"],"title":"Mysql事务","uri":"/mysql_transaction/"},{"categories":["Mysql"],"content":"completion type参数 completion=0 当我们执行 commit的时候会提交事务,在执行下一个事务时,需要我们 start transaction或者 begin开启 completion=1 当我们提交事务后,相当执行了 commit and chain,开启一个链式事务。每条 SQL语句都会自动进行提交,如果采用start transaction或者begin显式开启事务,那么这个事务只有在 commit时才会生效,在 rollback时才会回滚 completion=2 commit and release 当我们提交事务后,会自动与服务器断开连接 ","date":"2024-02-13","objectID":"/mysql_transaction/:0:4","tags":["Mysql"],"title":"Mysql事务","uri":"/mysql_transaction/"},{"categories":["Mysql"],"content":"隔离性 脏读 读到了其他事务还没有提交的数据 可重复读 可重复读指的是在一个事务内,最开始读到的数据和事务结束前的任意时刻读到的同一批数据都是一致的。通常针对数据更新(UPDATE)操作。 不可重复读 在同一事务内,不同的时刻读到的同一批数据可能是不一样的,可能会受到其他事务的影响。通常针对数据更新(UPDATE)操作 幻读 幻读是针对数据插入(INSERT)操作来说的 ","date":"2024-02-13","objectID":"/mysql_transaction/:0:5","tags":["Mysql"],"title":"Mysql事务","uri":"/mysql_transaction/"},{"categories":["Mysql"],"content":"隔离级别 隔离级别 脏读 不可重复读 幻读 读未提交 可能 可能 可能 读提交 不可能 可能 可能 可重复读 不可能 不可能 可能 串行化 不可能 不可能 不可能 举例 事务 A 事务 B 启动事务,查询得到值 1 启动事务 查询得到值 1 将 1改成 2 查询得到值 V1 提交事务 B 查询得到值 V2 提交事务 A 查询得到值 V3 若隔离级别是“读未提交”,则 V1 的值就是 2。这时候事务 B 虽然还没有提交,但是结果已经被 A 看到了。因此,V2、V3 也都是 2。 若隔离级别是“读提交”,则 V1 是 1,V2 的值是 2。事务 B 的更新在提交后才能被 A看到。所以, V3 的值也是 2。 若隔离级别是“可重复读”,则 V1、V2 是 1,V3 是 2。之所以 V2 还是 1,遵循的就是这个要求:事务在执行期间看到的数据前后必须是一致的。 若隔离级别是“串行化”,则在事务 B 执行“将 1 改成 2”的时候,会被锁住。直到事务 A 提交后,事务 B 才可以继续执行。所以从 A 的角度看, V1、V2 值是 1,V3 的值是 2。 ","date":"2024-02-13","objectID":"/mysql_transaction/:0:6","tags":["Mysql"],"title":"Mysql事务","uri":"/mysql_transaction/"},{"categories":["Mysql"],"content":"快照在MVCC里是怎么工作的? InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。 而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。 一个事务只需要在启动的时候声明说,“以我启动的时刻为准,如果一个数据版本是在我启动之前生成的,就认;如果是我启动以后才生成的,我就不认,我必须要找到它的上一个版本”。当然,如果“上一个版本”也不可见,那就得继续往前找。还有,如果是这个事务自己更新的数据,它自己还是要认的。 在实现上, InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务 ID。“活跃”指的就是,启动了但还没提交。数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1记为高水位。 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的; 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的; 如果落在黄色部分,那就包括两种情况 a. 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见; b. 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。 InnoDB 利用了“所有数据都有多个版本”的这个特性,实现了“秒级创建快照”的能力。 ","date":"2024-02-13","objectID":"/mysql_transaction/:0:7","tags":["Mysql"],"title":"Mysql事务","uri":"/mysql_transaction/"},{"categories":["Mysql"],"content":"更新逻辑 更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。 其实,除了 update 语句外,select 语句如果加锁(加上 lock inshare mode 或 for update),也是当前读。 事务的可重复读的能力是怎么实现的? 核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。 ","date":"2024-02-13","objectID":"/mysql_transaction/:0:8","tags":["Mysql"],"title":"Mysql事务","uri":"/mysql_transaction/"},{"categories":["Mysql"],"content":"为什么建议你尽量不要使用长事务? 长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面他可能用到的回滚记录都必须保留,这就会导致大量占用存储空间 ","date":"2024-02-13","objectID":"/mysql_transaction/:0:9","tags":["Mysql"],"title":"Mysql事务","uri":"/mysql_transaction/"},{"categories":["Mysql"],"content":"事务的启动方式 显式启动事物语句,begin或start transaction set autocommit=0,这个命令会将这个线程的自动提交关掉。 建议你总是使用 set autocommit=1, 通过显式语句的方式来启动事务。 在 information_schema 库的 innodb_trx 这个表中查询长事务。 ","date":"2024-02-13","objectID":"/mysql_transaction/:0:10","tags":["Mysql"],"title":"Mysql事务","uri":"/mysql_transaction/"},{"categories":["Mysql"],"content":"为保证数据库隔离级别的一致,将 MySQL 的隔离级别设置为“读提交” 配置的方式是,将启动参数 transaction-isolation 的值设置成 READ-COMMITTED。 show variables like ’transaction_isolation' ","date":"2024-02-13","objectID":"/mysql_transaction/:0:11","tags":["Mysql"],"title":"Mysql事务","uri":"/mysql_transaction/"},{"categories":["Go"],"content":"这篇文章展示了once用法","date":"2024-02-12","objectID":"/once/","tags":["Go"],"title":"once用法","uri":"/once/"},{"categories":["Go"],"content":"使用场景 once常常用来初始化单例资源,或者并发访问只需初始化一次的共享资源,或者在测试的时候初始化一次测试资源 ","date":"2024-02-12","objectID":"/once/:0:1","tags":["Go"],"title":"once用法","uri":"/once/"},{"categories":["Go"],"content":"实现 一个正确的once实现要使用一个互斥锁,这样初始化的时候如果有并发的goroutine,就会进入doSlow方法 ","date":"2024-02-12","objectID":"/once/:0:2","tags":["Go"],"title":"once用法","uri":"/once/"},{"categories":["Go"],"content":"2种错误 死锁 解决方案:不要在f参数中调用当前的这个once,不管是直接的还是间接的 未初始化 解决方案:可以自己实现一个类似once的并发原语,既可以以返回当前调用do方法是否正确完成,还可以在初始化失败后调用do方法再次尝试初始化,知道初始化成功才不再初始化了 ","date":"2024-02-12","objectID":"/once/:0:3","tags":["Go"],"title":"once用法","uri":"/once/"},{"categories":["Go"],"content":"这篇文章展示了random用法","date":"2024-02-12","objectID":"/rand/","tags":["Go"],"title":"random用法","uri":"/rand/"},{"categories":["Go"],"content":"golang生成随机数可以使用math/rand包 示例如下: package main import ( \"fmt\" \"math/rand\" ) func main() { for i:=0; i\u003c10; i++ { fmt.Println(rand.Intn(100)) } } 而发现这种情况,每次执行的结果一样. 修改如下: package main import ( \"fmt\" \"time\" \"math/rand\" ) func main() { r := rand.New(rand.NewSource(time.Now().UnixNano())) for i:=0; i\u003c10; i++ { fmt.Println(r.Intn(100)) } } 而这种方式就可以使用时间种子来获取不同的结果了。 示例2: package main import ( \"fmt\" \"math/rand\" \"time\" ) func main() { rand.Seed(time.Now().UnixNano()) for i := 0; i \u003c 10; i++ { x := rand.Intn(100) fmt.Println(x) } } 例子是打印10个100以内(0-99)的随机数字。 ","date":"2024-02-12","objectID":"/rand/:0:1","tags":["Go"],"title":"random用法","uri":"/rand/"},{"categories":["Mysql"],"content":"这篇文章展示了Mysql日志用法.","date":"2024-02-11","objectID":"/mysql_log/","tags":["Mysql"],"title":"Mysql日志","uri":"/mysql_log/"},{"categories":["Mysql"],"content":"binlog的写入机制 写入逻辑: 事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。 系统给 binlog cache 分配了一片内存,每个线程一个,参数 binlog_cache_size 用于控制单个线程内 binlog cache 所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。 事务提交的时候,执行器把 binlog cache 里的完整事务写入到 binlog 中,并清空 binlogcache。 write 和 fsync 的时机,是由参数 sync_binlog 控制的: sync_binlog=0 的时候,表示每次提交事务都只 write,不 fsync; sync_binlog=1 的时候,表示每次提交事务都会执行 fsync; sync_binlog=N(N\u003e1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才fsync。 因此,在出现 IO 瓶颈的场景里,将 sync_binlog 设置成一个比较大的值,可以提升性能。在实际的业务场景中,考虑到丢失日志量的可控性,一般不建议将这个参数设成 0,比较常见的是将其设置为 100~1000 中的某个数值。 但是,将 sync_binlog 设置为 N,对应的风险是:如果主机发生异常重启,会丢失最近 N个事务的 binlog 日志。 ","date":"2024-02-11","objectID":"/mysql_log/:1:0","tags":["Mysql"],"title":"Mysql日志","uri":"/mysql_log/"},{"categories":["Mysql"],"content":"Redo log的写入机制(Innodb特有) 当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log里面,并更新内存,这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。 事务还没提交的时候,redo log buffer 中的部分日志有没有可能被持久化到磁盘呢? 有。 为了控制 redo log 的写入策略,InnoDB 提供了 innodb_flush_log_at_trx_commit 参数,它有三种可能取值: 设置为 0 的时候,表示每次事务提交时都只是把 redo log 留在 redo log buffer 中 ; 设置为 1 的时候,表示每次事务提交时都将 redo log 直接持久化到磁盘(建议); 设置为 2 的时候,表示每次事务提交时都只是把 redo log 写到 page cache。 除了后台线程每秒一次的轮询操作外,还有两种场景会让一个没有提交的事务的redo log 写入到磁盘中。 一种是,redo log buffer 占用的空间即将达到 innodb_log_buffer_size 一半的时候,后台线程会主动写盘。 另一种是,并行的事务提交的时候,顺带将这个事务的 redo log buffer 持久化到磁盘。 通常我们说 MySQL 的“双 1”配置,指的就是 sync_binlog 和innodb_flush_log_at_trx_commit 都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是 redo log(prepare 阶段),一次是 binlog。 LSN是单调递增的,用来对应 redo log 的一个个写入点。每次写入长度为 length 的 redolog,LSN 的值就会加上 length。 WAL:write-Ahead Logging 先写日志,在写磁盘 WAL 机制主要得益于两个方面: redo log 和 binlog 都是顺序写,磁盘的顺序写比随机写速度要快; 组提交机制,可以大幅度降低磁盘的 IOPS 消耗。 如果你的 MySQL 现在出现了性能瓶颈,而且瓶颈在 IO 上,可以通过哪些方法来提升性能呢? 设置 binlog_group_commit_sync_delay 和binlog_group_commit_sync_no_delay_count 参数,减少 binlog 的写盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但没有丢失数据的风险。 将 sync_binlog 设置为大于 1 的值(比较常见是 100~1000)。这样做的风险是,主机掉电时会丢 binlog 日志。 将 innodb_flush_log_at_trx_commit 设置为 2。这样做的风险是,主机掉电的时候会丢数据。 ","date":"2024-02-11","objectID":"/mysql_log/:2:0","tags":["Mysql"],"title":"Mysql日志","uri":"/mysql_log/"},{"categories":["Mysql"],"content":"区别 redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。 redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。 redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。 ","date":"2024-02-11","objectID":"/mysql_log/:2:1","tags":["Mysql"],"title":"Mysql日志","uri":"/mysql_log/"},{"categories":["Mysql"],"content":"执行 Update更新流程 ","date":"2024-02-11","objectID":"/mysql_log/:2:2","tags":["Mysql"],"title":"Mysql日志","uri":"/mysql_log/"},{"categories":["Mysql"],"content":"这篇文章展示了Mysql锁.","date":"2024-02-11","objectID":"/mysql_lock/","tags":["Mysql"],"title":"Mysql锁","uri":"/mysql_lock/"},{"categories":["Mysql"],"content":"行锁功过:怎么减少行锁对性能的影响? 从两阶段锁说起 在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。 死锁和死锁检测 当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态, 两种策略: 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout 来设置。 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。 一种头痛医头的方法,就是如果你能确保这个业务一定不会出现死锁,可以临时把死锁检测关掉。 另一个思路就是控制并发度 ","date":"2024-02-11","objectID":"/mysql_lock/:1:0","tags":["Mysql"],"title":"Mysql锁","uri":"/mysql_lock/"},{"categories":["Mysql"],"content":"全局锁和表锁:给表加个字段怎么有这么多阻碍? ","date":"2024-02-11","objectID":"/mysql_lock/:2:0","tags":["Mysql"],"title":"Mysql锁","uri":"/mysql_lock/"},{"categories":["Mysql"],"content":"全局锁 命令: Flush tables with read lock (FTWRL) 使用场景:全库逻辑备份 逻辑备份工具: mysqldump。 当 mysqldump 使用参数–single-transaction的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。 single-transaction 方法只适用于所有的表使用事务引擎的库。如果有的表使用了不支持事务的引擎,那么备份就只能通过 FTWRL 方法。 FTWRL与set global readonly=true区别: 在有些系统中,readonly 的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。因此,修改 global 变量的方式影响面更大,我不建议你使用。 在异常处理机制上有差异。如果执行 FTWRL 命令之后由于客户端发生异常断开,那么 MySQL 会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为 readonly 之后,如果客户端发生异常,则数据库就会一直保持 readonly 状态,这样会导致整个库长时间处于不可写状态,风险较高。 ","date":"2024-02-11","objectID":"/mysql_lock/:2:1","tags":["Mysql"],"title":"Mysql锁","uri":"/mysql_lock/"},{"categories":["Mysql"],"content":"表级锁 表锁: 语法: lock tables … read/write。 对于 InnoDB 这种支持行锁的引擎,一般不使用 lock tables 命令来控制并发,毕竟锁住整个表的影响面还是太大。 MDL(metadata lock)元数据锁 MDL 不需要显式使用,在访问一个表的时候会被自动加上。 读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。 读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。 因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。 如何安全的给小表加字段? 解决长事务:在 MySQL 的information_schema 库的 innodb_trx 表中,你可以查到当前执行中的事务。如果你要做DDL 变更的表刚好有长事务在执行,要考虑先暂停 DDL,或者 kill 掉这个长事务。 如果你要变更的表是一个热点表,虽然数据量不大,但是上面的请求很频繁,而你不得不加个字段,你该怎么做呢? 在 alter table语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到 MDL 写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。之后开发人员或者 DBA 再通过重试命令重复这个过程。 ","date":"2024-02-11","objectID":"/mysql_lock/:2:2","tags":["Mysql"],"title":"Mysql锁","uri":"/mysql_lock/"},{"categories":["leetcode"],"content":"这篇文章展示了动态规划.","date":"2024-02-11","objectID":"/dynamic_programming/","tags":["leetcode"],"title":"动态规划","uri":"/dynamic_programming/"},{"categories":["leetcode"],"content":"0-1 背包问题 ","date":"2024-02-11","objectID":"/dynamic_programming/:1:0","tags":["leetcode"],"title":"动态规划","uri":"/dynamic_programming/"},{"categories":["leetcode","Binary Tree"],"content":"这篇文章展示了对称二叉树.","date":"2024-02-11","objectID":"/erchashu_duicheng/","tags":["leetcode","Binary Tree"],"title":"对称二叉树","uri":"/erchashu_duicheng/"},{"categories":["leetcode","Binary Tree"],"content":"对称二叉树 对称二叉树 ","date":"2024-02-11","objectID":"/erchashu_duicheng/:1:0","tags":["leetcode","Binary Tree"],"title":"对称二叉树","uri":"/erchashu_duicheng/"},{"categories":["leetcode","Binary Tree"],"content":"方法一:递归 在计算当前二叉树的最大深度时,可以先递归计算出其左子树和右子树的最大深度,然后在 O(1) 时间内计算出当前二叉树的最大深度。递归在访问到空节点时退出。 func isSymmetric(root *TreeNode) bool { return check(root, root) } func check(p, q *TreeNode) bool { if p == nil \u0026\u0026 q == nil { return true } if p == nil || q == nil { return false } return p.Val == q.Val \u0026\u0026 check(p.Left, q.Right) \u0026\u0026 check(p.Right, q.Left) } 复杂度分析 时间复杂度:这里遍历了这棵树,渐进时间复杂度为 O(n)。 空间复杂度:这里的空间复杂度和递归使用的栈空间有关,这里递归层数不超过 n,故渐进空间复杂度为 O(n)。 ","date":"2024-02-11","objectID":"/erchashu_duicheng/:1:1","tags":["leetcode","Binary Tree"],"title":"对称二叉树","uri":"/erchashu_duicheng/"},{"categories":["Data"],"content":"这篇文章展示了二叉树.","date":"2024-02-11","objectID":"/binary_tree/","tags":["Data"],"title":"二叉树","uri":"/binary_tree/"},{"categories":["Data"],"content":"树 比如下面这幅图,A 节点就是 B 节点的父节点,B 节点是 A 节点的子节点。B、C、D 这三 个节点的父节点是同一个节点,所以它们之间互称为兄弟节点。我们把没有父节点的节点叫 作根节点,也就是图中的节点 E。我们把没有子节点的节点叫作叶子节点或者叶节点,比如 图中的 G、H、I、J、K、L 都是叶子节点。 ","date":"2024-02-11","objectID":"/binary_tree/:1:0","tags":["Data"],"title":"二叉树","uri":"/binary_tree/"},{"categories":["Data"],"content":"二叉树 二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子节点和右 子节点。 二叉树中,叶子节点全都在最底层,除了叶子节点之外,每个节点都有左 右两个子节点,这种二叉树就叫作满二叉树。 二叉树中,叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,并且除 了最后一层,其他层的节点个数都要达到最大,这种二叉树叫作完全二叉树。 ","date":"2024-02-11","objectID":"/binary_tree/:2:0","tags":["Data"],"title":"二叉树","uri":"/binary_tree/"},{"categories":["Data"],"content":"二叉树的遍历 遍历方式 //前序遍历 void preOrder(Node* root) { if (root == null) return; print root // 此处为伪代码,表示打印 root 节点 preOrder(root-\u003eleft); preOrder(root-\u003eright); } //中序遍历 void inOrder(Node* root) { if (root == null) return; inOrder(root-\u003eleft); print root // 此处为伪代码,表示打印 root 节点 inOrder(root-\u003eright); } //后序遍历 void postOrder(Node* root) { if (root == null) return; postOrder(root-\u003eleft); postOrder(root-\u003eright); print root // 此处为伪代码,表示打印 root 节点 } ","date":"2024-02-11","objectID":"/binary_tree/:3:0","tags":["Data"],"title":"二叉树","uri":"/binary_tree/"},{"categories":["Data"],"content":"二叉查找树(Binary Search Tree) 二叉查找树要求,在树中的任意一个节点,其左子树 中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。 ","date":"2024-02-11","objectID":"/binary_tree/:4:0","tags":["Data"],"title":"二叉树","uri":"/binary_tree/"},{"categories":["Data"],"content":"查找操作 首先,我们看如何在二叉查找树中查找一个节点。我们先取根节点,如果它等于我们要查找 的数据,那就返回。如果要查找的数据比根节点的值小,那就在左子树中递归查找;如果要 查找的数据比根节点的值大,那就在右子树中递归查找。 public class BinarySearchTree { private Node tree; public Node find(int data) { Node p = tree; while (p != null) { if (data \u003c p.data) p = p.left; else if (data \u003e p.data) p = p.right; else return p; } return null; } public static class Node { private int data; private Node left; private Node right; public Node(int data) { this.data = data; } } } ","date":"2024-02-11","objectID":"/binary_tree/:4:1","tags":["Data"],"title":"二叉树","uri":"/binary_tree/"},{"categories":["Data"],"content":"插入操作 如果要插入的数据比节点的数据大,并且节点的右子树为空,就将新数据直接插到右子节点 的位置;如果不为空,就再递归遍历右子树,查找插入位置。同理,如果要插入的数据比节 点数值小,并且节点的左子树为空,就将新数据插入到左子节点的位置;如果不为空,就再 递归遍历左子树,查找插入位置。 public void insert(int data) { if (tree == null) { tree = new Node(data); return; } Node p = tree; while (p != null) { if (data \u003e p.data) { if (p.right == null) { p.right = new Node(data); return; } p = p.right; } else { // data \u003c p.data if (p.left == null) { p.left = new Node(data); return; } p = p.left; } } } ","date":"2024-02-11","objectID":"/binary_tree/:4:2","tags":["Data"],"title":"二叉树","uri":"/binary_tree/"},{"categories":["Data"],"content":"删除操作 public void delete(int data) { Node p = tree; // p 指向要删除的节点,初始化指向根节点 Node pp = null; // pp 记录的是 p 的父节点 while (p != null \u0026\u0026 p.data != data) { pp = p; if (data \u003e p.data) p = p.right; else p = p.left; } if (p == null) return; // 没有找到 // 要删除的节点有两个子节点 if (p.left != null \u0026\u0026 p.right != null) { // 查找右子树中最小节点 Node minP = p.right; Node minPP = p; // minPP 表示 minP 的父节点 while (minP.left != null) { minPP = minP; minP = minP.left; } p.data = minP.data; // 将 minP 的数据替换到 p 中 p = minP; // 下面就变成了删除 minP 了 pp = minPP; } // 删除节点是叶子节点或者仅有一个子节点 Node child; // p 的子节点 if (p.left != null) child = p.left; else if (p.right != null) child = p.right; else child = null; if (pp == null) tree = child; // 删除的是根节点 else if (pp.left == p) pp.left = child; else pp.right = child; } ","date":"2024-02-11","objectID":"/binary_tree/:4:3","tags":["Data"],"title":"二叉树","uri":"/binary_tree/"},{"categories":["Data"],"content":"其他操作 支持快速地查找最大节点和最小节 点、前驱节点和后继节点。 注意 重要的特性,就是中序遍历二叉查找树, 可以输出有序的数据序列,时间复杂度是 O(n),非常高效 ","date":"2024-02-11","objectID":"/binary_tree/:4:4","tags":["Data"],"title":"二叉树","uri":"/binary_tree/"},{"categories":["Data"],"content":"支持重复数据的二叉查找树 针对的都是不存在键值相同的情况。那如果存储的两个对 象键值相同,这种情况该怎么处理呢? 第一种方法比较容易。二叉查找树中每一个节点不仅会存储一个数据,因此我们通过链表和 支持动态扩容的数组等数据结构,把值相同的数据都存储在同一个节点上。 第二种方法比较不好理解,不过更加优雅。 每个节点仍然只存储一个数据。在查找插入位置的过程中,如果碰到一个节点的值,与要插 入数据的值相同,我们就将这个要插入的数据放到这个节点的右子树,也就是说,把这个新 插入的数据当作大于这个节点的值来处理。 当要查找数据的时候,遇到值相同的节点,我们并不停止查找操作,而是继续在右子树中查 找,直到遇到叶子节点,才停止。这样就可以把键值等于要查找值的所有节点都找出来 对于删除操作,我们也需要先查找到每个要删除的节点,然后再按前面讲的删除操作的方 法,依次删除。 ","date":"2024-02-11","objectID":"/binary_tree/:5:0","tags":["Data"],"title":"二叉树","uri":"/binary_tree/"},{"categories":["Data"],"content":"什么是“平衡二叉查找树”? 平衡二叉树的严格定义是这样的:二叉树中任意一个节点的左右子树的高度相差不能大于 1。 平衡二叉查找树中“平衡”的意思,其实就是让整棵树左右看起来比较“对称”、比 较“平衡”,不要出现左子树很高、右子树很矮的情况。这样就能让整棵树的高度相对来说 低一些,相应的插入、删除、查找等操作的效率高一些。 ","date":"2024-02-11","objectID":"/binary_tree/:6:0","tags":["Data"],"title":"二叉树","uri":"/binary_tree/"},{"categories":["Data"],"content":"如何定义一棵“红黑树”? 红黑树的英文是“Red-Black Tree”,简称 R-B Tree。它是一种不严格的平衡二叉查找 树 一棵红黑 树还需要满足这样几个要求: 根节点是黑色的; 每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不存储数据; 任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的; 每个节点,从该节点到达其可达叶子节点的所有路径,都包含相同数目的黑色节点; ","date":"2024-02-11","objectID":"/binary_tree/:7:0","tags":["Data"],"title":"二叉树","uri":"/binary_tree/"},{"categories":["Data"],"content":"为什么说红黑树是“近似平衡”的? 平衡”的意思可以等价为性能不退化。“近似平衡”就等价为性能不会 退化的太严重。 二叉查找树很多操作的性能都跟树的高度成正比。一棵极其平衡的二叉 树(满二叉树或完全二叉树)的高度大约是 log2n,所以如果要证明红黑树是近似平衡的, 我们只需要分析,红黑树的高度是否比较稳定地趋近 log2n 就好了。 首先,我们来看,如果我们将红色节点从红黑树中去掉,那单纯包含黑色节点的红黑树的高 度是多少呢? 红色节点删除之后,有些节点就没有父节点了,它们会直接拿这些节点的祖父节点(父节点 的父节点)作为父节点。所以,之前的二叉树就变成了四叉树。 前面红黑树的定义里有这么一条:从任意节点到可达的叶子节点的每个路径包含相同数目的 黑色节点。我们从四叉树中取出某些节点,放到叶节点位置,四叉树就变成了完全二叉树。 所以,仅包含黑色节点的四叉树的高度,比包含相同节点个数的完全二叉树的高度还要小。 我们现在知道只包含黑色节点的“黑树”的高度,那我们现在把红色节点加回去,高度会变 成多少呢? 在红黑树中,红色节点不能相邻,也就是说,有一个 红色节点就要至少有一个黑色节点,将它跟其他红色节点隔开。红黑树中包含最多黑色节点 的路径不会超过 log2n,所以加入红色节点之后,最长路径不会超过 2log2n,也就是说, 红黑树的高度近似2log2n ","date":"2024-02-11","objectID":"/binary_tree/:8:0","tags":["Data"],"title":"二叉树","uri":"/binary_tree/"},{"categories":["Data"],"content":"实现红黑树的基本思想 ","date":"2024-02-11","objectID":"/binary_tree/:9:0","tags":["Data"],"title":"二叉树","uri":"/binary_tree/"},{"categories":["Data"],"content":"插入操作的平衡调整 红黑树规定,插入的节点必须是红色的。而且,二叉查找树中新插入的节点都是放在叶子节 点上。所以,关于插入操作的平衡调整,有这样两种特殊情况,但是也都非常好处理。 如果插入节点的父节点是黑色的,那我们什么都不用做,它仍然满足红黑树的定义。 如果插入的节点是根节点,那我们直接改变它的颜色,把它变成黑色就可以了 除此之外,其他情况都会违背红黑树的定义,于是我们就需要进行调整,调整的过程包含两 种基础的操作:左右旋转和改变颜色 CASE 1:如果关注节点是 a,它的叔叔节点 d 是红色 将关注节点 a 的父节点 b、叔叔节点 d 的颜色都设置成黑色; 将关注节点 a 的祖父节点 c 的颜色设置成红色; 关注节点变成 a 的祖父节点 c; 跳到 CASE 2 或者 CASE 3。 CASE 2:如果关注节点是 a,它的叔叔节点 d 是黑色,关注节点 a 是其父节点 b 的右子节点 关注节点变成节点 a 的父节点 b; 围绕新的关注节点b 左旋; 跳到 CASE 3。 CASE 3:如果关注节点是 a,它的叔叔节点 d 是黑色,关注节点 a 是其父节点 b 的左子节点 围绕关注节点 a 的祖父节点 c 右旋; 将关注节点 a 的父节点 b、兄弟节点 c 的颜色互换。 调整结束。 ","date":"2024-02-11","objectID":"/binary_tree/:9:1","tags":["Data"],"title":"二叉树","uri":"/binary_tree/"},{"categories":["Data"],"content":"删除操作的平衡调整 1. 针对删除节点初步调整 2. 针对关注节点进行二次调整 ","date":"2024-02-11","objectID":"/binary_tree/:9:2","tags":["Data"],"title":"二叉树","uri":"/binary_tree/"},{"categories":["Data"],"content":"递归树与时间复杂度分析 递归的思想就是,将大问题分解为小问题来求解,然后再将小问题分解为小 小问题。这样一层一层地分解,直到问题的数据规模被分解得足够小,不用继续递归分解为 止。 如果我们把这个一层一层的分解过程画成图,它其实就是一棵树。我们给这棵树起一个名 字,叫作递归树。 ","date":"2024-02-11","objectID":"/binary_tree/:10:0","tags":["Data"],"title":"二叉树","uri":"/binary_tree/"},{"categories":["Data"],"content":"实战一:分析快速排序的时间复杂度 快速排序的过程中,每次分区都要遍历待分区区间的所有数据,所以,每一层分区操作所遍 历的数据的个数之和就是n。我们现在只要求出递归树的高度 h,这个快排过程遍历的数据个数 就是 h * n,也就是说,时间复杂度就是 O(h * n) 我们知道,快速排序结束的条件就是待排序的小区间,大小为1,也就是说叶子节点里的数据规模是 1。 从根节点n到叶子节点 1,递归树中最短的一个路径每次都乘以1/10,最长的一个路径每次都乘以9/10. 通过计算,我们可以得到,从根节点到叶子节点的最短路径是 log10n,最长的路径是 log10/9 n 当分区比例是1 : 9时, 快速排序的时间复杂度仍然是 O(n log n) ","date":"2024-02-11","objectID":"/binary_tree/:10:1","tags":["Data"],"title":"二叉树","uri":"/binary_tree/"},{"categories":["Data"],"content":"实战二:分析斐波那契的时间复杂度 f(n)分解为 f(n-1)和f(n-2),每次数据规模都是 -1 或者-2.叶子节点的数据规模是 1 或者 2, 所以,从跟节点到叶子节点,每条的路径都是长短不一的 每次分解之后的合并操作只需要一次加法计算,我们把这次加法运算的时间消耗记作 1,从上往下,第一层的总时间消耗为 1, 第二层的总时间消耗为 2,第三层的总时间消耗就是 依次类推,第 k层的时间消耗就是 2^k-1,那整个算法的总的时间就是每层时间消耗之和 所以,该算法的时间复杂度就介于O(2^n)和O(2^(n/2))之间 ","date":"2024-02-11","objectID":"/binary_tree/:10:2","tags":["Data"],"title":"二叉树","uri":"/binary_tree/"},{"categories":["leetcode","Binary Tree"],"content":"这篇文章展示了二叉树的直径.","date":"2024-02-11","objectID":"/erchashu_zhijing/","tags":["leetcode","Binary Tree"],"title":"二叉树的直径","uri":"/erchashu_zhijing/"},{"categories":["leetcode","Binary Tree"],"content":"二叉树的直径 二叉树的直径 ","date":"2024-02-11","objectID":"/erchashu_zhijing/:1:0","tags":["leetcode","Binary Tree"],"title":"二叉树的直径","uri":"/erchashu_zhijing/"},{"categories":["leetcode","Binary Tree"],"content":"方法一:递归 在计算当前二叉树的最大深度时,可以先递归计算出其左子树和右子树的最大深度,然后在 O(1) 时间内计算出当前二叉树的最大深度。递归在访问到空节点时退出。 func diameterOfBinaryTree(root *TreeNode) (ans int) { var dfs func(*TreeNode) int dfs = func(node *TreeNode) int { if node == nil { return -1 // 下面 +1 后,对于叶子节点就刚好是 0 } lLen := dfs(node.Left) + 1 // 左子树最大链长+1 rLen := dfs(node.Right) + 1 // 右子树最大链长+1 ans = max(ans, lLen+rLen) // 两条链拼成路径 return max(lLen, rLen) // 当前子树最大链长 } dfs(root) return } func max(a, b int) int { if a \u003c b { return b } return a } 复杂度分析 时间复杂度:O(n),其中 n 为二叉树的节点个数。 空间复杂度:O(n)。最坏情况下,二叉树退化成一条链,递归需要 O(n) 的栈空间。 ","date":"2024-02-11","objectID":"/erchashu_zhijing/:1:1","tags":["leetcode","Binary Tree"],"title":"二叉树的直径","uri":"/erchashu_zhijing/"},{"categories":["leetcode","Binary Tree"],"content":"这篇文章展示了二叉树的中序遍历.","date":"2024-02-11","objectID":"/erchashu_zhongxu/","tags":["leetcode","Binary Tree"],"title":"二叉树的中序遍历","uri":"/erchashu_zhongxu/"},{"categories":["leetcode","Binary Tree"],"content":"二叉树的中序遍历 二叉树的中序遍历 ","date":"2024-02-11","objectID":"/erchashu_zhongxu/:1:0","tags":["leetcode","Binary Tree"],"title":"二叉树的中序遍历","uri":"/erchashu_zhongxu/"},{"categories":["leetcode","Binary Tree"],"content":"方法一:递归 定义 inorder(root) 表示当前遍历到 root 节点的答案,那么按照定义,我们只要递归调用 inorder(root.left) 来遍历 root 节点的左子树,然后将 root 节点的值加入答案,再递归调用inorder(root.right) 来遍历 root 节点的右子树即可,递归终止的条件为碰到空节点。 func inorderTraversal(root *TreeNode) (res []int) { var inorder func(node *TreeNode) inorder = func(node *TreeNode) { if node == nil { return } inorder(node.Left) res = append(res, node.Val) inorder(node.Right) } inorder(root) return } 复杂度分析 时间复杂度:O(n),其中 n 为二叉树节点的个数。二叉树的遍历中每个节点会被访问一次且只会被访问一次。 空间复杂度:O(n)。空间复杂度取决于递归的栈深度,而栈深度在二叉树为一条链的情况下会达到 O(n) 的级别。 ","date":"2024-02-11","objectID":"/erchashu_zhongxu/:1:1","tags":["leetcode","Binary Tree"],"title":"二叉树的中序遍历","uri":"/erchashu_zhongxu/"},{"categories":["leetcode","Binary Tree"],"content":"方法二:迭代 方法一的递归函数我们也可以用迭代的方式实现,两种方式是等价的,区别在于递归的时候隐式地维护了一个栈,而我们在迭代的时候需要显式地将这个栈模拟出来,其他都相同,具体实现可以看下面的代码。 func inorderTraversal(root *TreeNode) (res []int) { stack := []*TreeNode{} for root != nil || len(stack) \u003e 0 { for root != nil { stack = append(stack, root) root = root.Left } root = stack[len(stack)-1] stack = stack[:len(stack)-1] res = append(res, root.Val) root = root.Right } return } 复杂度同方法一 ","date":"2024-02-11","objectID":"/erchashu_zhongxu/:1:2","tags":["leetcode","Binary Tree"],"title":"二叉树的中序遍历","uri":"/erchashu_zhongxu/"},{"categories":["leetcode","Binary Tree"],"content":"这篇文章展示了二叉树的最大深度.","date":"2024-02-11","objectID":"/erchashu_zuidashendu/","tags":["leetcode","Binary Tree"],"title":"二叉树的最大深度","uri":"/erchashu_zuidashendu/"},{"categories":["leetcode","Binary Tree"],"content":"二叉树的最大深度 二叉树的最大深度 ","date":"2024-02-11","objectID":"/erchashu_zuidashendu/:1:0","tags":["leetcode","Binary Tree"],"title":"二叉树的最大深度","uri":"/erchashu_zuidashendu/"},{"categories":["leetcode","Binary Tree"],"content":"方法一:深度优先搜索 在计算当前二叉树的最大深度时,可以先递归计算出其左子树和右子树的最大深度,然后在 O(1) 时间内计算出当前二叉树的最大深度。递归在访问到空节点时退出。 func maxDepth(root *TreeNode) int { if root == nil { return 0 } return max(maxDepth(root.Left), maxDepth(root.Right)) + 1 } func max(a, b int) int { if a \u003e b { return a } return b } 复杂度分析 时间复杂度:O(n),其中 n 为二叉树节点的个数。每个节点在递归中只被遍历一次。 空间复杂度:O(height),其中 height 表示二叉树的高度。递归函数需要栈空间,而栈空间取决于递归的深度,因此空间复杂度等价于二叉树的高度。 ","date":"2024-02-11","objectID":"/erchashu_zuidashendu/:1:1","tags":["leetcode","Binary Tree"],"title":"二叉树的最大深度","uri":"/erchashu_zuidashendu/"},{"categories":["leetcode","Binary Tree"],"content":"方法二:广度优先搜索 方法一的递归函数我们也可以用迭代的方式实现,两种方式是等价的,区别在于递归的时候隐式地维护了一个栈,而我们在迭代的时候需要显式地将这个栈模拟出来,其他都相同,具体实现可以看下面的代码。 func maxDepth(root *TreeNode) int { if root == nil { return 0 } queue := []*TreeNode{} queue = append(queue, root) ans := 0 for len(queue) \u003e 0 { sz := len(queue) for sz \u003e 0 { node := queue[0] queue = queue[1:] if node.Left != nil { queue = append(queue, node.Left) } if node.Right != nil { queue = append(queue, node.Right) } sz-- } ans++ } return ans } 复杂度同方法一 ","date":"2024-02-11","objectID":"/erchashu_zuidashendu/:1:2","tags":["leetcode","Binary Tree"],"title":"二叉树的最大深度","uri":"/erchashu_zuidashendu/"},{"categories":["leetcode","Binary Tree"],"content":"这篇文章展示了翻转二叉树.","date":"2024-02-11","objectID":"/erchashu_fanzhuan/","tags":["leetcode","Binary Tree"],"title":"翻转二叉树","uri":"/erchashu_fanzhuan/"},{"categories":["leetcode","Binary Tree"],"content":"翻转二叉树 翻转二叉树 ","date":"2024-02-11","objectID":"/erchashu_fanzhuan/:1:0","tags":["leetcode","Binary Tree"],"title":"翻转二叉树","uri":"/erchashu_fanzhuan/"},{"categories":["leetcode","Binary Tree"],"content":"方法一:递归 在计算当前二叉树的最大深度时,可以先递归计算出其左子树和右子树的最大深度,然后在 O(1) 时间内计算出当前二叉树的最大深度。递归在访问到空节点时退出。 func invertTree(root *TreeNode) *TreeNode { if root == nil { return nil } left := invertTree(root.Left) right := invertTree(root.Right) root.Left = right root.Right = left return root } 复杂度分析 时间复杂度:O(N),其中 N 为二叉树节点的数目。我们会遍历二叉树中的每一个节点,对每个节点而言,我们在常数时间内交换其两棵子树。 空间复杂度:O(N)。使用的空间由递归栈的深度决定,它等于当前节点在二叉树中的高度。在平均情况下,二叉树的高度与节点个数为对数关系,即 O(logN)。而在最坏情况下,树形成链状,空间复杂度为 O(N)。 ","date":"2024-02-11","objectID":"/erchashu_fanzhuan/:1:1","tags":["leetcode","Binary Tree"],"title":"翻转二叉树","uri":"/erchashu_fanzhuan/"},{"categories":["Data"],"content":"这篇文章展示了复杂度.","date":"2024-02-11","objectID":"/complexity/","tags":["Data"],"title":"复杂度","uri":"/complexity/"},{"categories":["Data"],"content":"为什么需要复杂度分析? 测试结果非常依赖测试环境 测试结果受数据规模的影响很大 所以,我们需要一个不用具体的测试数据来测试,就可以粗略地估计算法的执行效率的方法。 ","date":"2024-02-11","objectID":"/complexity/:1:0","tags":["Data"],"title":"复杂度","uri":"/complexity/"},{"categories":["Data"],"content":"大 O 复杂度表示法 其中,T(n),它表示代码执行的时间;n 表 示数据规模的大小;f(n) 表示每行代码执行的次数总和。因为这是一个公式,所以用 f(n) 来表示。公式中的 O,表示代码的执行时间 T(n) 与 f(n) 表达式成正比。 大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是 表示代码执行时间随数据规模增长的变化趋势,所以,也叫作渐进时间复杂度 (asymptotic time complexity),简称时间复杂度。 ","date":"2024-02-11","objectID":"/complexity/:2:0","tags":["Data"],"title":"复杂度","uri":"/complexity/"},{"categories":["Data"],"content":"时间复杂度分析 ","date":"2024-02-11","objectID":"/complexity/:3:0","tags":["Data"],"title":"复杂度","uri":"/complexity/"},{"categories":["Data"],"content":"只关注循环执行次数最多的一段代码 我们在分析一个算法、一段代码的时间复杂度的时候,也只关注循环执行次数最多的那一段代码就可以了 ","date":"2024-02-11","objectID":"/complexity/:3:1","tags":["Data"],"title":"复杂度","uri":"/complexity/"},{"categories":["Data"],"content":"加法法则:总复杂度等于量级最大的那段代码的复杂度 ","date":"2024-02-11","objectID":"/complexity/:3:2","tags":["Data"],"title":"复杂度","uri":"/complexity/"},{"categories":["Data"],"content":"乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积 ","date":"2024-02-11","objectID":"/complexity/:3:3","tags":["Data"],"title":"复杂度","uri":"/complexity/"},{"categories":["Data"],"content":"几种常见时间复杂度实例分析 ","date":"2024-02-11","objectID":"/complexity/:4:0","tags":["Data"],"title":"复杂度","uri":"/complexity/"},{"categories":["Data"],"content":"O(1) int i = 8; int j = 6; int sum = i+j; 一般情况下,只要算法中不存在循环语句、递归语句,即使有成千 上万行的代码,其时间复杂度也是Ο(1)。 ","date":"2024-02-11","objectID":"/complexity/:4:1","tags":["Data"],"title":"复杂度","uri":"/complexity/"},{"categories":["Data"],"content":"O(logn)、O(nlogn) i = 1; while (i \u003c= n) { i = i * 2 } 如果一段代码的时间复杂度是 O(logn),我们循环执行 n 遍,时间复杂度就是 O(nlogn) 了。而且,O(nlogn) 也是一种非常常见的算法时间复杂度。 ","date":"2024-02-11","objectID":"/complexity/:4:2","tags":["Data"],"title":"复杂度","uri":"/complexity/"},{"categories":["Data"],"content":"O(m+n)、O(m*n) int cal(int m, int n) { int sum_1 = 0; int i = 1; for (;i\u003cm;++i) { sum_1 = sum_1 + i; } int sum_2 = 0; int j = 1; for (;j\u003cn;++j) { sum_2 = sum_2 + j; } return sum_1 + sum_2; } 从代码中可以看出,m 和 n 是表示两个数据规模。我们无法事先评估 m 和 n 谁的量级 大,所以我们在表示复杂度的时候,就不能简单地利用加法法则,省略掉其中一个。所以, 上面代码的时间复杂度就是 O(m+n)。 ","date":"2024-02-11","objectID":"/complexity/:4:3","tags":["Data"],"title":"复杂度","uri":"/complexity/"},{"categories":["Data"],"content":"空间复杂度分析 空间复杂度全称就是渐进空间复杂度(asymptotic space complexity),表示算法的存储空间与数据规模之间的增长关系。 void print(int n) { int i = 0; int[] a = new int[n]; for (i; i \u003cn; ++i) { a[i] = i * i; } for (i = n-1; i \u003e= 0; --i) { print out a[i] } } 跟时间复杂度分析一样,我们可以看到,第 2 行代码中,我们申请了一个空间存储变量 i, 但是它是常量阶的,跟数据规模 n 没有关系,所以我们可以忽略。第 3 行申请了一个大小 为 n 的 int 类型数组,除此之外,剩下的代码都没有占用更多的空间,所以整段代码的空 间复杂度就是 O(n)。 ","date":"2024-02-11","objectID":"/complexity/:5:0","tags":["Data"],"title":"复杂度","uri":"/complexity/"},{"categories":["Data"],"content":"最好、最坏情况时间复杂度 // n 表示数组 array 的长度 int find(int[] array, int n, int x) { int i = 0; int pos = -1; for (; i \u003c n; ++i) { if (array[i] == x) { pos = i; break; } } return pos; } 要查找的变量 x 可能出现在数组的任意位置。如果数组中第一个元素正好是要查找 的变量 x,那就不需要继续遍历剩下的 n-1 个数据了,那时间复杂度就是 O(1)。但如果数 组中不存在变量 x,那我们就需要把整个数组都遍历一遍,时间复杂度就成了 O(n)。所 以,不同的情况下,这段代码的时间复杂度是不一样的。 为了表示代码在不同情况下的不同时间复杂度,我们需要引入三个概念:最好情况时间复杂 度、最坏情况时间复杂度和平均情况时间复杂度。 顾名思义,最好情况时间复杂度就是,在最理想的情况下,执行这段代码的时间复杂度。就 像我们刚刚讲到的,在最理想的情况下,要查找的变量 x 正好是数组的第一个元素,这个 时候对应的时间复杂度就是最好情况时间复杂度。 同理,**最坏情况时间复杂度就是,在最糟糕的情况下,执行这段代码的时间复杂度。**就像刚 举的那个例子,如果数组中没有要查找的变量 x,我们需要把整个数组都遍历一遍才行,所 以这种最糟糕情况下对应的时间复杂度就是最坏情况时间复杂度。 ","date":"2024-02-11","objectID":"/complexity/:6:0","tags":["Data"],"title":"复杂度","uri":"/complexity/"},{"categories":["Data"],"content":"平均情况时间复杂度 平均时间复杂度的全称应该叫加权平均时间复杂度或者期望时间复杂度。 ","date":"2024-02-11","objectID":"/complexity/:7:0","tags":["Data"],"title":"复杂度","uri":"/complexity/"},{"categories":["Data"],"content":"均摊时间复杂度 // array 表示一个长度为 n 的数组 // 代码中的 array.length 就等于 n int[] array = new int[n]; int count = 0; void insert(int val) { if (count == array.length) { int sum = 0; for (int i = 0; i \u003c array.length; ++i) { sum = sum + array[i]; } array[0] = sum; count = 1; } array[count] = val; ++count; } 针对这种特殊的场景,我们引入了一种更加简单的分析方法:摊还分析法,通过摊还分析得 到的时间复杂度我们起了一个名字,叫均摊时间复杂度。 每一次 O(n) 的插入操作,都会跟着 n-1 次 O(1) 的插入操作,所以把耗时多的那次操作均摊到接下来的 n-1 次耗时少的操作上,均摊 下来,这一组连续的操作的均摊时间复杂度就是 O(1)。 均摊时间复杂度就是一种特殊的平均时间复杂度 ","date":"2024-02-11","objectID":"/complexity/:8:0","tags":["Data"],"title":"复杂度","uri":"/complexity/"},{"categories":["leetcode","LinkList"],"content":"这篇文章展示了链表.","date":"2024-02-11","objectID":"/lianbiao_huanxing/","tags":["leetcode","LinkList"],"title":"环形链表","uri":"/lianbiao_huanxing/"},{"categories":["leetcode","LinkList"],"content":"环形链表 环形链表 ","date":"2024-02-11","objectID":"/lianbiao_huanxing/:1:0","tags":["leetcode","LinkList"],"title":"环形链表","uri":"/lianbiao_huanxing/"},{"categories":["leetcode","LinkList"],"content":"方法一:哈希表 使用哈希表来存储所有已经访问过的节点。每次我们到达一个节点,如果该节点已经存在于哈希表中,则说明该链表是环形链表,否则就将该节点加入哈希表中。重复这一过程,直到我们遍历完整个链表即可。 func hasCycle(head *ListNode) bool { seen := map[*ListNode]struct{}{} for head != nil { if _, ok = seen[head]; ok { return true } seen[head] = struct{}{} head = head.Next } return false } 复杂度分析 时间复杂度:O(n),其中n 是链表的长度。最坏情况下我们需要遍历每个节点一次。 空间复杂度:O(n),其中n 是链表的长度。主要为哈希表的开销,最坏情况下我们需要将每个节点插入到哈希表中一次。 ","date":"2024-02-11","objectID":"/lianbiao_huanxing/:1:1","tags":["leetcode","LinkList"],"title":"环形链表","uri":"/lianbiao_huanxing/"},{"categories":["leetcode","LinkList"],"content":"方法二:快慢指针 具体地,我们定义两个指针,一快一慢。慢指针每次只移动一步,而快指针每次移动两步。初始时,慢指针在位置 head,而快指针在位置 head.next。这样一来,如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。 func hasCycle(head *ListNode) bool { if head == nil || head.Next == nil { return false } slow, fast := head, head.Next for slow != fast { if fast == nil || fast.Next == nil { return false } slow = slow.Next fast = fast.Next.Next } return true } 复杂度分析 时间复杂度:O(n),其中n 是链表的长度。 当链表中不存在环时,快指针将先于慢指针到达链表尾部,链表中每个节点至多被访问两次。 当链表中存在环时,每一轮移动后,快慢指针的距离将减小一。而初始距离为环的长度,因此至多移动 N 轮。 空间复杂度:O(1),我们只使用了两个指针的额外空间。 ","date":"2024-02-11","objectID":"/lianbiao_huanxing/:1:2","tags":["leetcode","LinkList"],"title":"环形链表","uri":"/lianbiao_huanxing/"},{"categories":["leetcode","LinkList"],"content":"这篇文章展示了链表的中间结点.","date":"2024-02-11","objectID":"/lianbiao_zhongjian/","tags":["leetcode","LinkList"],"title":"链表的中间结点","uri":"/lianbiao_zhongjian/"},{"categories":["leetcode","LinkList"],"content":"链表的中间结点 链表的中间结点 ","date":"2024-02-11","objectID":"/lianbiao_zhongjian/:1:0","tags":["leetcode","LinkList"],"title":"链表的中间结点","uri":"/lianbiao_zhongjian/"},{"categories":["leetcode","LinkList"],"content":"方法一:快慢指针 用两个指针 slow 与 fast 一起遍历链表。slow 一次走一步,fast 一次走两步。那么当 fast 到达链表的末尾时,slow 必然位于中间。 func middleNode(head *ListNode) *ListNode { // 快慢指针,快指针走2步,慢指针走1步 if head.Next == nil { return head } slow, fast := head, head for fast != nil \u0026\u0026 fast.Next != nil { fast = fast.Next.Next slow = slow.Next } return slow } 复杂度分析 时间复杂度:O(n),其中n是链表的长度。需要遍历链表一次。 空间复杂度:O(1)。只需要常数空间存放 slow 和 fast 两个指针。 ","date":"2024-02-11","objectID":"/lianbiao_zhongjian/:1:1","tags":["leetcode","LinkList"],"title":"链表的中间结点","uri":"/lianbiao_zhongjian/"},{"categories":["leetcode","LinkList"],"content":"这篇文章展示了链表.","date":"2024-02-11","objectID":"/lianbiao_fanzhuan/","tags":["leetcode","LinkList"],"title":"链表反转","uri":"/lianbiao_fanzhuan/"},{"categories":["leetcode","LinkList"],"content":"链表反转 链表反转 ","date":"2024-02-11","objectID":"/lianbiao_fanzhuan/:1:0","tags":["leetcode","LinkList"],"title":"链表反转","uri":"/lianbiao_fanzhuan/"},{"categories":["leetcode","LinkList"],"content":"方法一:迭代 在遍历链表时,将当前节点的 next 指针改为指向前一个节点。由于节点没有引用其前一个节点,因此必须事先存储其前一个节点。在更改引用之前,还需要存储后一个节点。最后返回新的头引用。 func reverseList(head *ListNode) *ListNode { var prev *ListNode curr := head for curr != nil { next := curr.Next curr.Next = prev prev = curr curr = next } return prev } 复杂度分析 时间复杂度:O(n),其中n是链表的长度。需要遍历链表一次。 空间复杂度:O(1)。 ","date":"2024-02-11","objectID":"/lianbiao_fanzhuan/:1:1","tags":["leetcode","LinkList"],"title":"链表反转","uri":"/lianbiao_fanzhuan/"},{"categories":["leetcode","LinkList"],"content":"方法二:递归法 func reverseList(head *ListNode) *ListNode { if head == nil || head.Next == nil { return head } newHead := reverseList(head.Next) head.Next.Next = head head.Next = nil return newHead } 复杂度分析 时间复杂度:O(n),其中n 是链表的长度。需要对链表的每个节点进行反转操作。 空间复杂度:O(n),其中n 是链表的长度。空间复杂度主要取决于递归调用的栈空间,最多为n层。 ","date":"2024-02-11","objectID":"/lianbiao_fanzhuan/:1:2","tags":["leetcode","LinkList"],"title":"链表反转","uri":"/lianbiao_fanzhuan/"},{"categories":["leetcode","LinkList"],"content":"这篇文章展示了两两交换链表中的节点.","date":"2024-02-11","objectID":"/lianbiao_liangliangfanzhuan/","tags":["leetcode","LinkList"],"title":"两两交换链表中的节点","uri":"/lianbiao_liangliangfanzhuan/"},{"categories":["leetcode","LinkList"],"content":"两两交换链表中的节点 两两交换链表中的节点 ","date":"2024-02-11","objectID":"/lianbiao_liangliangfanzhuan/:1:0","tags":["leetcode","LinkList"],"title":"两两交换链表中的节点","uri":"/lianbiao_liangliangfanzhuan/"},{"categories":["leetcode","LinkList"],"content":"方法一:迭代 创建哑结点 dummyHead,令 dummyHead.next = head。令 temp 表示当前到达的节点,初始时 temp = dummyHead。每次需要交换 temp 后面的两个节点。 如果 temp 的后面没有节点或者只有一个节点,则没有更多的节点需要交换,因此结束交换。否则,获得 temp 后面的两个节点 node1 和 node2,通过更新节点的指针关系实现两两交换节点。 具体而言,交换之前的节点关系是 temp -\u003e node1 -\u003e node2,交换之后的节点关系要变成 temp -\u003e node2 -\u003e node1,因此需要进行如下操作。 temp.next = node2 node1.next = node2.next node2.next = node1 完成上述操作之后,节点关系即变成 temp -\u003e node2 -\u003e node1。再令 temp = node1,对链表中的其余节点进行两两交换,直到全部节点都被两两交换。 两两交换链表中的节点之后,新的链表的头节点是 dummyHead.next,返回新的链表的头节点即可。 func swapPairs(head *ListNode) *ListNode { dummyHead := \u0026ListNode{0, head} temp := dummyHead for temp.Next != nil \u0026\u0026 temp.Next.Next != nil { node1 := temp.Next node2 := temp.Next.Next temp.Next = node2 node1.Next = node2.Next node2.Next = node1 temp = node1 } return dummyHead.Next } 复杂度分析 时间复杂度:O(n),其中n是链表的长度。需要遍历链表一次。 空间复杂度:O(1)。 ","date":"2024-02-11","objectID":"/lianbiao_liangliangfanzhuan/:1:1","tags":["leetcode","LinkList"],"title":"两两交换链表中的节点","uri":"/lianbiao_liangliangfanzhuan/"},{"categories":["Data"],"content":"这篇文章展示了如何高效学习数据结构.","date":"2024-02-11","objectID":"/how_to_learn/","tags":["Data"],"title":"如何高效学习数据结构","uri":"/how_to_learn/"},{"categories":["Data"],"content":"什么是数据结构?什么是算法? 从广义上讲,数据结构就是指一组数据的存储结构。算法就是操作数据的一组方法。 ","date":"2024-02-11","objectID":"/how_to_learn/:1:0","tags":["Data"],"title":"如何高效学习数据结构","uri":"/how_to_learn/"},{"categories":["Data"],"content":"关系 数据结构和算法是相辅相成的。数据结构是为算法服务的,算法要作用在特定的 数据结构之上。 因此,我们无法孤立数据结构来讲算法,也无法孤立算法来讲数据结构。 ","date":"2024-02-11","objectID":"/how_to_learn/:1:1","tags":["Data"],"title":"如何高效学习数据结构","uri":"/how_to_learn/"},{"categories":["Data"],"content":"学习的重点在什么地方? 想要学习数据结构与算法,首先要掌握一个数据结构与算法中最重要的概念——复杂度分 析。 不要只是死记硬背,不要为了学习而学习, 而是要学习它的“来历”“自身的特点”“适合解决的问题”以及“实际的应用场景”。 ","date":"2024-02-11","objectID":"/how_to_learn/:2:0","tags":["Data"],"title":"如何高效学习数据结构","uri":"/how_to_learn/"},{"categories":["Data"],"content":"一些可以让你事半功倍的学习技巧 ","date":"2024-02-11","objectID":"/how_to_learn/:3:0","tags":["Data"],"title":"如何高效学习数据结构","uri":"/how_to_learn/"},{"categories":["Data"],"content":"边学边练,适度刷题 可以“适度”刷题,但一定不要浪费太多时间在刷题上。我们学习的目的还 是掌握,然后应用。 ","date":"2024-02-11","objectID":"/how_to_learn/:3:1","tags":["Data"],"title":"如何高效学习数据结构","uri":"/how_to_learn/"},{"categories":["Data"],"content":"多问、多思考、多互动 学习最好的方法是,找到几个人一起学习,一块儿讨论切磋,有问题及时寻求老师答疑。 ","date":"2024-02-11","objectID":"/how_to_learn/:3:2","tags":["Data"],"title":"如何高效学习数据结构","uri":"/how_to_learn/"},{"categories":["Data"],"content":"打怪升级学习法 学习的过程中,我们碰到最大的问题就是,坚持不下来。 所以,我们在枯燥的学习过程中,也可以给自己设立一个切实可行的目标,就像打怪升级一 样。 ","date":"2024-02-11","objectID":"/how_to_learn/:3:3","tags":["Data"],"title":"如何高效学习数据结构","uri":"/how_to_learn/"},{"categories":["Data"],"content":"知识需要沉淀,不要想试图一下子掌握所有 学习知识的过程 是反复迭代、不断沉淀的过程。 ","date":"2024-02-11","objectID":"/how_to_learn/:3:4","tags":["Data"],"title":"如何高效学习数据结构","uri":"/how_to_learn/"},{"categories":["Data"],"content":"这篇文章展示了数据结构.","date":"2024-02-11","objectID":"/struct/","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"数组 ","date":"2024-02-11","objectID":"/struct/:1:0","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"如何实现随机访问? 数组是最常用的数据结构,创建数组必须要内存中一块连续的空间,并且数组中必须存放相同的数据类型 随机快速读写是数组的一个重要特性,但是要随机访问数据,必须知道数据在数组中的下标。如果我们只是知道数据的值,想要在数组中找到这个值,那么就只能遍历整个数组,时间复杂度为 O(N) 数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。 ","date":"2024-02-11","objectID":"/struct/:1:1","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"低效的“插入”和“删除” 插入 如果数组中的数据是有序的,我们在某个位置插入一个新的元素时,就必须按照刚才的方法 搬移 k 之后的数据。但是,如果数组中存储的数据并没有任何规律,数组只是被当作一个 存储数据的集合。在这种情况下,如果要将某个数组插入到第 k 个位置,为了避免大规模 的数据搬移,我们还有一个简单的办法就是,直接将第 k 位的数据搬移到数组元素的最 后,把新的元素直接放入第 k 个位置。 删除 实际上,在某些特殊场景下,我们并不一定非得追求数组中数据的连续性。如果我们将多次 删除操作集中在一起执行,删除的效率是不是会提高很多呢? 警惕数组的访问越界问题 ","date":"2024-02-11","objectID":"/struct/:1:2","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"链表 链表可以使用零散的内存空间存储数据。 链表中的每个数据元素都必须包含一个指向下一个数据元素的内存地址指针。 因为链表是不连续存储的,要想在链表中查找一个数据,只能遍历链表,所以链表的查找复杂度总是 O(N)。 在链表中插入或者删除一个数据是非常容易的,只要找到要插入(删除)的位置,修改链表指针就可以了。 ","date":"2024-02-11","objectID":"/struct/:2:0","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"单链表 把第一个结点叫作头结点,把最后一个结点叫作尾结 点。其中,头结点用来记录链表的基地址。有了它,我们就可以遍历得到整条链表。而尾结 点特殊的地方是:指针不是指向下一个结点,而是指向一个空地址 NULL,表示这是链表 上最后一个结点。 ","date":"2024-02-11","objectID":"/struct/:2:1","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"循环链表 循环链表是一种特殊的单链表。实际上,循环链表也很简单。它跟单链表唯一的区别就在尾 结点。我们知道,单链表的尾结点指针指向空地址,表示这就是最后的结点了。而循环链表 的尾结点指针是指向链表的头结点。 ","date":"2024-02-11","objectID":"/struct/:2:2","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"双向链表 双向链表,顾名 思义,它支持两个方向,每个结点不止有一个后继指针 next 指向后面的结点,还有一个前 驱指针 prev 指向前面的结点。 从结构上来看,双向链表可以支持 O(1) 时间复杂度的情况下找到前驱结点,正是这样的特 点,也使双向链表在某些情况下的插入、删除等操作都要比单链表简单、高效。 用空间换时间的设计思想。当内 存空间充足的时候,如果我们更加追求代码的执行速度,我们就可以选择空间复杂度相对较 高、但时间复杂度相对很低的算法或者数据结构。相反,如果内存比较紧缺,比如代码跑在 手机或者单片机上,这个时候,就要反过来用时间换空间的设计思路。 ","date":"2024-02-11","objectID":"/struct/:2:3","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"如何写链表 理解指针或引用的含义 将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中 存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。 警惕指针丢失和内存泄漏 如图所示,我们希望在结点 a 和相邻的结点 b 之间插入结点 x,假设当前指针 p 指向结点 a。如果我们将代码实现变成下面这个样子,就会发生指针丢失和内存泄露。 p-\u003enext = x; // 将 p 的 next 指针指向 x 结点; x-\u003enext = p-\u003enext; // 将 x 的结点的 next 指针指向 b 结点; 初学者经常会在这儿犯错。p-\u003enext 指针在完成第一步操作之后,已经不再指向结点 b 了,而是指向结点 x。第 2 行代码相当于将 x 赋值给 x-\u003enext,自己指向自己。因此,整 个链表也就断成了两半,从结点 b 往后的所有结点都无法访问到了。 利用哨兵简化实现难度 如果我们引入哨兵结点,在任何时候,不管链表是不是空,head 指针都会一直指向这个哨 兵结点。我们也把这种有哨兵结点的链表叫带头链表。相反,没有哨兵结点的链表就叫作不 带头链表。 重点留意边界条件处理 我经常用来检查链表代码是否正确的边界条件有这样几个: 如果链表为空时,代码是否能正常工作? 如果链表只包含一个结点时,代码是否能正常工作? 如果链表只包含两个结点时,代码是否能正常工作? 代码逻辑在处理头结点和尾结点的时候,是否能正常工作? 举例画图,辅助思考 多写多练,没有捷径 单链表反转 链表中环的检测 两个有序的链表合并 删除链表倒数第 n 个结点 求链表的中间结点 ","date":"2024-02-11","objectID":"/struct/:2:4","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"栈 栈就是在线性表的基础上加了这样的操作限制条件:后面添加的数据,在删除的时候必须先删除,即通常所说的“后进先出” 栈在线性表的基础上增加了操作限制,具体实现的时候,因为栈不需要随机访问、也不需要在中间添加、删除数据,所以可以用数组实现,也可以用链表实现。 用数组实现的栈,我们叫作顺序 栈,用链表实现的栈,我们叫作链式栈。 ","date":"2024-02-11","objectID":"/struct/:3:0","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"如何实现一个栈 // 基于数组实现的顺序栈 public class ArrayStack { private String[] items; // 数组 private int count; // 栈中元素个数 private int n; // 栈的大小 // 初始化数组,申请一个大小为 n 的数组空间 public ArrayStack(int n) { this.items = new String[n]; this.n = n; this.count = 0; } // 入栈操作 public boolean push(String item) { // 数组空间不够了,直接返回 false,入栈失败。 if (count == n) return false; // 将 item 放到下标为 count 的位置,并且 count 加一 items[count] = item; ++count; return true; } // 出栈操作 public String pop() { // 栈为空,则直接返回 null if (count == 0) return null; // 返回下标为 count-1 的数组元素,并且栈中元素个数 count 减一 String tmp = items[count-1]; --count; return tmp; } } 存储数据只需要一个大小为 n 的数组就够了。在入栈和出 栈过程中,只需要一两个临时变量存储空间,所以空间复杂度是 O(1)。 注意,这里存储数据需要一个大小为 n 的数组,并不是说空间复杂度就是 O(n)。因为,这 n 个空间是必须的,无法省掉。所以我们说空间复杂度的时候,是指除了原本的数据存储空 间外,算法运行还需要额外的存储空间。 时间复杂度也不难。不管是顺序栈还是链式栈,入栈、出栈 只涉及栈顶个别数据的操作,所以时间复杂度都是 O(1)。 ","date":"2024-02-11","objectID":"/struct/:3:1","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"栈在函数调用中的应用 操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种 结构, 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入 栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。为了让你更好地理 解,我们一块来看下这段代码的执行过程。 ","date":"2024-02-11","objectID":"/struct/:3:2","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"栈在表达式求值中的应用 ","date":"2024-02-11","objectID":"/struct/:3:3","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"栈在括号匹配中的应用 ","date":"2024-02-11","objectID":"/struct/:3:4","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"队列 队列是先进先出 ","date":"2024-02-11","objectID":"/struct/:4:0","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"如何理解队列 栈只支持两个基本操作:入栈 push()和出栈 pop()。队列跟栈非常相似,支持 的操作也很有限,最基本的操作也是两个:入队 enqueue(),放一个数据到队列尾部;出 队 dequeue(),从队列头部取一个元素。 跟栈一样,队列可以用数组来实现,也可以用链表来实现。用数组实现的栈叫作顺序栈,用 链表实现的栈叫作链式栈。同样,用数组实现的队列叫作顺序队列,用链表实现的队列叫作 链式队列。 // 用数组实现的队列 public class ArrayQueue { // 数组:items,数组大小:n private String[] items; private int n = 0; // head 表示队头下标,tail 表示队尾下标 private int head = 0; private int tail = 0; // 申请一个大小为 capacity 的数组 public ArrayQueue(int capacity) { items = new String[capacity]; n = capacity; } // 入队 public boolean enqueue(String item) { // 如果 tail == n 表示队列已经满了 if (tail == n) return false; items[tail] = item; ++tail; return true; } // 出队 public String dequeue() { // 如果 head == tail 表示队列为空 if (head == tail) return null; // 为了让其他语言的同学看的更加明确,把 -- 操作放到单独一行来写了 String ret = items[head]; ++head; return ret; } } ","date":"2024-02-11","objectID":"/struct/:4:1","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"循环队列 在用数组实现的非循环队列中,队满的判断条件是 tail == n,队空的判断条件是 head == tail。那针对循环队列,如何判断队空和队满呢? 队列为空的判断条件仍然是 head == tail。但队列满的判断条件就稍微有点复杂了。 当队满时,(tail+1)%n=head,tail 指向的位置实际上是没有存储数据的。所以,循 环队列会浪费一个数组的存储空间。 ","date":"2024-02-11","objectID":"/struct/:4:2","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"阻塞队列和并发队列 阻塞队列其实就是在队列基础上增加了阻塞操作。简单来说,就是在队列为空的时候,从队 头取数据会被阻塞。因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已 经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返 回。 阻塞队列,在多线程情况下,会有多个线程同时操作队列,这个时候就会存在 线程安全问题,那如何实现一个线程安全的队列呢? 线程安全的队列我们叫作并发队列。最简单直接的实现方式是直接在 enqueue()、 dequeue() 方法上加锁,但是锁粒度大并发度会比较低,同一时刻仅允许一个存或者取操 作。实际上,基于数组的循环队列,利用 CAS 原子操作,可以实现非常高效的并发队列。 这也是循环队列比链式队列应用更加广泛的原因 ","date":"2024-02-11","objectID":"/struct/:4:3","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"hash表 Hash 表中数据以 Key、Value 的方式存储 Hash 表的物理存储其实是一个数组,如果我们能够根据 Key 计算出数组下标,那么就可以快速在数组中查找到需要的 Key 和 Value。 不同的 Key 有可能计算得到相同的数组下标,这就是所谓的 Hash 冲突,解决 Hash 冲突常用的方法是链表法。 因为数组要求存储固定数据类型,主要目的是每个数组元素中要存放固定长度的数据。 所以,数组中存储的是 Key、Value 数据元素的地址指针。一旦发生 Hash 冲突,只需要将相同下标,不同 Key 的数据元素添加到这个链表就可以了。查找的时候再遍历这个链表, 匹配正确的 Key。 ","date":"2024-02-11","objectID":"/struct/:5:0","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"递归 ","date":"2024-02-11","objectID":"/struct/:6:0","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"递归需要满足的三个条件 一个问题的解可以分解为几个子问题的解 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样 存在递归终止条件 写递归代码的关键就是找到如何将大问题分解为小问题的规律,并且基于此写 出递推公式,然后再推敲终止条件,最后将递推公式和终止条件翻译成代码。 编写递归代码的关键是,只要遇到递归,我们就把它抽象成一个递推公式,不用想一 层层的调用关系,不要试图用人脑去分解递归的每个步骤。 ","date":"2024-02-11","objectID":"/struct/:6:1","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"递归代码要警惕堆栈溢出 最大允许的递归深度跟当前线程剩余的栈空间大小有 关,事先无法计算。如果实时计算,代码过于复杂,就会影响代码的可读性。所以,如果最 大深度比较小,比如 10、50,就可以用这种方法,否则这种方法并不是很实用。 ","date":"2024-02-11","objectID":"/struct/:6:2","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"递归代码要警惕重复计算 ","date":"2024-02-11","objectID":"/struct/:6:3","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"排序 ","date":"2024-02-11","objectID":"/struct/:7:0","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"冒泡排序 可以看出,经过一次冒泡操作之后,6 这个元素已经存储在正确的位置上。要想完成所有数 据的排序,我们只要进行 6 次这样的冒泡操作就行了。 // 冒泡排序,a 表示数组,n 表示数组大小 public void bubbleSort(int[] a, int n) { if (n \u003c= 1) return; for (int i = 0; i \u003c n; ++i) { // 提前退出冒泡循环的标志位 boolean flag = false; for (int j = 0; j \u003c n - i - 1; ++j) { if (a[j] \u003e a[j+1]) { // 交换 int tmp = a[j]; a[j] = a[j+1]; a[j+1] = tmp; flag = true; // 表示有数据交换 } } if (!flag) break; // 没有数据交换,提前退出 } } 第一,冒泡排序是原地排序算法吗? 冒泡的过程只涉及相邻数据的交换操作,只需要常量级的临时空间,所以它的空间复杂度为 O(1),是一个原地排序算法。 第二,冒泡排序是稳定的排序算法吗? 在冒泡排序中,只有交换才可以改变两个元素的前后顺序。为了保证冒泡排序算法的稳定 性,当有相邻的两个元素大小相等的时候,我们不做交换,相同大小的数据在排序前后不会 改变顺序,所以冒泡排序是稳定的排序算法。 第三,冒泡排序的时间复杂度是多少? 最好情况下,要排序的数据已经是有序的了,我们只需要进行一次冒泡操作,就可以结束 了,所以最好情况时间复杂度是 O(n)。而最坏的情况是,要排序的数据刚好是倒序排列 的,我们需要进行 n 次冒泡操作,所以最坏情况时间复杂度为 O(n2)。 对于包含 n 个数据的数组,这 n 个数据就有 n! 种排列方式。不同的排列方式,冒泡排序执 行的时间肯定是不同的。比如我们前面举的那两个例子,其中一个要进行 6 次冒泡,而另 一个只需要 4 次。如果用概率论方法定量分析平均时间复杂度,涉及的数学推理和计算就 会很复杂。我这里还有一种思路,通过“有序度”和“逆序度”这两个概念来进行分析。 有序度是数组中具有有序关系的元素对的个数。 同理,对于一个倒序排列的数组,比如 6,5,4,3,2,1,有序度是 0;对于一个完全有 序的数组,比如 1,2,3,4,5,6,有序度就是n*(n-1)/2,也就是 15。我们把这种完全 有序的数组的有序度叫作满有序度。 逆序度 = 满有序度 - 有序度。 ","date":"2024-02-11","objectID":"/struct/:7:1","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"插入排序 // 插入排序,a 表示数组,n 表示数组大小 public void insertionSort(int[] a, int n) { if (n \u003c= 1) return; for (int i = 1; i \u003c n; ++i) { int value = a[i]; int j = i - 1; // 查找插入的位置 for (; j \u003e= 0; --j) { if (a[j] \u003e value) { a[j+1] = a[j]; // 数据移动 } else { break; } } a[j+1] = value; // 插入数据 } } 第一,插入排序是原地排序算法吗? 从实现过程可以很明显地看出,插入排序算法的运行并不需要额外的存储空间,所以空间复 杂度是 O(1),也就是说,这是一个原地排序算法。 第二,插入排序是稳定的排序算法吗? 在插入排序中,对于值相同的元素,我们可以选择将后面出现的元素,插入到前面出现元素 的后面,这样就可以保持原有的前后顺序不变,所以插入排序是稳定的排序算法。 第三,插入排序的时间复杂度是多少? 如果要排序的数据已经是有序的,我们并不需要搬移任何数据。如果我们从尾到头在有序数 据组里面查找插入位置,每次只需要比较一个数据就能确定插入的位置。所以这种情况下, 最好是时间复杂度为 O(n)。注意,这里是从尾到头遍历已经有序的数据。 ","date":"2024-02-11","objectID":"/struct/:7:2","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"选择排序 选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序 每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。 首先,选择排序空间复杂度为 O(1),是一种原地排序算法。选择排序的最好情况时间复杂 度、最坏情况和平均情况时间复杂度都为O(n2) 选择排序是一种不稳定的排序算法。从我前面画的那张图中,你可以看出 来,选择排序每次都要找剩余未排序元素中的最小值,并和前面的元素交换位置,这样破坏 了稳定性。 ","date":"2024-02-11","objectID":"/struct/:7:3","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"归并排序 如果要排序一个数组,我们先把数组从中间分成前后两 部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有 序了。 ","date":"2024-02-11","objectID":"/struct/:7:4","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"快速排序 如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r 之间的任意一个数据作为 pivot(分区点)。 我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。 根据分治、递归的处理思想,我们可以用递归排序下标从 p 到 q-1 之间的数据和下标从 q+1 到 r 之间的数据,直到区间缩小为 1,就说明所有的数据都有序了。 如果我们用递推公式来将上面的过程写出来的话,就是这样: 递推公式: quick_sort(p...r) = quick_sort(p...q-1) + quick_sort(q+1, r) 终止条件: p \u003e= r 我将递推公式转化成递归代码。跟归并排序一样,我还是用伪代码来实现,你可以翻译成你 熟悉的任何语言。 // 快速排序,A 是数组,n 表示数组的大小 quick_sort(A, n) { quick_sort_c(A, 0, n-1) } // 快速排序递归函数,p,r 为下标 quick_sort_c(A, p, r) { if p \u003e= r then return q = partition(A, p, r) // 获取分区点 quick_sort_c(A, p, q-1) quick_sort_c(A, q+1, r) } ","date":"2024-02-11","objectID":"/struct/:7:5","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Data"],"content":"树 非线性表 ","date":"2024-02-11","objectID":"/struct/:8:0","tags":["Data"],"title":"数据结构","uri":"/struct/"},{"categories":["Manage"],"content":"这篇文章展示了解决的问题是什么相关知识.","date":"2024-02-09","objectID":"/what_is_problem/","tags":["Manage"],"title":"解决的问题是什么?","uri":"/what_is_problem/"},{"categories":["Manage"],"content":"不要把解决方案当作问题的定义,而忽略了真正要解决的问题是什么 会议讨论的重点就是解决方案本身:这个功能怎么做,这个技术怎么应用落 地。而不是讨论真正的问题是什么:为了解决真正的问题,这个功能是不是必须要做,有没 有更好的解决办法;这个技术是不是必须要上,能不能带来足够的价值。 ","date":"2024-02-09","objectID":"/what_is_problem/:0:1","tags":["Manage"],"title":"解决的问题是什么?","uri":"/what_is_problem/"},{"categories":["Manage"],"content":"你不需要去解决别人的问题,你只需要提醒他问题的存在 日常开发中,产品、运营、开发、测 试、运维,也有很多交互合作,需要互相帮助;哪些问题对方可以轻易解决,哪些问题应该 通过修改软件功能来解决,应该思考清楚。 ","date":"2024-02-09","objectID":"/what_is_problem/:0:2","tags":["Manage"],"title":"解决的问题是什么?","uri":"/what_is_problem/"},{"categories":["Manage"],"content":"鱼是最后一个看到水的,身处问题之中的人往往并不觉得有问题 身处问题之中的人常常并不能感知到问题的存在,正如身在水中的鱼儿看不到水一样。太多 的问题被人们的适应能力忽略掉了,直到有人解决了这些问题,身处其中的人才恍然,原来 过去的方式都是有问题的。 所以,如果你到一个新环境中,发现存在着一些问题,而身处其中的人却熟视无睹,往往不 是他们有问题,也不是你有问题,可能只是他们已经适应了问题的存在,而你还没有适应。 关于问题的定义有个公式:问题 = 期望 - 体验。 ","date":"2024-02-09","objectID":"/what_is_problem/:0:3","tags":["Manage"],"title":"解决的问题是什么?","uri":"/what_is_problem/"},{"categories":["Manage"],"content":"这篇文章展示了如何成长相关知识.","date":"2024-02-09","objectID":"/how_to_change/","tags":["Manage"],"title":"如何成长","uri":"/how_to_change/"},{"categories":["Manage"],"content":"德雷福斯模型 德雷福斯是一个专业人员能力成长模型,这个模型认为所有专 业人员都需要经历 5 个成长阶段,不管是医生还是律师,或者是软件开发,任何专业技能 的从业者都需要经历新手、高级新手、胜任者、精通者、专家 5 个阶段。 ","date":"2024-02-09","objectID":"/how_to_change/:0:1","tags":["Manage"],"title":"如何成长","uri":"/how_to_change/"},{"categories":["Manage"],"content":"如何在工作中成长 勇于承担责任 好的技术都是经过现实锤炼的,能够真正解决现实问题的,得到大多数人拥护的。所以自己 去学习各种各样的新技术固然重要,但是更重要的是要将这些技术应用到实践中,去领悟技 术背后的原理和思想。 而所有真正的领悟都是痛的领悟,只有你对自己工作的结果承担责任和后果,在出现问题或 者可能出现问题的时候,倒逼自己思考技术的关键点,技术的缺陷与优势,才能真正地理解 这项技术。 如果你只是去遵循别人的指令,按别人的规则去做事情,你永远不会知道事物的真相是什 么。只有你对结果负责的时候,在压力之下,你才会看透事物的本质,才会抓住技术的核心 和关键,才能够让你去学好技术,用好技术,在团队中承担核心的技术职责和产生自己的技 术影响,并巩固自己的技术地位。 在实践中保持技能 有个说法叫做 1 万小时定律,是说要想成为某个领域的专家,必须经过 1 万小时高强度的 训练才可以,对软件开发这样更强调技术的领域来说,这一点尤其明显。我们必须要经过长 时间的编程实践,从持续的编程实践中提升技术认知,才能够理解技术的精髓,感悟到技术 的真谛。 但是 1 万小时的编程时间并不是说你重复的编程 1 万小时就能够自动提升成为专家的。真 正对你有帮助的是不断超越自我,挑战自我的工作。也就是说,每一次在完成一个工作以 后,下一次的工作都要比上一次的工作难度再增加一点点,不断地让自己去挑战更高难度的 工作,从而拥有更高的技术能力和技术认知。 关注问题场景 现实中,很多人觉得,学好某一个技术就大功告成了。但事实上是,即使你熟练掌握了强大 的技术,但如果对问题不了解,对上下文缺乏感知,也不会真正地用好技术,也就无法去解 决真正的问题。试图用自己擅长的技术去解决所有问题,就好像是拿着锤子去找钉子,敲敲 打打大半天,才发现打的根本就不是一个钉子。 所谓的专家其实是善于根据问题场景发现解决方法的那个人,如果你关注场景,根据场景去 寻找解决办法,也许你会发现解决问题的办法可能会非常简单,也许并不需要多么高深的工 具和方法就能够解决,这时候你才能成为真正的专家。也就是在这个时候你会意识到方法、 技术、工具这些都不是最复杂的,而真正复杂的是问题的场景,是如何真正地理解问题。 这个世界没有万能的方法,没有一劳永逸的银弹。每一种方法都有适用的场景,每一种技术 都有优点和缺点,你必须要理解问题的关键细节、上下文场景,才能够选择出最合适的技术 方案,真正地解决问题 ","date":"2024-02-09","objectID":"/how_to_change/:0:2","tags":["Manage"],"title":"如何成长","uri":"/how_to_change/"},{"categories":["Manage"],"content":"这篇文章展示了如何解决问题相关知识.","date":"2024-02-09","objectID":"/how_to_solve_problem/","tags":["Manage"],"title":"如何解决问题","uri":"/how_to_solve_problem/"},{"categories":["Manage"],"content":"如果某人能够解决问题,而他自己却感受不到问题,那么就让他感受一下 在工作合作的过程中,有的时候,对于对方来说,明明是举手之劳的事情,但他偏偏在拖 延,你去催促也没什么效果。这时候,我们很容易将问题归结为对方的工作态度有问题,事 实上,很多时候,其实是对方没有理解你的问题,他觉得你在没事找事,你才是工作态度有 问题的人。 将问题归结为人的态度问题,大多数情况下,是无法解决问题的,况且,很多时候确实不是 态度问题,而是不同的人做事能力、理解能力、立场和看待事物的角度不同而已。所以,如 果只是立场和角度的问题,那么就可以将对方拉到同一个立场来解决问题,如果对方没有感 受到问题,那么想办法让对方感受一下问题。 通常说来,上司的能力要比你的能力强,调动的资源也比你多,有些事情对你而言可能非常 困难,但是你上司也许一句话就可以搞定,这个时候,你可以考虑利用你的上司去解决问 题。如果他没有感觉到问题,那么想办法让他感觉到问题。 所以有句话叫做:用人的最高境界是用上司。 有的时候,对于一件有风险的工作,如果你自己做决策,那么当事情不顺利的时候,你可能 无法承担风险,那么你就应该将你的上司拉进来。你可以直接问他:有这样一个方案和计 划,你觉得合适吗?但是这种提问方式,可能会导致你的方案被上司否定。更好的提问方式 是:这里有 A、B 两个方案,你觉得哪个方案更合适?从而将上司的回答引导到你期望的方 案上面去。 而上司一旦回答了你的问题,就等于参与到你工作中去了,当事情出现风险的时候,你再去 找他寻求支持的时候,因为是他曾经做出的决策,那么他更容易跟你站在一起,帮你解决问 题。 这里要注意的是,当你寻求上司支持的时候,不要问上司怎么办,不要给上司提开放式的问 题。一则上司可能不理解你的问题上下文,无法给出合适的建议,从而使上司和你都难堪。 再则上司如果给出的方案是你难以执行的,你是在给自己挖坑往里跳。 而封闭式的问题,只需要回答好不好就可以,比如选择 A 方案还是 B 方案,就不会有上面 的问题。 ","date":"2024-02-09","objectID":"/how_to_solve_problem/:0:1","tags":["Manage"],"title":"如何解决问题","uri":"/how_to_solve_problem/"},{"categories":["Manage"],"content":"直言有讳 在合作的过程中,合作的小伙伴可能犯一些错误,如果这个错误影响了你,你应该指出来, 而不是为了和谐假装视而不见,任由事情向失败的方向滑落。但是,这里要注意的是,你指 出错误是为了改正错误,达成目标,而不是为了责备、打压对方,也就是所谓的:要批评而 不要责难,要对事而不要对人。 如果你针对人,那么对方就一定和你处在对立的一面,你们就是在进行人际斗争,而不是在 解决问题。直言有讳就是说,指出负面情况的时候,要直接,不要兜圈子、说含糊话,否则 你的语言就没有力量,无法解决问题,但是也不要想说什么就说什么,要有所避讳,主要就 是不要把问题指向人。可以说这件事情这样做是不对的,但是不要说你这个人是有问题的。 以赞成的方式表示反对。当他要反对一个技术方案的时候,他先 是表示赞成,他会说,这个方案很好,然后从设计、价值几个方面快速说几个比较好的点, 这个时候,方案的设计者就和他站在同一个立场上了,将他接纳为自己人。接着,他就会将 话题转换,他会说:“但是,我还有几个小小的疑问和建议。”然后,他会说出他反对的观 点,而设计方案的提出者因为已经从内心接纳了他,所以能够认真倾听他的疑问和建议,重 新思考自己的方案。 还有一种情况,就是有些新来的同事,会针对公司现状提出各种建议和方案,这些方案和建 议有的并不靠谱,但是,如果你直接指出其中的不靠谱之处,可能会非常打击新同事的积极 性,他们甚至会怀疑公司的合作氛围。 这种情况下,适当的逃避问题,反而是一种解决问题的正确方法。可以跟新同事说:我今天 比较忙,改天我们组织个会议详细聊。将讨论的时间推后,将讨论的门槛提高(组织会 议),新同事将有时间更严肃地思考他的方案,他会自己发现方案的问题,自己放弃这个方 案。这样的结果,对新同事,对同事之间的关系,对公司都有好处。 ","date":"2024-02-09","objectID":"/how_to_solve_problem/:0:2","tags":["Manage"],"title":"如何解决问题","uri":"/how_to_solve_problem/"},{"categories":["Manage"],"content":"如果你想解决一个大家都不关注的问题,那么试试让问题变得更糟 如果你觉得这里真的有问题,需要尽快解决,那么就不要试图对问题进行修修补补, 使问题被拖得越来越久。也许你放任问题发生,尽快暴露出问题,反而却使大家对问题的严 重性达成一致意见,完全支持你去解决问题。 ","date":"2024-02-09","objectID":"/how_to_solve_problem/:0:3","tags":["Manage"],"title":"如何解决问题","uri":"/how_to_solve_problem/"},{"categories":["Manage"],"content":"如果你不填老师想要的答案,你就是个傻瓜 如果你觉得一个问题很重要,而你的上司却不觉得,那么你辛苦去解决这个问题可能 就是在白费功夫。你无法在一个管理体系中获得认可,你的工作无法获得正反馈,你的努力 是无法持续的。 所以,如果这个问题真的很重要,而你无法让你的上司认可其重要性,那么对于你而言,真 正严重的问题不是问题本身,而是你的上司本身。 ","date":"2024-02-09","objectID":"/how_to_solve_problem/:0:4","tags":["Manage"],"title":"如何解决问题","uri":"/how_to_solve_problem/"},{"categories":["File"],"content":"这篇文章展示了文件系统相关知识.","date":"2024-02-09","objectID":"/system/","tags":["File"],"title":"文件系统","uri":"/system/"},{"categories":["File"],"content":"硬盘 硬盘是一种可持久保存、多次读写数据的存储介质 机械式硬盘的数据就存储在具有磁性特质的盘片上,因此这种硬盘也被称为磁盘 固态硬盘则没有这种磁性特质的存储介质,也没有电机驱动的机械式结构 ","date":"2024-02-09","objectID":"/system/:0:1","tags":["File"],"title":"文件系统","uri":"/system/"},{"categories":["File"],"content":"文件系统 inode 中记录着文件权限、所有者、修改时间和文件大小等文件属性信息,以及文件数据块硬盘地址索引。inode 是固定结构的,能够记录的硬盘地址索引数也是固定的,只有 15 个索引。其中前 12 个索引直接记录数据块地址,第 13 个索引记录索引地址,也就是说, 索引块指向的硬盘数据块并不直接记录文件数据,而是记录文件数据块的索引表,每个索引表可以记录 256 个索引;第 14 个索引记录二级索引地址,第 15 个索引记录三级索引地址 ","date":"2024-02-09","objectID":"/system/:0:2","tags":["File"],"title":"文件系统","uri":"/system/"},{"categories":["File"],"content":"RAID RAID,即独立硬盘冗余阵列,将多块硬盘通过硬件 RAID 卡或者软件 RAID 的方案管理起来,使其共同对外提供服务。 常用 RAID 有五种,分别是 RAID 0、RAID 1、RAID 10、RAID 5 和 RAID 6。 ","date":"2024-02-09","objectID":"/system/:0:3","tags":["File"],"title":"文件系统","uri":"/system/"},{"categories":["File"],"content":"分布式文件系统 DataNode 负责文件数据的存储和读写操作,HDFS 将文件数据分割成若干数据块(Block),每个 DataNode 存储一部分数据块,这样文件就分布存储在整个 HDFS 服务器集群中。 NameNode 负责整个分布式文件系统的元数据(MetaData)管理,也就是文件路径名、访问权限、数据块的 ID 以及存储位置等信息,相当于 Linux 系统中 inode 的角色。HDFS 为了保证数据的高可用,会将一个数据块复制为多份(缺省情况为3份),并将多份相同的数据块存储在不同的服务器上,甚至不同的机架上。这样当有硬盘损坏,或者某个 DataNode 服务器宕机,甚至某个交换机宕机,导致其存储的数据块不能访问的时候,客户端会查找其备份的数据块进行访问。 DFS为了保证数据的高可用,会将一个数据块复制为多份(缺省情况为3份),并将多份相同的数据块存储在不同的服务器上,甚至不同的机架上。这样当有硬盘损坏,或者某个DataNode服务器宕机,甚至某个交换机宕机,导致其存储的数据块不能访问的时候,客户端会查找其备份的数据块进行访问。 ","date":"2024-02-09","objectID":"/system/:0:4","tags":["File"],"title":"文件系统","uri":"/system/"},{"categories":["Manage"],"content":"这篇文章展示了文件系统相关知识.","date":"2024-02-09","objectID":"/skill_manage/","tags":["Manage"],"title":"文件系统","uri":"/skill_manage/"},{"categories":["Manage"],"content":"彼得定律 彼得在 20 世纪 70 年代,研究了美国数千个组织,包括政府部门、学校、企业等各种类型 的组织后,发现,在一个成熟有效的组织中,当一个员工在其岗位能够出色完成工作,就会 得到晋升,被提拔到更高一级职位。如果在这个职位,他能够继续出色完成工作,就会继续 得到晋升,直到他晋升到某个职位以后,无法出色完成工作为止。 这是职场晋升的一般规则,看起来似乎也没什么,但是彼得在对这些得到晋升的人进行各种 观察以后,得到一个结论:在一个层级组织中,每个员工都会趋向于晋升到他所不能胜任的 职位。这就是彼得定律,事实上,我们根据晋升的一般规则,也能推导出这个定律。利用这 个定律做进一步的推导,还能得到一个彼得定律的推论:**一个成熟的组织中,所有的职位都 被不能够胜任它的人承担着。**这个推论也很好理解,每个人都会晋升到他不能胜任的职位, 那么稳定下来以后,所有的职位都被不能胜任的人承担。不得不说这个结论实在是让人有点 吃惊,但是却很好地解释了组织中的各种奇怪现象。 彼得进一步对这些不能胜任自己职位的人进行观察,发现当一个人位于他不能胜任的职位上 时,他必须投入全部的精力才能有效完成工作,这个职位也被称作这个人的彼得高地。一个 处于彼得高地的人,精疲力尽于他手头的工作,就无法再进行更进一步的思考和学习,他的 个人能力提升和职业进步都将止步于此。 所以,一个人在其职业生涯中能够晋升的最高职位,能够在专业技能上进化的最高阶段,依 赖于他的专业能力和综合素养,依赖于他拥有的持续学习和专业训练的条件与环境。和他晋 升的速度无关,有时候也许恰恰相反。 对公司而言,真正有价值的是你为公司解决了多少问题,而不是完成了多少工作,工作本身 没有意义,解决问题才有意义。对于你自己而言,真正有价值的不是你获得了多快的晋升, 多高的加薪,而是你获得了多少持续高强度训练的机会。而这两者,本质上是统一的。 所以,对自己未来有更多期待,更有进取心的工程师们,应该将精力更多放在发现企业中的 各种问题并致力于去解决问题,在这个过程中,你将同步收获职场晋升和个人能力提升。 ","date":"2024-02-09","objectID":"/skill_manage/:0:1","tags":["Manage"],"title":"文件系统","uri":"/skill_manage/"},{"categories":["Manage"],"content":"用目标驱动 在技术管理领域,常见的管理方式有两种,一种是问题驱动型管理,一种是流程驱动型管 理。 问题驱动型管理着眼于问题,每天关注最新的问题是什么,然后解决问题。流程驱动型管理 着眼于流程,关注事情的进展是否符合流程规范,是否在有序的规章制度下行事,看起来像 监工。 老实说,这两种都不是高效的管理方法。对于技术管理而言,更高效的管理方式是目标型管 理。 目标驱动的管理者并不特别关注问题,他更关注解决方案。当系统出现故障的时候,他不会 关注是谁导致的 bug,他更关注谁可以解决这个 bug。当项目进展缓慢,他并不关注是谁 导致了拖延,他更关注我们如何做才能赶上进度。他不问问题为什么出现,因为他知道,所 有的问题最后都是人的问题,而纠结于人的问题,只能导致人和人的扯皮。 目标驱动的管理者其实并不是不关注问题,他只是不用问题进行管理,不让团队纠结于问题 之中,而是去着眼于未来和解决方案本身。管理者自身其实对问题非常清楚,但是他把问题 转化为目标,引导团队前行。 ","date":"2024-02-09","objectID":"/skill_manage/:0:2","tags":["Manage"],"title":"文件系统","uri":"/skill_manage/"},{"categories":["Mysql"],"content":"这篇文章展示了Mysql Scheme内容.","date":"2024-02-08","objectID":"/mysql_scheme/","tags":["Mysql"],"title":"Mysql Scheme","uri":"/mysql_scheme/"},{"categories":["Mysql"],"content":"选择优化的数据类型 字符串类型 VARCHAR类型用于存储可变长的字符串,所以它需要1或2个额外的字节记录字符串的长度:如果列的长度小于或等于255个字节,则只使用1个字节表示,否则使用2个字节表示。例如varchar(10)就需要11个字节,varchar(1000)则需要1002个字节。 VARCHAR节省了存储空间,所以对性能有所帮助。但由于行是变长的,在UPDATE时可能是原来的行更长,这就会导致需要做一些额外的工作。如果一个行占用的空间曾长,并且在页内没有更多的空间可以存储,这是INNODB就会分裂当前页来使行可以放进页内。 下面这些情况使用VARCHAR是合适的: 字符串列的最大长度比平均长度大很多 列的更新很少 使用了UTF-8这样的字符集,每个字符都是用不同的字节存储 CHAR类型是定长的:MySQL总是根据定义字符串的长度分配足够空间。因为CHAR会根据需要采用空格填充到字符串末尾,而且当你检索时,CHAR会删除末尾的空格。所以会有一个很有趣的事情发生,当你存储一个\"Johnson “到char(10)时,检索出来的结果却是\"Johnson”,因为MySQL并不知道这空格是你存的还是系统自动填充的。 CHAR很适合存储很短的字符串或所有值都接近同一个长度。例如密码的MD5值。 BLOB和TEXT都是为了存储很大的数据类型而设计的字符串数据类型,分别采用二进制和字符方式存储。而且当它们存储的数据过大时,INNOSB会使用专门的‘外部’空间来存储数据,此时每个值的行内仅存储一个1 ~ 4个字节的指针,然后在外部区域存储真实的指。当需要对BLOB和TEXT排序时,它只对每个列的最前 max_sort_length 进行排序。这个值是可以配置的。 ","date":"2024-02-08","objectID":"/mysql_scheme/:0:1","tags":["Mysql"],"title":"Mysql Scheme","uri":"/mysql_scheme/"},{"categories":["Tool"],"content":"这篇文章展示了vim.","date":"2024-01-17","objectID":"/vim/","tags":["Tool"],"title":"vim","uri":"/vim/"},{"categories":["Tool"],"content":"存活 ","date":"2024-01-17","objectID":"/vim/:1:0","tags":["Tool"],"title":"vim","uri":"/vim/"},{"categories":["Tool"],"content":"模式切换 i → Insert 模式,按 ESC 回到 Normal 模式. x → 删当前光标所在的一个字符。 :wq → 存盘 + 退出 (:w 存盘, :q 退出) 技巧 :w 后可以跟文件名 dd → 删除当前行,并把删除的行存到剪贴板里 p → 粘贴剪贴板 注意 hjkl (墙裂推荐使用其移动光标,但不必需) 你也可以使用光标键 (←↓↑→). 注: j 就像下箭头。 :help → 显示相关命令的帮助。你也可以就输入 :help 而不跟命令。(退出帮助需要输入:q) ","date":"2024-01-17","objectID":"/vim/:1:1","tags":["Tool"],"title":"vim","uri":"/vim/"},{"categories":["Tool"],"content":"感觉良好 ","date":"2024-01-17","objectID":"/vim/:2:0","tags":["Tool"],"title":"vim","uri":"/vim/"},{"categories":["Tool"],"content":"各种插入模式 a → 在光标后插入 o → 在当前行后插入一个新行 O → 在当前行前插入一个新行 cw → 替换从光标所在位置后到一个单词结尾的字符 ","date":"2024-01-17","objectID":"/vim/:2:1","tags":["Tool"],"title":"vim","uri":"/vim/"},{"categories":["Tool"],"content":"简单的移动光标 0 → 数字零,到行头 ^ → 到本行第一个不是blank字符的位置(所谓blank字符就是空格,tab,换行,回车等) $ → 到本行行尾 g_ → 到本行最后一个不是blank字符的位置。 /pattern → 搜索 pattern 的字符串 注意 如果搜索出多个匹配,可按n键到下一个 ","date":"2024-01-17","objectID":"/vim/:2:2","tags":["Tool"],"title":"vim","uri":"/vim/"},{"categories":["Tool"],"content":"拷贝/粘贴 P → 粘贴 yy → 拷贝当前行当行于 ddP ","date":"2024-01-17","objectID":"/vim/:2:3","tags":["Tool"],"title":"vim","uri":"/vim/"},{"categories":["Tool"],"content":"Undo/Redo u → undo → redo ","date":"2024-01-17","objectID":"/vim/:2:4","tags":["Tool"],"title":"vim","uri":"/vim/"},{"categories":["Tool"],"content":"打开/保存/退出/改变文件(Buffer) :e \u003cpath/to/file\u003e → 打开一个文件 :w → 存盘 :saveas \u003cpath/to/file\u003e → 另存为 \u003cpath/to/file\u003e :x, ZZ 或 :wq → 保存并退出 (:x 表示仅在需要时保存,ZZ不需要输入冒号并回车) :q! → 退出不保存 :qa! 强行退出所有的正在编辑的文件,就算别的文件有更改。 :bn 和 :bp → 你可以同时打开很多文件,使用这两个命令来切换下一个或上一个文件。 注意 我喜欢使用:n到下一个文件) ","date":"2024-01-17","objectID":"/vim/:2:5","tags":["Tool"],"title":"vim","uri":"/vim/"},{"categories":["Tool"],"content":"更好,更强,更快 ","date":"2024-01-17","objectID":"/vim/:3:0","tags":["Tool"],"title":"vim","uri":"/vim/"},{"categories":["Tool"],"content":"重复 . → (小数点) 可以重复上一次的命令 N → 重复某个命令N次 NG → 到第 N 行 (陈皓注:注意命令中的G是大写的,另我一般使用 : N 到第N行,如 :137 到第137行) gg → 到第一行。(陈皓注:相当于1G,或 :1) G → 到最后一行。 按单词移动: ","date":"2024-01-17","objectID":"/vim/:3:1","tags":["Tool"],"title":"vim","uri":"/vim/"},{"categories":["Docker"],"content":"这篇文章展示了Docker基本知识.","date":"2024-01-14","objectID":"/architecture/","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"Docker架构及底层技术 ","date":"2024-01-14","objectID":"/architecture/:1:0","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"Docker Engine 后台进程(dockerd) Rest Api Server CLI接口 ","date":"2024-01-14","objectID":"/architecture/:1:1","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"架构 ","date":"2024-01-14","objectID":"/architecture/:1:2","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"底层技术支持 Namespaces:做隔离pid,net,ipc,mnt,uts Control groups: 做资源限制 Union file systems:Container和 image的分层 ","date":"2024-01-14","objectID":"/architecture/:1:3","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"Docker Image ","date":"2024-01-14","objectID":"/architecture/:2:0","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"定义 文件和meta data的集合 分层的,并且每一层都可以添加改变删除文件,成为一个新的 image 不同的 image可以共享相同的 layer 本身是 read-only的 ","date":"2024-01-14","objectID":"/architecture/:2:1","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"获取 Build from Dockerfile Pull from Registry ","date":"2024-01-14","objectID":"/architecture/:2:2","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"Docker Container ","date":"2024-01-14","objectID":"/architecture/:3:0","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"定义 通过 Image创建 在 Image layer之上建立一个 container layer 类比面向对象:类和实例 Image负责 app的存储和分发,Container负责运行 app ","date":"2024-01-14","objectID":"/architecture/:3:1","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"构建自己的镜像 docker commit container_name repositry_name/tag_name ","date":"2024-01-14","objectID":"/architecture/:3:2","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"Dockerfile语法梳理及最佳实践 ","date":"2024-01-14","objectID":"/architecture/:4:0","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"From 尽量使用官方的 image FROM scratch # 制作 base image FROM centos # 使用 base image FROM ubuntu:14.04 ","date":"2024-01-14","objectID":"/architecture/:4:1","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"Label LABEL maintainer=\"email.com\" LABEL version=\"1.0\" LABEL description=\"this is description\" ","date":"2024-01-14","objectID":"/architecture/:4:2","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"RUN 复杂的 RUN请用反斜线换行 避免无用分层,合并多条命令成一行 RUN yum update \u0026\u0026 yum install -y vim \\ python-dev ","date":"2024-01-14","objectID":"/architecture/:4:3","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"WORKDIR 不要使用 RUN cd WORKDIR /root WORKDIR /test #如果没有会自动创建 test目录 ","date":"2024-01-14","objectID":"/architecture/:4:4","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"ADD and COPY COPY优于 ADD ADD 除了 COPY还有额外的功能(解压) 添加远程文件/目录请使用 curl或者 wget ADD hello / ADD test.tar.gz / # 添加到根目录并解压 WORDIR /root COPY hello test/ ","date":"2024-01-14","objectID":"/architecture/:4:5","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"ENV 尽量使用 ENV ENV MYSQL_VERSION 5.6 RUN apt-get install -y mysql-server= \"${MYSQL_VERSION}\" ","date":"2024-01-14","objectID":"/architecture/:4:6","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"VOLUME and EXPOSE ","date":"2024-01-14","objectID":"/architecture/:4:7","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"CMD and ENTRYPOINT CMD 设置容器启动时默认执行的命令和参数 如果 doker run制定了其他命令,CMD命令将被忽略 如果定义了多个 CMD,只有最后一个会执行 ENTRYPOINT 设置容器启动时运行的命令 让容器以应用程序或者服务的形式运行 不会被忽略,一定会执行 最佳实践:写一个 shell脚本作为 entrypoint Shell 格式 RUN apt-get install -y vim CMD echo \"hello docker\" ENTRYPOINT echo \"hello docker\" Exec 格式 RUN [\"apt-get\", \"install\", \"-y\", \"vim\"] CMD [\"/bin/echo\", \"hello docker\"] ENTRYPOINT [\"/bin/echo\", \"hello docker\"] ","date":"2024-01-14","objectID":"/architecture/:4:8","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"最佳实践 FROM python:2.7 LABEL \"maintaioner=mail.com\" RUN pip install flask COPY app.py /app/ WORKDIR /app EXPOSE 5000 CMD [\"python\", \"app.py\"] ","date":"2024-01-14","objectID":"/architecture/:4:9","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"镜像的发布 docker login docker push dockerhub_id/name 推荐的做法是跟 github做关联 ","date":"2024-01-14","objectID":"/architecture/:5:0","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"Docker Network ","date":"2024-01-14","objectID":"/architecture/:6:0","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"网络的分层 ISO/OSI 7层 TCP/IP 5 层 公有 ip和私有 ip Public IP:可以访问 Internet Private IP:不可访问 internet A类 10.0.0.0–10.255.255.255 B类 172.16.0.0–172.31.255.255 C类 192.168.0.0–192.168.255.255 网络地址转换 NAT ping和 telnet ping(ICMP):验证 ip的可达性 telnet:验证服务的可用性 wireshark ","date":"2024-01-14","objectID":"/architecture/:6:1","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"Linux网络命名空间 主机和容器网络隔离,容器具有自己独立的网络命名空间,且他们也可以互通 ip netns add test1 ip netns add test2 ip netns list ip netns exec test1 ip a ip netns exec test1 ip link set dev lo up ip link add veth-test1 type veth peer name veth-test2 ip link set veth-test1 netns test1 ip link set veth-test2 netns test2 ip netns exec test1 ip addr add 192.168.1.1/24 dev veth-test1 ip netns exec test2 ip addr add 192.168.1.2/24 dev veth-test2 ip netns exec test1 ip link set dev veth-test1 up ip netns exec test2 ip link set dev veth-test2 up ip netns exec test1 ping 192.168.1.2 ip netns exec test2 ping 192.168.1.1 ","date":"2024-01-14","objectID":"/architecture/:6:2","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"Bridge0详解 ","date":"2024-01-14","objectID":"/architecture/:6:3","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"容器之间的 link 一个容器如果想访问到另外一个容器,可以通过link的方式,知道对方的network name即可访问 link是有方向限制的 如果是连接到用户自定义的 bridge network,默认两者网络都已关联,没有方向限制 ","date":"2024-01-14","objectID":"/architecture/:6:4","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"单机 Bridge Network docker network create -d bridge my-bridge brctl show #可以看到创建好的 bridge docker run -d --name test --network my-bridge busybox docker network connect my-bridge test Host Network host模式类似于Vmware的桥接模式,与宿主机在同一个网络中,但没有独立IP地址。 docker run --name=nginx --net=host -p 80:80 -d nginx None Network 容器有自己的 network namespace,但是没有做任何的网络配置。 ","date":"2024-01-14","objectID":"/architecture/:6:5","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"多机 Overlay Network docker network create -d overlay demo ","date":"2024-01-14","objectID":"/architecture/:6:6","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"Docker的持久化存储和数据共享 ","date":"2024-01-14","objectID":"/architecture/:7:0","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"Data Volume docker run -v mysql:/var/lib/mysql ","date":"2024-01-14","objectID":"/architecture/:7:1","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"Bind Mouting docker run -v /home/aaa:/root/aaa ","date":"2024-01-14","objectID":"/architecture/:7:2","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"持久化方案 基于本地文件系统的 Volume 基于 plugin的 Volume,例如 NAS,aws ","date":"2024-01-14","objectID":"/architecture/:7:3","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"Volume 类型 受管理的 data Volume,由 docker后台自动创建 绑定挂载的 Volume,具体挂载位置可以由用户指定 ","date":"2024-01-14","objectID":"/architecture/:7:4","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"Docker Compose ","date":"2024-01-14","objectID":"/architecture/:8:0","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"定义 Docker Compose是一个工具,可以通过一个 yml文件定义多容器的 docker应用 ","date":"2024-01-14","objectID":"/architecture/:8:1","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"三大概念 Services 一个 service代表一个 container Service的启动类似 docker run,可以给其指定 network和 volume Networks Volumes ","date":"2024-01-14","objectID":"/architecture/:8:2","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"水平扩容和负载均衡 docker-compose up --scale web=2 -d 负载均衡 lb: image: dockercloud/haproxy links: - web ports: 8080:80 volumes: - /var/run/docker.sock:/var/run/docker.sock ","date":"2024-01-14","objectID":"/architecture/:8:3","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"容器编排kubenetes ","date":"2024-01-14","objectID":"/architecture/:9:0","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"ReplicaSet和 Replication Controller ","date":"2024-01-14","objectID":"/architecture/:9:1","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"Deployment kubectl create -f deployment.yml kubectl get deployment -o wide ##升级 kubectl set image deployment nginx-deployment nginx=nginx:1.13 kubectl rollout history deployment nginx-deployment ##回滚 kubectl rollout undo deployment nginx-deployment 技巧 自动补全: kubectl completion zsh source \u003c(kubectl completion zsh) ","date":"2024-01-14","objectID":"/architecture/:9:2","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"Services ","date":"2024-01-14","objectID":"/architecture/:9:3","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"ClusterIP pod改变,service的 clusterIP不会改变 内部使用 ","date":"2024-01-14","objectID":"/architecture/:9:4","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"NodePort kubectl expose pods nginx-pod --type=NodePort 有端口范围限制 ","date":"2024-01-14","objectID":"/architecture/:9:5","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"LoadBalancer kubectl expose pods nginx-pod --type=LoadBalancer ","date":"2024-01-14","objectID":"/architecture/:9:6","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"External name ","date":"2024-01-14","objectID":"/architecture/:9:7","tags":["Docker"],"title":"Docker","uri":"/architecture/"},{"categories":["Docker"],"content":"这篇文章展示了容器监控基本知识.","date":"2024-01-14","objectID":"/monitor/","tags":["Docker"],"title":"容器监控","uri":"/monitor/"},{"categories":["Docker"],"content":"基本监控 ","date":"2024-01-14","objectID":"/monitor/:1:0","tags":["Docker"],"title":"容器监控","uri":"/monitor/"},{"categories":["Docker"],"content":"单个容器 docker ps -a docker top container_name ","date":"2024-01-14","objectID":"/monitor/:1:1","tags":["Docker"],"title":"容器监控","uri":"/monitor/"},{"categories":["Docker"],"content":"weavescope图形化界面 scope ","date":"2024-01-14","objectID":"/monitor/:1:2","tags":["Docker"],"title":"容器监控","uri":"/monitor/"},{"categories":["Docker"],"content":"集群 grafana ","date":"2024-01-14","objectID":"/monitor/:1:3","tags":["Docker"],"title":"容器监控","uri":"/monitor/"},{"categories":["Docker"],"content":"根据资源占用自动横向伸缩 kubectl run php-apache --image=us.gcr.io/k8s-artifacts-prod/hpa-example --requests=cpu=200m --expose --port=80 kubectl autoscale deployment php-apache --cpu-percent=50 --min=1 --max=10 kubectl get horizontalpodautoscaler ","date":"2024-01-14","objectID":"/monitor/:2:0","tags":["Docker"],"title":"容器监控","uri":"/monitor/"},{"categories":["Docker"],"content":"日志 ELK ","date":"2024-01-14","objectID":"/monitor/:3:0","tags":["Docker"],"title":"容器监控","uri":"/monitor/"},{"categories":["Docker"],"content":"Prometheus Prometheus ","date":"2024-01-14","objectID":"/monitor/:4:0","tags":["Docker"],"title":"容器监控","uri":"/monitor/"},{"categories":["Design"],"content":"这篇文章展示了软件建模基本知识.","date":"2024-01-14","objectID":"/modeling/","tags":["Design"],"title":"软件建模","uri":"/modeling/"},{"categories":["Design"],"content":"软件建模 模型是对客观存在的抽象 抽象表达事务的本质规律 把握事物的本质规律和主要特征,正确建造模型和使用模型,以防在各种细节中迷失方向。 一个是我们要解决的领域问题 另一个客观存在就是最终开发出来的软件系统 ","date":"2024-01-14","objectID":"/modeling/:1:0","tags":["Design"],"title":"软件建模","uri":"/modeling/"},{"categories":["Design"],"content":"4+1视图模型 逻辑视图 描述软件的功能逻辑,由哪些模块组成,模块中包含那些类,其依赖关系如何。 开发视图 包括系统架构层面的层次划分,包的管理,依赖的系统与第三方的程序包。 开发视图某些方面和逻辑视图有一定重复性,不同视角看到的可能是同一个东西,开发视图中一个程序包,可能正好对应逻辑视图中的一个功能模块。 过程视图 描述程序运行期的进程、线程、对象实例,以及与此相关的并发、同步、通信等问题。 物理视图 描述软件如何安装并部署到物理的服务上,以及不同的服务器之间如何关联、通信。 场景视图 针对具体的用例场景,将上述 4 个视图关联起来,一方面从业务角度描述, 功能流程如何完成,一方面从软件角度描述,相关组成部分如何互相依赖、调用。 ","date":"2024-01-14","objectID":"/modeling/:2:0","tags":["Design"],"title":"软件建模","uri":"/modeling/"},{"categories":["Design"],"content":"UML建模 一方面满足设计阶段和各个相关方沟通的目的;一方面可以用来思考,即使软件开发过程不需要跟其他人沟通,或者还没到沟通的时候,依然可以使用 UML 建模画图,帮助自己进行设计思考。 ","date":"2024-01-14","objectID":"/modeling/:3:0","tags":["Design"],"title":"软件建模","uri":"/modeling/"},{"categories":["Design"],"content":"这篇文章展示了软件设计文档基本知识.","date":"2024-01-13","objectID":"/doc/","tags":["Design"],"title":"软件设计文档","uri":"/doc/"},{"categories":["Design"],"content":"类图 类之间有 6 种静态关系:关联、依赖、组合、聚合、继承、泛化。 ","date":"2024-01-13","objectID":"/doc/:1:0","tags":["Design"],"title":"软件设计文档","uri":"/doc/"},{"categories":["Design"],"content":"序列图 序列图则用来描述参与者之间的动态调用关系。 ","date":"2024-01-13","objectID":"/doc/:2:0","tags":["Design"],"title":"软件设计文档","uri":"/doc/"},{"categories":["Design"],"content":"组件图 一方面满足设计阶段和各个相关方沟通的目的;一方面可以用来思考,即使软件开发过程不需要跟其他人沟通,或者还没到沟通的时候,依然可以使用 UML 建模画图,帮助自己进行设计思考。 ","date":"2024-01-13","objectID":"/doc/:3:0","tags":["Design"],"title":"软件设计文档","uri":"/doc/"},{"categories":["Design"],"content":"部署图 ","date":"2024-01-13","objectID":"/doc/:4:0","tags":["Design"],"title":"软件设计文档","uri":"/doc/"},{"categories":["Design"],"content":"用例图 用例图主要用在需求分析阶段,通过反映用户和软件系统的交互,描述系统的功能需求 ","date":"2024-01-13","objectID":"/doc/:5:0","tags":["Design"],"title":"软件设计文档","uri":"/doc/"},{"categories":["Design"],"content":"状态图 状态图用来展示单个对象生命周期的状态变迁 ","date":"2024-01-13","objectID":"/doc/:6:0","tags":["Design"],"title":"软件设计文档","uri":"/doc/"},{"categories":["Design"],"content":"活动图 活动图主要用来描述过程逻辑和业务流程。 ","date":"2024-01-13","objectID":"/doc/:7:0","tags":["Design"],"title":"软件设计文档","uri":"/doc/"},{"categories":["Go"],"content":"这篇文章展示了channel用法","date":"2024-01-12","objectID":"/channel/","tags":["Go"],"title":"channel用法","uri":"/channel/"},{"categories":["Go"],"content":"CSP 允许使用进程组件来描述系统,它们独立运行,并且只通过消息传递的方式通信 ","date":"2024-01-12","objectID":"/channel/:0:0","tags":["Go"],"title":"channel用法","uri":"/channel/"},{"categories":["Go"],"content":"channel的应用场景 执行业务处理的 goroutine 不要通过共享内存的方式通信,而是要通过 Channel 通信的方式分享数据 五种类型: 数据交流 数据传递 信号通知 任务编排 锁 ","date":"2024-01-12","objectID":"/channel/:0:1","tags":["Go"],"title":"channel用法","uri":"/channel/"},{"categories":["Go"],"content":"channel的基本用法 只能接收、只能发送、既可以接收又可以发送三种类型 发送数据 ch \u003c- 2000 接收数据 x := \u003c-ch //把接收的一条数据赋值给变量x foo(\u003c-ch) //把接收的一个数据作为参数传给函数 \u003c-ch //丢弃接收的一条数据 其他操作 send和recv都可以作为select语句的case clause 可以用于for-range语句中 ","date":"2024-01-12","objectID":"/channel/:0:2","tags":["Go"],"title":"channel用法","uri":"/channel/"},{"categories":["Go"],"content":"实现原理 数据结构 qcount:代表 chan 中已经接收但还没被取走的元素的个数。内建函数 len 可以返回这个字段的值。 dataqsiz:队列的大小。chan 使用一个循环队列来存放元素,循环队列很适合这种生产者 - 消费者的场景(我很好奇为什么这个字段省略 size 中的 e)。 buf:存放元素的循环队列的 buffer。 elemtype 和 elemsize:chan 中元素的类型和 size。因为 chan 一旦声明,它的元素类型是固定的,即普通类型或者指针类型,所以元素大小也是固定的 sendx:处理发送数据的指针在 buf 中的位置。一旦接收了新的数据,指针就会加上elemsize,移向下一个位置。buf 的总大小是 elemsize 的整数倍,而且 buf 是一个循环列表。 recvx:处理接收请求时的指针在 buf 中的位置。一旦取出数据,此指针会移动到下一个位置。 recvq:chan 是多生产者多消费者的模式,如果消费者因为没有数据可读而被阻塞了,就会被加入到 recvq 队列中。 sendq:如果生产者因为 buf 满了而阻塞,会被加入到 sendq 队列 初始化 Go 在编译的时候,会根据容量的大小选择调用 makechan64,还是 makechan。 我们只关注 makechan 就好了,因为 makechan64 只是做了 size 检查,底层还是调用makechan 实现的。makechan 的目标就是生成 hchan 对象。 send Go 在编译发送数据给 chan 的时候,会把 send 语句转换成 chansend1 函数,chansend1 函数会调用 chansend recv 在处理从 chan 中接收数据时,Go 会把代码转换成 chanrecv1 函数,如果要返回两个返回值,会转换成 chanrecv2,chanrecv1 函数和 chanrecv2 会调用 chanrecv。 close 通过 close 函数,可以把 chan 关闭,编译器会替换成 closechan 方法的调用 ","date":"2024-01-12","objectID":"/channel/:0:3","tags":["Go"],"title":"channel用法","uri":"/channel/"},{"categories":["Go"],"content":"容易犯的错误 常见panic close 为 nil 的 chan; send 已经 close 的 chan; close 已经 close 的 chan goroutine泄漏: func process(timeout time.Duration) bool { ch := make(chan bool) go func() { // 模拟处理耗时的业务 time.Sleep((timeout + time.Second)) ch \u003c- true // block fmt.Println(\"exit goroutine\") }() select { case result := \u003c-ch: return result case \u003c-time.After(timeout): return false } } 在这个例子中,process 函数会启动一个 goroutine,去处理需要长时间处理的业务,处理完之后,会发送 true 到 chan 中,目的是通知其它等待的 goroutine,可以继续处理了。 我们来看一下第 10 行到第 15 行,主 goroutine 接收到任务处理完成的通知,或者超时后就返回了。这段代码有问题吗? 如果发生超时,process 函数就返回了,这就会导致 unbuffered 的 chan 从来就没有被读取。我们知道,unbuffered chan 必须等 reader 和 writer 都准备好了才能交流,否则就会阻塞。超时导致未读,结果就是子 goroutine 就阻塞在第 7 行永远结束不了,进而导致 goroutine 泄漏。 解决这个 Bug 的办法很简单,就是将 unbuffered chan 改成容量为 1 的 chan,这样第 7 行就不会被阻塞了。 ","date":"2024-01-12","objectID":"/channel/:0:4","tags":["Go"],"title":"channel用法","uri":"/channel/"},{"categories":["Go"],"content":"选择方法 共享资源的并发访问使用传统并发原语; 复杂的任务编排和消息传递使用 Channel; 消息通知机制使用 Channel,除非只想 signal 一个 goroutine,才使用 Cond 简单等待所有任务的完成用 WaitGroup,也有 Channel 的推崇者用 Channel,都可以; 需要和 Select 语句结合,使用 Channel; 需要和超时配合时,使用 Channel 和 Context。 nil empty full not full\u0026empty closed receive block block read value read value 返回未读的元素,读完后返回零值 send block write value block write value panic closed panic closed,没有未读元素 closed,保留未读元素 closed,保留未读元素 panic ","date":"2024-01-12","objectID":"/channel/:0:5","tags":["Go"],"title":"channel用法","uri":"/channel/"},{"categories":["Go"],"content":"这篇文章展示了cond用法","date":"2024-01-12","objectID":"/cond/","tags":["Go"],"title":"cond用法","uri":"/cond/"},{"categories":["Go"],"content":"go标准库cond cond通常应用于等待某个条件一组gorotine,等条件变为true的时候,其中一个goroutine或者所有goroutine都会被唤醒执行 ","date":"2024-01-12","objectID":"/cond/:1:0","tags":["Go"],"title":"cond用法","uri":"/cond/"},{"categories":["Go"],"content":"cond的基本用法 ","date":"2024-01-12","objectID":"/cond/:2:0","tags":["Go"],"title":"cond用法","uri":"/cond/"},{"categories":["Go"],"content":"signal 允许调用者caller唤醒一个等待此cond的goroutine ","date":"2024-01-12","objectID":"/cond/:2:1","tags":["Go"],"title":"cond用法","uri":"/cond/"},{"categories":["Go"],"content":"broadcase 允许调用者caller唤醒所有等待此cond的goroutine ","date":"2024-01-12","objectID":"/cond/:2:2","tags":["Go"],"title":"cond用法","uri":"/cond/"},{"categories":["Go"],"content":"wait 会把调用者caller放入cond的等待队列中并阻塞,直到被signal或者broadcast的方法从等待队列中移除并唤醒 ","date":"2024-01-12","objectID":"/cond/:2:3","tags":["Go"],"title":"cond用法","uri":"/cond/"},{"categories":["Go"],"content":"使用cond的2个常见错误 调用wait的时候没有加锁 只调用了一次wait,没有检查等待条件是否满足,结果条件没满足。程序就继续执行 ","date":"2024-01-12","objectID":"/cond/:3:0","tags":["Go"],"title":"cond用法","uri":"/cond/"},{"categories":["Go"],"content":"知名项目中cond的使用 ","date":"2024-01-12","objectID":"/cond/:4:0","tags":["Go"],"title":"cond用法","uri":"/cond/"},{"categories":["Go"],"content":"K8s 定义了优先队列PriorityQueue这样一个数据结构,用来实现pod的调用 ","date":"2024-01-12","objectID":"/cond/:4:1","tags":["Go"],"title":"cond用法","uri":"/cond/"},{"categories":["Go"],"content":"cond有3点特性,是channel无法替代的 cond和一个locker关联,可以利用这个locker对相关的依赖条件更改提供保护 cond可以同时支持signal和broadcast方法,而channel只能同时支持其中一个 cond的broadcast方法可以被重复调用 ","date":"2024-01-12","objectID":"/cond/:4:2","tags":["Go"],"title":"cond用法","uri":"/cond/"},{"categories":["Go"],"content":"使用案例 var done = false func read(name string, c *sync.Cond) { c.L.Lock() for !done { c.Wait() } log.Println(name, \"starts reading\") c.L.Unlock() } func write(name string, c *sync.Cond) { log.Println(name, \"starts writing\") time.Sleep(time.Second) c.L.Lock() done = true c.L.Unlock() log.Println(name, \"wakes all\") c.Broadcast() } func main() { cond := sync.NewCond(\u0026sync.Mutex{}) go read(\"reader1\", cond) go read(\"reader2\", cond) go read(\"reader3\", cond) write(\"writer\", cond) time.Sleep(time.Second * 3) } ","date":"2024-01-12","objectID":"/cond/:5:0","tags":["Go"],"title":"cond用法","uri":"/cond/"},{"categories":["Go"],"content":"这篇文章展示了context用法","date":"2024-01-12","objectID":"/context/","tags":["Go"],"title":"context用法","uri":"/context/"},{"categories":["Go"],"content":"来历 Go在1.7的版本中才正式把context加入到标准库中 ","date":"2024-01-12","objectID":"/context/:1:0","tags":["Go"],"title":"context用法","uri":"/context/"},{"categories":["Go"],"content":"适用场景 ","date":"2024-01-12","objectID":"/context/:2:0","tags":["Go"],"title":"context用法","uri":"/context/"},{"categories":["Go"],"content":"上下文信息传递 ","date":"2024-01-12","objectID":"/context/:2:1","tags":["Go"],"title":"context用法","uri":"/context/"},{"categories":["Go"],"content":"控制子goroutine的运行 ","date":"2024-01-12","objectID":"/context/:2:2","tags":["Go"],"title":"context用法","uri":"/context/"},{"categories":["Go"],"content":"超时控制 ","date":"2024-01-12","objectID":"/context/:2:3","tags":["Go"],"title":"context用法","uri":"/context/"},{"categories":["Go"],"content":"可以取消的方法调用 ","date":"2024-01-12","objectID":"/context/:2:4","tags":["Go"],"title":"context用法","uri":"/context/"},{"categories":["Go"],"content":"基本使用方法 ","date":"2024-01-12","objectID":"/context/:3:0","tags":["Go"],"title":"context用法","uri":"/context/"},{"categories":["Go"],"content":"4个实现方法 deadline方法:会返回这个context会被取消的截止日期 done方法:返回一个channel对象 Err:如果done没有close,Err方法返回nil;如果done被close,Err会返回done被close的原因 value返回此ctx中和制定的key相关联的value ","date":"2024-01-12","objectID":"/context/:3:1","tags":["Go"],"title":"context用法","uri":"/context/"},{"categories":["Go"],"content":"常用的生成顶层context的方法 Context.Backgroud():返回一个非nil、空的Context,没有任何之,不会被cancel,不会超时,没有截止日期 Context.TODO():返回一个非nil、空的context,没有任何值,不会被cancel,不会超时,没有截止日期 ","date":"2024-01-12","objectID":"/context/:3:2","tags":["Go"],"title":"context用法","uri":"/context/"},{"categories":["Go"],"content":"适用规则 一般函数使用context的时候,会把这个参数放在第一个参数的位置 从来不把nil当作context类型的参数值,可以使用context.Backgroud()创建一个空的上下文对象,也不要使用nil context只用来临时做函数之间的上下文透传,不能持久化context或者context长久保存 key的类型不应该使用字符串类型或者其他内建类型,否则容易在包之间使用context时候冲突 常常使用struct{}作为底层类型定义key的类型 ","date":"2024-01-12","objectID":"/context/:3:3","tags":["Go"],"title":"context用法","uri":"/context/"},{"categories":["Go"],"content":"创建特殊用途context的方法 WithValue:基于parent context生成一个新的context,保存了一个key-value简直对,常常用来传递上下文 WithCancel:返回parent的副本,只是副本中的done channel是新建的对象,他的类型是cancelCtx WithTimeout:其实和WithDeadline一样,只不过一个参数是超时时间,一个参数是截止时间 WithDeadline:返回一个parent的副本,并且设置了一个不晚于参数d的截止时间,类型为timeCtx ","date":"2024-01-12","objectID":"/context/:4:0","tags":["Go"],"title":"context用法","uri":"/context/"},{"categories":["Go"],"content":"这篇文章展示了map用法","date":"2024-01-12","objectID":"/map/","tags":["Go"],"title":"map用法","uri":"/map/"},{"categories":["Go"],"content":"go内建的map类型 map的类型是map[key] key类型的k必须是可比较的 map[key]函数返回结果可以是一个值,也可以是两个值 map是无序的,想要保证遍历mao时元素有序,可以使用orderedmap ","date":"2024-01-12","objectID":"/map/:0:1","tags":["Go"],"title":"map用法","uri":"/map/"},{"categories":["Go"],"content":"使用map的2种常见错误 常见错误一:未初始化 常见错误二:并发读写 ","date":"2024-01-12","objectID":"/map/:0:2","tags":["Go"],"title":"map用法","uri":"/map/"},{"categories":["Go"],"content":"如何实现线程安全的map类型 加读写锁:扩展map,支持并发读写 分片加锁:更高效的并发map getShard是一个关键的方法,能够根据key计算出分片索引 ","date":"2024-01-12","objectID":"/map/:0:3","tags":["Go"],"title":"map用法","uri":"/map/"},{"categories":["Go"],"content":"应对特殊场景的sync.map 应用场景不多 设计与实现 store方法 load方法 delete方法 loadAndDelete方法 loadOrStore方法 range方法 实现优化点 空间换时间 优先从read字段读取,更新,删除 动态调整 double-checking 延迟删除 ","date":"2024-01-12","objectID":"/map/:0:4","tags":["Go"],"title":"map用法","uri":"/map/"},{"categories":["Go"],"content":"这篇文章展示了mutex用法","date":"2024-01-12","objectID":"/mutex/","tags":["Go"],"title":"mutex用法","uri":"/mutex/"},{"categories":["Go"],"content":"互斥锁的实现机制 在并发编程中,如果程序中的一部分会被并发访问或修改,那么,为了避免并发访问导致的意想不到的结果,这部分程序需要被保护起来,这部分被保护起来的程序,就叫做临界区 使用互斥锁,限定临界区只能同时由一个线程持有 ","date":"2024-01-12","objectID":"/mutex/:1:0","tags":["Go"],"title":"mutex用法","uri":"/mutex/"},{"categories":["Go"],"content":"适用场景: 共享资源。并发地读写共享资源,会出现数据竞争(data race)的问题,所以需要Mutex、RWMutex 这样的并发原语来保护。 ","date":"2024-01-12","objectID":"/mutex/:1:1","tags":["Go"],"title":"mutex用法","uri":"/mutex/"},{"categories":["Go"],"content":"Mutex的基本使用方法 互斥锁 Mutex 就提供两个方法 Lock 和 Unlock:进入临界区之前调用 Lock方法,退出临界区的时候调用 Unlock 方法 当一个 goroutine 通过调用 Lock 方法获得了这个锁的拥有权后,其它请求锁的goroutine 就会阻塞在 Lock 方法的调用上,直到锁被释放并且自己获取到了这个锁的拥有权。 Go race detector是基于 Google 的 C/C++ sanitizers 技术实现的,编译器通过探测所有的内存访问,加入代码能监视对这些内存地址的访问(读还是写)。在代码运行的时候,race detector 就能监控到对共享变量的非同步访问,出现 race 的时候,就会打印出警告信息 ","date":"2024-01-12","objectID":"/mutex/:2:0","tags":["Go"],"title":"mutex用法","uri":"/mutex/"},{"categories":["Go"],"content":"基本用法 func main() { var mu sync.Mutex var count = 0 var wg sync.WaitGroup wg.Add(10) for i := 0; i \u003c 10; i++ { go func() { defer wg.Done() for j := 0; j \u003c 100000; j++ { mu.Lock() count++ mu.Unlock() } }() } wg.Wait() fmt.Println(count) } 很多情况下,Mutex 会嵌入到其它 struct 中使用 package main import ( \"fmt\" \"sync\" ) type Counter struct { CounterType int Name string mu sync.Mutex count uint64 } func (c *Counter) Incr() { c.mu.Lock() c.count++ c.mu.Unlock() } func (c *Counter) Count() uint64 { c.mu.Lock() defer c.mu.Unlock() return c.count } func main() { var counter Counter var wg sync.WaitGroup wg.Add(10) for i := 0; i \u003c 10; i++ { go func() { defer wg.Done() for j := 0; j \u003c 100000; j++ { counter.Incr() } }() } wg.Wait() fmt.Println(counter.Count()) } ","date":"2024-01-12","objectID":"/mutex/:3:0","tags":["Go"],"title":"mutex用法","uri":"/mutex/"},{"categories":["Go"],"content":"演进之路 ","date":"2024-01-12","objectID":"/mutex/:4:0","tags":["Go"],"title":"mutex用法","uri":"/mutex/"},{"categories":["Go"],"content":"初版的互斥锁 Unlock 方法可以被任意的 goroutine 调用释放锁,即使是没持有这个互斥锁的goroutine,也可以进行这个操作。这是因为,Mutex 本身并没有包含持有这把锁的goroutine 的信息,所以,Unlock 也不会对此进行检查。Mutex 的这个设计一直保持至今 mutex包含两个字段: 字段key 字段sema 初版的 Mutex 实现有一个问题: 请求锁的 goroutine 会排队等待获取互斥锁。虽然这貌似很公平,但是从性能上来看,却不是最优的 ","date":"2024-01-12","objectID":"/mutex/:4:1","tags":["Go"],"title":"mutex用法","uri":"/mutex/"},{"categories":["Go"],"content":"给新人机会 第一个字段改成了state 相对于初版的设计,这次的改动主要就是,新来的 goroutine 也有机会先获取到锁,甚至一个 goroutine 可能连续获取到锁,打破了先来先得的逻辑。但是,代码复杂度也显而易见 ","date":"2024-01-12","objectID":"/mutex/:4:2","tags":["Go"],"title":"mutex用法","uri":"/mutex/"},{"categories":["Go"],"content":"多给些机会 如果新来的 goroutine 或者是被唤醒的 goroutine 首次获取不到锁,它们就会通过自旋(spin,通过循环不断尝试,spin 的逻辑是在runtime 实现的)的方式,尝试检查锁是否被释放。在尝试一定的自旋次数后,再执行原来的逻辑 ","date":"2024-01-12","objectID":"/mutex/:4:3","tags":["Go"],"title":"mutex用法","uri":"/mutex/"},{"categories":["Go"],"content":"解决饥饿 只需要记住,Mutex 绝不容忍一个goroutine 被落下,永远没有机会获取锁。不抛弃不放弃是它的宗旨,而且它也尽可能地让等待较长的 goroutine 更有机会获取到锁 跟之前的实现相比,当前的 Mutex 最重要的变化,就是增加饥饿模式。第 12 行将饥饿模式的最大等待时间阈值设置成了 1 毫秒,这就意味着,一旦等待者等待的时间超过了这个阈值,Mutex 的处理就有可能进入饥饿模式,优先让等待者先获取到锁 饥饿模式和正常模式 正常模式下,waiter 都是进入先入先出队列,被唤醒的 waiter 并不会直接持有锁,而是要和新来的 goroutine 进行竞争。新来的 goroutine 有先天的优势,它们正在 CPU 中运行,可能它们的数量还不少,所以,在高并发情况下,被唤醒的 waiter 可能比较悲剧地获取不到锁,这时,它会被插入到队列的前面。如果 waiter 获取不到锁的时间超过阈值 1 毫秒,那么,这个 Mutex 就进入到了饥饿模式。 在饥饿模式下,Mutex 的拥有者将直接把锁交给队列最前面的 waiter。新来的 goroutine不会尝试获取锁,即使看起来锁没有被持有,它也不会去抢,也不会 spin,它会乖乖地加入到等待队列的尾部。 如果拥有 Mutex 的 waiter 发现下面两种情况的其中之一,它就会把这个 Mutex 转换成正常模式: 此 waiter 已经是队列中的最后一个 waiter 了,没有其它的等待锁的 goroutine 了; ","date":"2024-01-12","objectID":"/mutex/:4:4","tags":["Go"],"title":"mutex用法","uri":"/mutex/"},{"categories":["Go"],"content":"常见的 4 种错误场景 lock/unlock不是成对出现 Lock/Unlock 没有成对出现,就意味着会出现死锁的情况,或者是因为 Unlock 一个未加锁的 Mutex 而导致 panic Copy已使用的Mutex vet 工具,把检查写在 Makefile 文件中,在持续集成的时候跑一跑,这样可以及时发现问题,及时修复。我们可以使用 go vet 检查这个 Go文件 重入 当一个线程获取锁时,如果没有其它线程拥有这个锁,那么,这个线程就成功获取到这个锁。之后,如果其它线程再请求这个锁,就会处于阻塞等待的状态。但是,如果拥有这把锁的线程再请求这把锁的话,不会阻塞,而是成功返回,所以叫可重入锁(有时候也叫做递归锁)。只要你拥有这把锁,你可以可着劲儿地调用,比如通过递归实现一些算法,调用者不会阻塞或者死锁 ","date":"2024-01-12","objectID":"/mutex/:5:0","tags":["Go"],"title":"mutex用法","uri":"/mutex/"},{"categories":["Go"],"content":"Mutex 不是可重入的锁 实现重入锁的方案: 方案一:gorotine id 第一步:我们先获取到 TLS 对象; 第二步:再从 TLS 中获取 goroutine 结构的 g 指针; 第三步:再从 g 指针中取出 goroutine id 推荐一个常用的库:petermattis/goid // RecursiveMutex 包装一个Mutex,实现可重入 type RecursiveMutex struct { sync.Mutex owner int64 // 当前持有锁的goroutine id recursion int32 // 这个goroutine 重入的次数 } func (m *RecursiveMutex) Lock() { gid := goid.Get() // 如果当前持有锁的goroutine就是这次调用的goroutine,说明是重入 if atomic.LoadInt64(\u0026m.owner) == gid { m.recursion++ return } m.Mutex.Lock() // 获得锁的goroutine第一次调用,记录下它的goroutine id,调用次数加1 atomic.StoreInt64(\u0026m.owner, gid) m.recursion = 1 } func (m *RecursiveMutex) Unlock() { gid := goid.Get() // 非持有锁的goroutine尝试释放锁,错误的使用 if atomic.LoadInt64(\u0026m.owner) != gid { panic(fmt.Sprintf(\"wrong the owner(%d): %d!\", m.owner, gid)) } // 调用次数减1 m.recursion-- if m.recursion != 0 { // 如果这个goroutine还没有完全释放,则直接返回 return } // 此goroutine最后一次调用,需要释放锁 atomic.StoreInt64(\u0026m.owner, -1) m.Mutex.Unlock() } 方案二:token // Token方式的递归锁 type TokenRecursiveMutex struct { sync.Mutex token int64 recursion int32 } // 请求锁,需要传入token func (m *TokenRecursiveMutex) Lock(token int64) { if atomic.LoadInt64(\u0026m.token) == token { //如果传入的token和持有锁的token一致,说明是递归调用 m.recursion++ return } m.Mutex.Lock() // 传入的token不一致,说明不是递归调用 // 抢到锁之后记录这个token atomic.StoreInt64(\u0026m.token, token) m.recursion = 1 } // 释放锁 func (m *TokenRecursiveMutex) Unlock(token int64) { if atomic.LoadInt64(\u0026m.token) != token { // 释放其它token持有的锁 panic(fmt.Sprintf(\"wrong the owner(%d): %d!\", m.token, token)) } m.recursion-- // 当前持有这个锁的token释放锁 if m.recursion != 0 { // 还没有回退到最初的递归调用 return } atomic.StoreInt64(\u0026m.token, 0) // 没有递归调用了,释放锁 m.Mutex.Unlock() } 死锁 四个条件: 互斥 持有和等待 不可剥夺 环路等待 ","date":"2024-01-12","objectID":"/mutex/:5:1","tags":["Go"],"title":"mutex用法","uri":"/mutex/"},{"categories":["Go"],"content":"功能扩展 ","date":"2024-01-12","objectID":"/mutex/:6:0","tags":["Go"],"title":"mutex用法","uri":"/mutex/"},{"categories":["Go"],"content":"TryLock 当一个 goroutine 调用这个TryLock 方法请求锁的时候,如果这把锁没有被其他 goroutine 所持有,那么,这个goroutine 就持有了这把锁,并返回 true;如果这把锁已经被其他 goroutine 所持有,或者是正在准备交给某个被唤醒的 goroutine,那么,这个请求锁的 goroutine 就直接返回false,不会阻塞在方法调用上 func (m *Mutex) TryLock() bool { // 已加锁/饥饿状态返回false old := m.state if old\u0026(mutexLocked|mutexStarving) != 0 { return false } // 竞争失败则返回false,否则标记锁状态 if !atomic.CompareAndSwapInt32(\u0026m.state, old, old|mutexLocked) { return false } if race.Enabled { race.Acquire(unsafe.Pointer(m)) } return true } ","date":"2024-01-12","objectID":"/mutex/:6:1","tags":["Go"],"title":"mutex用法","uri":"/mutex/"},{"categories":["Go"],"content":"获取等待者的数量等指标 const ( mutexLocked = 1 \u003c\u003c iota // mutex is locked mutexWoken mutexStarving mutexWaiterShift = iota ) type Mutex struct { sync.Mutex } func (m *Mutex) Count() int { // 获取state字段的值 v := atomic.LoadInt32((*int32)(unsafe.Pointer(\u0026m.Mutex))) v = v \u003e\u003e mutexWaiterShift //得到等待者的数值 v = v + (v \u0026 mutexLocked) //再加上锁持有者的数量,0或者1 return int(v) } state 这个字段的第一位是用来标记锁是否被持有,第二位用来标记是否已经唤醒了一个等待者,第三位标记锁是否处于饥饿状态,通过分析这个 state 字段我们就可以得到这些状态信息。我们可以为这些状态提供查询的方法,这样就可以实时地知道锁的状态了 // 锁是否被持有 func (m *Mutex) IsLocked() bool { state := atomic.LoadInt32((*int32)(unsafe.Pointer(\u0026m.Mutex))) return state\u0026mutexLocked == mutexLocked } // 是否有等待者被唤醒 func (m *Mutex) IsWoken() bool { state := atomic.LoadInt32((*int32)(unsafe.Pointer(\u0026m.Mutex))) return state\u0026mutexWoken == mutexWoken } // 锁是否处于饥饿状态 func (m *Mutex) IsStarving() bool { state := atomic.LoadInt32((*int32)(unsafe.Pointer(\u0026m.Mutex))) return state\u0026mutexStarving == mutexStarving } ","date":"2024-01-12","objectID":"/mutex/:6:2","tags":["Go"],"title":"mutex用法","uri":"/mutex/"},{"categories":["Go"],"content":"实现一个线程安全的队列 队列,我们可以通过 Slice 来实现,但是通过 Slice 实现的队列不是线程安全的,出队(Dequeue)和入队(Enqueue)会有 data race 的问题。这个时候,Mutex 就要隆重出场了,通过它,我们可以在出队和入队的时候加上锁的保护 type SliceQueue struct { data []interface{} mu sync.Mutex } func NewSliceQueue(n int) (q *SliceQueue) { return \u0026SliceQueue{data: make([]interface{}, 0, n)} } // Enqueue 把值放在队尾 func (q *SliceQueue) Enqueue(v interface{}) { q.mu.Lock() q.data = append(q.data, v) q.mu.Unlock() } // Dequeue 移去队头并返回 func (q *SliceQueue) Dequeue() interface{} { q.mu.Lock() if len(q.data) == 0 { q.mu.Unlock() return nil } v := q.data[0] q.data = q.data[1:] q.mu.Unlock() return v } ","date":"2024-01-12","objectID":"/mutex/:7:0","tags":["Go"],"title":"mutex用法","uri":"/mutex/"},{"categories":["Go"],"content":"这篇文章展示了rwmutex用法","date":"2024-01-12","objectID":"/rwmutex/","tags":["Go"],"title":"rwmutex用法","uri":"/rwmutex/"},{"categories":["Go"],"content":"什么是RWMutex? 方法: Lock/Unlock:写操作时调用的方法 RLock/RUnlock:读操作时调用的方法 RLocker:为读操作返回一个Locker接口的对象 package main import ( \"sync\" \"time\" ) type Counter struct { mu sync.RWMutex count uint64 } func (c *Counter) Incr() { c.mu.Lock() c.count++ c.mu.Unlock() } func (c *Counter) Value() uint64 { c.mu.RLock() defer c.mu.RUnlock() return c.count } func main() { var counter Counter for i := 0; i \u003c 10; i++ { go func() { for { counter.Value() time.Sleep(time.Millisecond) } }() } for { counter.Incr() time.Sleep(time.Millisecond) } } 应用场景 如果你遇到可以明确区分 reader 和 writer goroutine 的场景,且有大量的并发读、少量的并发写,并且有强烈的性能需求,你就可以考虑使用读写锁 RWMutex 替换 Mutex。 实现原理 readers-writers问题分为三类: Read-preferring Write-preferring 不指定优先级 Go 标准库中的 RWMutex 设计是 Write-preferring 方案。一个正在阻塞的 Lock 调用会排除新的 reader 请求到锁 实现 RLock/RUnlock实现 func (rw *RWMutex) RLock() { if race.Enabled { _ = rw.w.state race.Disable() } if rw.readerCount.Add(1) \u003c 0 { // A writer is pending, wait for it. runtime_SemacquireRWMutexR(\u0026rw.readerSem, false, 0) } if race.Enabled { race.Enable() race.Acquire(unsafe.Pointer(\u0026rw.readerSem)) } } .... func (rw *RWMutex) RUnlock() { if race.Enabled { _ = rw.w.state race.ReleaseMerge(unsafe.Pointer(\u0026rw.writerSem)) race.Disable() } if r := rw.readerCount.Add(-1); r \u003c 0 { // 有等待的 writer rw.rUnlockSlow(r) } if race.Enabled { race.Enable() } } func (rw *RWMutex) rUnlockSlow(r int32) { if r+1 == 0 || r+1 == -rwmutexMaxReaders { race.Enable() fatal(\"sync: RUnlock of unlocked RWMutex\") } if rw.readerWait.Add(-1) == 0 { //最后一个 reader,writer有机会获取锁了 runtime_Semrelease(\u0026rw.writerSem, false, 1) } } Lock func (rw *RWMutex) Lock() { if race.Enabled { _ = rw.w.state race.Disable() } // 首先解决其他 writer的竞争问题 rw.w.Lock() // 反转 readerCount,告诉 reader有 writer竞争锁 r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders // 如果当前 reader持有锁,那么需要等待 if r != 0 \u0026\u0026 rw.readerWait.Add(r) != 0 { runtime_SemacquireRWMutex(\u0026rw.writerSem, false, 0) } if race.Enabled { race.Enable() race.Acquire(unsafe.Pointer(\u0026rw.readerSem)) race.Acquire(unsafe.Pointer(\u0026rw.writerSem)) } } Unlock func (rw *RWMutex) Unlock() { if race.Enabled { _ = rw.w.state race.Release(unsafe.Pointer(\u0026rw.readerSem)) race.Disable() } // 告诉 reader没有活跃的 writer了 r := rw.readerCount.Add(rwmutexMaxReaders) if r \u003e= rwmutexMaxReaders { race.Enable() fatal(\"sync: Unlock of unlocked RWMutex\") } // 唤醒阻塞的 reader们 for i := 0; i \u003c int(r); i++ { runtime_Semrelease(\u0026rw.readerSem, false, 0) } // 释放内部的互斥锁 rw.w.Unlock() if race.Enabled { race.Enable() } } 3 个踩坑点 坑点1:不可复制 坑点2:重入导致死锁 场景 1: func main() { l := \u0026sync.RWMutex{} foo(l) } func foo(l *sync.RWMutex) { fmt.Println(\"in foo\") l.Lock() bar(l) l.Unlock() } func bar(l *sync.RWMutex) { l.Lock() fmt.Println(\"in bar\") l.Unlock() } 场景二: 有活跃 reader 的时候,writer 会等待,如果我们在 reader 的读操作时调用 writer 的写操作(它会调用 Lock 方法),那么,这个 reader和 writer 就会形成互相依赖的死锁状态 场景三: writer 依赖活跃的 reader -\u003e 活跃的 reader 依赖新来的 reader -\u003e 新来的 reader依赖 writer func main() { var mu sync.RWMutex go func() { time.Sleep(100 * time.Millisecond) mu.Lock() fmt.Println(\"Lock\") time.Sleep(100 * time.Millisecond) mu.Unlock() fmt.Println(\"Unlock\") }() go func() { factorial(\u0026mu, 10) }() select {} } func factorial(mu *sync.RWMutex, n int) int { if n \u003c 1 { return 0 } fmt.Println(\"Rlock\") mu.RLock() defer func() { fmt.Println(\"RUnlock\") mu.RUnlock() }() time.Sleep(100 * time.Millisecond) return factorial(mu, n-1) * n } 坑点3:释放未加锁的RWMutex ","date":"2024-01-12","objectID":"/rwmutex/:0:1","tags":["Go"],"title":"rwmutex用法","uri":"/rwmutex/"},{"categories":["Go"],"content":"这篇文章展示了waitgroup用法","date":"2024-01-12","objectID":"/waitgroup/","tags":["Go"],"title":"waitgroup用法","uri":"/waitgroup/"},{"categories":["Go"],"content":"基本用法 once常常用来初始化单例资源,或者并发访问只需初始化一次的共享资源,或者在测试的时候初始化一次测试资源 ","date":"2024-01-12","objectID":"/waitgroup/:0:1","tags":["Go"],"title":"waitgroup用法","uri":"/waitgroup/"},{"categories":["Go"],"content":"实现 Add方法逻辑: Add 方法主要操作的是 state 的计数部分。你可以为计数值增加一个 delta 值,内部通过原子操作把这个值加到计数值上。需要注意的是,这个 delta 也可以是个负数,相当于为计数值减去一个值,Done 方法内部其实就是通过Add(-1) 实现的。 Wait方法逻辑: 不断检查 state 的值。如果其中的计数值变为了 0,那么说明所有的任务已完成,调用者不必再等待,直接返回。如果计数值大于 0,说明此时还有任务没完成,那么调用者就变成了等待者,需要加入 waiter 队列,并且阻塞住自己。 ","date":"2024-01-12","objectID":"/waitgroup/:0:2","tags":["Go"],"title":"waitgroup用法","uri":"/waitgroup/"},{"categories":["Go"],"content":"常见问题 计数器设置为负数 解决方案: a.调用add的时候传递一个负数 b.调用done方法的次数过多,超过了waitGroup的计数值 使用 WaitGroup 的正确姿势是,预先确定好 WaitGroup 的计数值,然后调用相同次数的 Done 完成相应的任务 不期望的add时机 解决方案:等所有的 Add 方法调用之后再调用 Wait 前一个wait还没结束就重用waitGroup 解决方案:WaitGroup 虽然可以重用,但是是有一个前提的,那就是必须等到上一轮的Wait 完成之后,才能重用 WaitGroup 执行下一轮的 Add/Wait,如果你在 Wait 还没执行完的时候就调用下一轮 Add 方法,就有可能出现 panic ","date":"2024-01-12","objectID":"/waitgroup/:0:3","tags":["Go"],"title":"waitgroup用法","uri":"/waitgroup/"},{"categories":["Go"],"content":"基本用法 package main import ( \"fmt\" \"sync\" \"time\" ) type Counter struct { mu sync.RWMutex count uint64 } func (c *Counter) Incr() { c.mu.Lock() c.count++ c.mu.Unlock() } func (c *Counter) Count() uint64 { c.mu.RLock() defer c.mu.RUnlock() return c.count } func worker(c *Counter, wg *sync.WaitGroup) { defer wg.Done() time.Sleep(time.Second) c.Incr() } func main() { var counter Counter var wg sync.WaitGroup wg.Add(10) for i := 0; i \u003c 10; i++ { go worker(\u0026counter, \u0026wg) } wg.Wait() fmt.Println(counter.Count()) } ","date":"2024-01-12","objectID":"/waitgroup/:0:4","tags":["Go"],"title":"waitgroup用法","uri":"/waitgroup/"},{"categories":["Go"],"content":"实现 type WaitGroup struct { noCopy noCopy // 高 32 位为计数值,低 32 位是 waiter的计数 state atomic.Uint64 sema uint32 } ","date":"2024-01-12","objectID":"/waitgroup/:0:5","tags":["Go"],"title":"waitgroup用法","uri":"/waitgroup/"},{"categories":["Design"],"content":"这篇文章展示了软件设计原则基本知识.","date":"2024-01-12","objectID":"/mod/","tags":["Design"],"title":"软件设计原则","uri":"/mod/"},{"categories":["Design"],"content":"糟糕的设计 ","date":"2024-01-12","objectID":"/mod/:1:0","tags":["Design"],"title":"软件设计原则","uri":"/mod/"},{"categories":["Design"],"content":"僵化性 软件代码之间耦合严重,难以改动,任何微小的改动都会引起更大范围的改动。 ","date":"2024-01-12","objectID":"/mod/:1:1","tags":["Design"],"title":"软件设计原则","uri":"/mod/"},{"categories":["Design"],"content":"脆弱性 微小的改动容易引起莫名其妙的崩溃或者 bug,出现 bug 的地方看似与改动的地方毫无关联,或者软件进行了一个看似简单的改动,重新启动,然后就莫名其妙地崩溃了。 ","date":"2024-01-12","objectID":"/mod/:1:2","tags":["Design"],"title":"软件设计原则","uri":"/mod/"},{"categories":["Design"],"content":"牢固性 软件无法进行快速、有效地拆分。 ","date":"2024-01-12","objectID":"/mod/:1:3","tags":["Design"],"title":"软件设计原则","uri":"/mod/"},{"categories":["Design"],"content":"粘滞性 ","date":"2024-01-12","objectID":"/mod/:1:4","tags":["Design"],"title":"软件设计原则","uri":"/mod/"},{"categories":["Design"],"content":"晦涩性 ","date":"2024-01-12","objectID":"/mod/:1:5","tags":["Design"],"title":"软件设计原则","uri":"/mod/"},{"categories":["Design"],"content":"开闭原则 软件实体(模块、类、函数等等)应该对扩展是开放的,对修改是关闭的。 对扩展是开放的,意味着软件实体的行为是可扩展的,当需求变更的时候,可以对模块进行扩展,使其满足需求变更的要求。 对修改是关闭的,意味着当对软件实体进行扩展的时候,不需要改动当前的软件实体;不需要修改代码;对于已经完成的类文件不需要重新编辑;对于已经编译打包好的模块,不需要再重新编译。 ","date":"2024-01-12","objectID":"/mod/:2:0","tags":["Design"],"title":"软件设计原则","uri":"/mod/"},{"categories":["Design"],"content":"一个违反开闭原则的例子 当我们在代码中看到 else 或者 switch/case 关键字的时候,基本可以判断违反开闭原则了。 ","date":"2024-01-12","objectID":"/mod/:2:1","tags":["Design"],"title":"软件设计原则","uri":"/mod/"},{"categories":["Design"],"content":"使用策略模式实现开闭原则 策略模式是一种行为模式,多个策略实现同一个策略接口 ","date":"2024-01-12","objectID":"/mod/:2:2","tags":["Design"],"title":"软件设计原则","uri":"/mod/"},{"categories":["Design"],"content":"使用适配器模式实现开闭原则 ","date":"2024-01-12","objectID":"/mod/:2:3","tags":["Design"],"title":"软件设计原则","uri":"/mod/"},{"categories":["Design"],"content":"使用观察者模式实现开闭原则 观察者模式是一种行为模式,解决一对多的对象依赖关系,将被观察者对象的行为通知到多个观察者,也就是监听者对象。 ","date":"2024-01-12","objectID":"/mod/:2:4","tags":["Design"],"title":"软件设计原则","uri":"/mod/"},{"categories":["Design"],"content":"使用模板方法模式实现开闭原则 ","date":"2024-01-12","objectID":"/mod/:2:5","tags":["Design"],"title":"软件设计原则","uri":"/mod/"},{"categories":["Mysql"],"content":"这篇文章展示了Mysql架构基本知识.","date":"2024-01-11","objectID":"/mysql_prepare/","tags":["Mysql"],"title":"Mysql架构","uri":"/mysql_prepare/"},{"categories":["Mysql"],"content":"数据库架构与 SQL执行过程 –\u003e连接器–\u003e语法分析器–\u003e语义分析与优化器–\u003e执行引擎 应用程序需要对数据库连接进行管理,一方面通过连接池对连接进行管理,空闲连接会被及时释放;另一方面微服务架构可以大大减少数据库连接 语法分析器生成的抽象语法树并不仅仅可以用来做语法校验,它也是下一步处理的基础。语义分析与优化器会对抽象语法树进一步做语义优化 连接器收到 SQL 以后,会将 SQL 交给语法分析器进行处理,语法分析器工作比较简单机械,就是根据 SQL 语法规则生成对应的抽象语法树 语义分析与优化器最后会输出一个执行计划,由执行引擎完成数据查询或者更新。 ","date":"2024-01-11","objectID":"/mysql_prepare/:0:1","tags":["Mysql"],"title":"Mysql架构","uri":"/mysql_prepare/"},{"categories":["Mysql"],"content":"使用 PrepareStatement 执行 SQL 的好处 一个是 PrepareStatement 会预先提交带占位符的 SQL 到数据库进行预处理,提前生成执行计划,当给定占位符参数,真正执行 SQL 的时候,执行引擎可以直接执行,效率更好一点。 另一个好处则更为重要,PrepareStatement 可以防止 SQL 注入攻击。 ","date":"2024-01-11","objectID":"/mysql_prepare/:0:2","tags":["Mysql"],"title":"Mysql架构","uri":"/mysql_prepare/"},{"categories":["Mysql"],"content":"数据库文件存储原理 B+ 树是一种 N 叉排序树,树的每个节点包含 N 个数据,这些数据按顺序排好,两个数据之间是一个指向子节点的指针, 而子节点的数据则在这两个数据大小之间。 一种是聚簇索引,聚簇索引的数据库记录和索引存储在一起,上面这张图就是聚簇索引的示意图,在叶子节点,索引 1 和记录行 r1 存储在一起,查找到索引就是查找到数据库记录。像 MySQL 数据库的主键就是聚簇索引,主键 ID 和所在的记录行存储在一起。MySQL 的数据库文件实际上是以主键作为中间节点,行记录作为叶子节点的一颗 B+ 树。 另一种数据库索引是非聚簇索引,非聚簇索引在叶子节点记录的就不是数据行记录,而是聚簇索引,也就是主键 通过 B+ 树在叶子节点找到非聚簇索引 a,和索引 a 在一起存储的是主键 1,再根据主键 1 通过主键(聚簇)索引就可以找到对应的记录 r1,这种通过非聚簇索引找到主键索引,再通过主键索引找到行记录的过程也被称作回表 ","date":"2024-01-11","objectID":"/mysql_prepare/:0:3","tags":["Mysql"],"title":"Mysql架构","uri":"/mysql_prepare/"},{"categories":["Mysql"],"content":"基础架构 连接器 命令: Mysql -h$ip -P$port -u$user -p 查看mysql是否是空闲状态: Show processlist 其中的Command 列显示为“Sleep”的这一行,就表示现在系统里面有一个空闲连接。 长连接和短链接: 长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接。短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。 查询缓存 将参数 query_cache_type 设置成 DEMAND,这样对于默认的 SQL 语句都不使用查询缓存。而对于你确定要使用查询缓存的语句,可以用 SQL_CACHE 显式指定,像下面这个语句一样: Select SQL_CACHE * from T where id = 10; Mysql8.0以上版本不在适用。 分析器 词法分析-\u003e语法分析 优化器 在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。 执行器 判断是否有执行查询的权限 慢查询日志中看到一个 rows_examined 的字段,表示这个语句执行过程中扫描了多少行。这个值就是在执行器每次调用引擎获取数据行的时候累加的。 引擎扫描行数跟rows_examined 并不是完全相同的。 ","date":"2024-01-11","objectID":"/mysql_prepare/:0:4","tags":["Mysql"],"title":"Mysql架构","uri":"/mysql_prepare/"},{"categories":["Go"],"content":"这篇文章展示了使用Goroutine如何控制HTTP请求的并发量","date":"2024-01-10","objectID":"/concurrency/","tags":["Go"],"title":"使用Goroutine如何控制HTTP请求的并发量","uri":"/concurrency/"},{"categories":["Go"],"content":"场景 我们使用 go 并发调用接口发起 HTTP 请求时,只需要在 func() 前面加上 go 关键字就很容易完成了,就是因为让并发变得如此简单,所以有的时候我们就需要控制一下并发请求的数量。 现在有个需求:本地有一千万条手机号,需要调用聚合数据 手机号码归属地 接口,并记录省份、城市、区号、邮编、运营商等查询结果数据。 ","date":"2024-01-10","objectID":"/concurrency/:0:1","tags":["Go"],"title":"使用Goroutine如何控制HTTP请求的并发量","uri":"/concurrency/"},{"categories":["Go"],"content":"实现 package main import( \"fmt\" \"io/ioutil\" \"math/rand\" \"net/http\" \"net/url\" \"sync\" \"time\" ) type Limit struct { number int channel chan struct{} } // Limit struct 初始化 func New(number int) *Limit { return \u0026Limit{ number: number, channel: make(chan struct{}, number), } } // Run 方法:创建有限的 go f 函数的 goroutine func (limit *Limit) Run(f func()) { limit.channel \u003c- struct{}{} go func() { f() \u003c-limit.channel }() } // WaitGroup 对象内部有一个计数器,从0开始 // 有三个方法:Add(), Done(), Wait() 用来控制计数器的数量 var wg = sync.WaitGroup{} const ( concurrency = 5 // 控制并发量 ) func main() { start := time.Now() limit := New(concurrency) // New Limit 控制并发量 // 接口请求URL apiUrl := \"http://apis.juhe.cn/mobile/get\" // 不要使用接口地址测试 //max := int(math.Pow10(8)) // 模拟一千万数据 max := 5 // 先测试5次吧 // 初始化参数 param := url.Values{} param.Set(\"key\", \"您申请的KEY\") // 接口请求Key for i := 0; i \u003c max; i++ { wg.Add(1) value := i goFunc := func() { fmt.Printf(\"start func: %dn\", value) // 配置请求参数,方法内部已处理urlencode问题,中文参数可以直接传参 phone := RandMobile() param.Set(\"phone\", phone) // 需要查询的手机号码或手机号码前7位 // 发送请求 data, err := Get(apiUrl, param) if err != nil { fmt.Println(err) return } // 其它逻辑代码... fmt.Println(\"phone: \", phone, string(data)) wg.Done() } limit.Run(goFunc) } // 阻塞代码防止退出 wg.Wait() fmt.Printf(\"耗时: %fs\", time.Now().Sub(start).Seconds()) } // Get 方式发起网络请求 func Get(apiURL string, params url.Values) (rs []byte, err error) { var Url *url.URL Url, err = url.Parse(apiURL) if err != nil { fmt.Printf(\"解析url错误:rn%v\", err) return nil, err } //如果参数中有中文参数,这个方法会进行URLEncode Url.RawQuery = params.Encode() resp, err := http.Get(Url.String()) if err != nil { fmt.Println(\"err:\", err) return nil, err } defer resp.Body.Close() return ioutil.ReadAll(resp.Body) } var MobilePrefix = [...]string{\"130\", \"131\", \"132\", \"133\", \"134\", \"135\", \"136\", \"137\", \"138\", \"139\", \"145\", \"147\", \"150\", \"151\", \"152\", \"153\", \"155\", \"156\", \"157\", \"158\", \"159\", \"170\", \"176\", \"177\", \"178\", \"180\", \"181\", \"182\", \"183\", \"184\", \"185\", \"186\", \"187\", \"188\", \"189\"} // GeneratorPhone 生成手机号码 func RandMobile() string { return MobilePrefix[RandInt(0, len(MobilePrefix))] + fmt.Sprintf(\"%0*d\", 8, RandInt(0, 100000000)) } // 指定范围随机 int func RandInt(min, max int) int { rand.Seed(time.Now().UnixNano()) return min + rand.Intn(max-min) } ","date":"2024-01-10","objectID":"/concurrency/:0:2","tags":["Go"],"title":"使用Goroutine如何控制HTTP请求的并发量","uri":"/concurrency/"},{"categories":["Tips"],"content":"这篇文章展示了工作技巧相关知识.","date":"2024-01-09","objectID":"/work_tips/","tags":["Tips"],"title":"工作技巧","uri":"/work_tips/"},{"categories":["Tips"],"content":"保持交际和赞美 一方面,良好的交际关系可以营造一种更愉快的工作氛围,自己和其他同事可以保持更好的工作状态; 另一方面,处理某些问题的时候,比如,需要指出某个人工作失误的时候, 良好的关系可以缓冲这类指责带来的负面影响。 赞美不是奉承,不是泛泛地说一些:你好棒,你真厉害。赞美是对对方做得好的事情,明确表达你的称赞。 此外,赞美和批评并不冲突,你可以对一个人既赞美又批评,只要你明确指出赞美和批评的具体事情,对方就可以更加明白你的标准和边界,后面的合作也会更加的顺利。 ","date":"2024-01-09","objectID":"/work_tips/:0:1","tags":["Tips"],"title":"工作技巧","uri":"/work_tips/"},{"categories":["Tips"],"content":"平衡力量和温暖 所谓的力量是指能够达成目标的能力,包括技术能力、整合资源的能力、决策力、意志力等各种能力,通过这些能力,能够完成工作目标和任务。人们愿意和有力量的人合作,追随有力量的人,因为这样获得成功的可能性就越大。 而温暖是指拥有让他人产生熟悉感和归属感的能力。表明上看,这种能力是一种共情能力,可以理解他人的喜怒哀乐,进而产生熟悉和归属的感觉。事实上,这是一种构建共同的目标和价值观的能力。 ","date":"2024-01-09","objectID":"/work_tips/:0:2","tags":["Tips"],"title":"工作技巧","uri":"/work_tips/"},{"categories":["Tips"],"content":"学会聆听和提问 在工作沟通的过程中,有时候直接提出自己的观点或者方案,并不能得到其他人的赞同和支持,因为其他人可能并不了解你的问题和场景,没有思考过你的问题,所以对你的观点和方案不置可否,不积极参与。这种情况下,可以通过一些提问的方式,将对方拉到你的思考上下文中,让对方通过自己的思考得出你想要表达的观点和方案,这种情况下再去推动事情的发展就容易多了。 ","date":"2024-01-09","objectID":"/work_tips/:0:3","tags":["Tips"],"title":"工作技巧","uri":"/work_tips/"},{"categories":["Tips"],"content":"这篇文章展示了有用的技巧.","date":"2024-01-02","objectID":"/tips/","tags":[" Tips"],"title":"有用的技巧","uri":"/tips/"},{"categories":["Tips"],"content":"为整型常量值增加一个 String () 方法 如果你使用 iota 的自定义整型用于枚举,请始终增加一个 String () 方法。假设你写了这样的代码 type State int const ( Running State = iota Stopped Rebooting Terminated ) 如果你为 State 类型赋值,并且输出它,那么你将会看到一个数字 在这里 0 是没有任何意义的,除非你去看回你声明变量的代码。如果你为你的 State 类型增加一个 String () 方法,就不用再去看回声明变量的代码了。 func (s State) String() string { switch s { case Running: return \"Running\" case Stopped: return \"Stopped\" case Rebooting: return \"Rebooting\" case Terminated: return \"Terminated\" default: return \"Unknown\" } } 其实这些都可以通过 Stringer 工具去自动化实现: stringer ","date":"2024-01-02","objectID":"/tips/:1:0","tags":[" Tips"],"title":"有用的技巧","uri":"/tips/"},{"categories":["Tips"],"content":"为访问 map 增加 setter,getters 如果你重度使用 map 读写数据,那么就为其添加 getter 和 setter 吧。通过 getter 和 setter 你可以将逻辑封分别装到函数里。这里最常见的错误就是并发访问。如果你在某个 goroutein 里有这样的代码: m[\"foo\"] = bar 假设你在其他地方也使用这个 map。你必须把互斥量放得到处都是!然而通过 getter 和 setter 函数就可以很容易的避免这个问题: func Put(key, value string) { mu.Lock() m[key] = value mu.Unlock() } func Delete(key string) { mu.Lock() delete(m, key) mu.Unlock() } 使用接口可以对这一过程做进一步的改进。你可以将实现完全隐藏起来。只使用一个简单的、设计良好的接口,然后让包的用户使用它们: type Storage interface { Delete(key string) Get(key string) string Put(key, value string) } ","date":"2024-01-02","objectID":"/tips/:2:0","tags":[" Tips"],"title":"有用的技巧","uri":"/tips/"}]