Maven Dependency Search 在 VS Code 中快速搜索 Maven 中央仓库，一键将 <dependency> 坐标插入到编辑器光标处。 ✨ 功能特性 🔍 智能搜索 关键词模糊搜索：输入 spring-boot 自动展开匹配 spring-boot-starter-web、spring-boot-starter-parent、spring-boot-autoconfigure 等所有相关依赖 通配符支持：可手动输入 org.springframework* 批量搜索整个 Group 下的所有 Artifact 防抖自动触发：输入停顿 500ms 自动搜索，无需手动按 Enter 🎛 高级搜索 展开高级面板，支持多字段精确组合查询： 字段 说明 Group ID 按 groupId 精确或前缀搜索，如 org.springframework.boot Artifact ID 按 artifactId 精确或前缀搜索，如 spring-boot-starter Packaging 按打包类型过滤：jar / pom / war / aar 每页条数 10 / 20 / 50 条可选 多字段同时填写时以 AND 逻辑组合。 📋 分页浏览 结果列表支持分页，顶部显示 共 N 条，第 X/Y 页 页码按钮导航（超出显示省略号） 上页 / 下页快捷按钮 支持直接跳转到指定页 🔎 详情预览 点击左侧任意搜索结果，右侧面板实时展示： Artifact ID、Group ID 版本选择器（自动加载该 Artifact 的全部历史版本，最多 50 个） Packaging 类型 语法高亮的 <dependency> XML 预览，随版本切换实时更新 ⬇ 一键插入 插入到光标处：将选中版本的 <dependency> XML 片段直接写入编辑器当前光标位置 复制到剪贴板：不打开文件也可快速复制坐标 🚀 快速开始 在插件市场搜索 Maven Dependency Search 打开搜索面板 方式 1： 在任意文件中右键 → 🔍 搜索 Maven 依赖 方式 2： 命令面板（Ctrl+Shift+P）→ 输入 Maven: 搜索 Maven 依赖 📖 使用流程 1. 右键编辑器 → 🔍 搜索 Maven 依赖 ↓ 2. 搜索框输入关键词（或展开高级搜索填写 groupId/artifactId） ↓ 3. 左侧列表展示搜索结果 → 点击某条结果 ↓ 4. 右侧显示详情 → 版本下拉框选择所需版本 ↓ 5. 点击「⬇ 插入到光标处」完成 ⌨️ 键盘快捷键 操作 快捷键 立即搜索 Enter 选择上一条结果 ↑ 选择下一条结果 ↓ 🔧 搜索技巧 场景 推荐输入 找 Spring Boot 全家桶 关键词：spring-boot（自动展开子包） 只找 starter 类包 Artifact ID：spring-boot-starter* 找某公司所有包 Group ID：com.alibaba* 找特定 groupId 下的 pom Group ID：org.springframework.boot，Packaging：pom 找 Jackson 相关 关键词：jackson-databind 📦 插入结果示例 插入后，光标处会写入如下内容： <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.2.5</version> </dependency> 🌐 数据来源 本插件通过 Maven Central REST API 实时检索： https://search.maven.org/solrsearch/select?q=<query>&rows=<n>&start=<offset>&wt=json 数据完全来自 Maven 官方中央仓库，实时准确。 📋 变更日志 请查看 CHANGELOG.md 📄 许可证 MIT
权限操作：RBAC+ACL搭配 1. RBAC(基于角色的访问控制) RBAC将权限分配给角色，用户通过关联角色间接获得权限。系统通过“用户-角色-权限”的层级关系实现权限管理。 这种经典的权限控制设计有一个缺点，就是难以处理特殊个体的细粒度权限，比如我作为管理员，我需要将某个文件的访问权限交给不是管理员，但是信任的人(比如张三)进行访问。 2. ACL(访问控制列表) ACL直接在资源上定义允许操作的主体，每条记录明确指定"谁可以对某资源做什么" 这种划分权限类似于我们平常使用的windows文件管理的方式，其主要优势在于灵活性强，每条权限规则都明确可见。但是缺点也很明显，那就是当用户数量或者资源数量很大的时候，ACL条目呈现指数级增长。 3. RBAC 与 ACL 联合使用的优势 灵活性与效率的平衡 RBAC 处理通用权限（如部门角色、职级权限） ACL 处理例外场景（如特殊用户需要访问特定资源） 比如，在企业系统中，普通员工通过角色获得基础权限，但某个机密文件可通过ACL单独授权给少数高管。 减少角色爆炸 通过ACL补充RBAC，避免在RBAC中因为个别的特殊权限去创建冗余角色 精细化权限管理 RBAC管理粗粒度权限（如”访问财务模块“），ACL控制细粒度操作（如”仅能查看报表，不能导出“） 冲突解决与优先级 当RBAC和ACL权限有冲突的时候，可以设定优先级，比如ACL优先于RBAC 4. RBAC + ACL模型举例 RBAC（基于角色的访问控制）经典模型中通常包含以下5张核心表： -- 1. 用户表（3个用户） CREATE TABLE users ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, email VARCHAR(100) ); INSERT INTO users (username, email) VALUES ('alice', 'alice@example.com'), -- admin ('bob', 'bob@example.com'), -- user ('charlie', 'charlie@example.com'); -- user -- 2. 角色表（admin 和 user） CREATE TABLE roles ( role_id INT PRIMARY KEY AUTO_INCREMENT, role_name VARCHAR(50) NOT NULL UNIQUE ); INSERT INTO roles (role_name) VALUES ('admin'), ('user'); -- 3. 权限表（只包含读和写） CREATE TABLE permissions ( permission_id INT PRIMARY KEY AUTO_INCREMENT, permission_key VARCHAR(50) NOT NULL UNIQUE ); INSERT INTO permissions (permission_key) VALUES ('article:read'), ('article:write'); -- 4. 用户-角色关联表 CREATE TABLE user_roles ( user_id INT, role_id INT, PRIMARY KEY (user_id, role_id), FOREIGN KEY (user_id) REFERENCES users(user_id), FOREIGN KEY (role_id) REFERENCES roles(role_id) ); -- 分配角色（1个admin，2个user） INSERT INTO user_roles (user_id, role_id) VALUES (1, 1), -- alice 是 admin (2, 2), -- bob 是 user (3, 2); -- charlie 是 user -- 5. 角色-权限关联表 CREATE TABLE role_permissions ( role_id INT, permission_id INT, PRIMARY KEY (role_id, permission_id), FOREIGN KEY (role_id) REFERENCES roles(role_id), FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) ); -- 权限分配：admin有读写，user只有读 INSERT INTO role_permissions (role_id, permission_id) VALUES (1, 1), -- admin -> article:read (1, 2), -- admin -> article:write (2, 1); -- user -> article:read 然后如果需要对权限进行更加细粒度的控制，有如下ACL表可供参考： CREATE TABLE acl_entry ( id INT PRIMARY KEY AUTO_INCREMENT, resource_type VARCHAR(50) NOT NULL, -- 资源类型，如 article、order、comment resource_id INT NOT NULL, -- 具体资源ID grantee_type ENUM('USER', 'ROLE') NOT NULL, -- 授权对象类型：用户或角色 grantee_id INT NOT NULL, -- 被授权的用户或角色的ID permission VARCHAR(20) NOT NULL, -- 权限类型，如 read、write created_by INT NOT NULL, -- 授权人ID created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_acl_entry(resource_type, resource_id, grantee_type, grantee_id) ); -- admin (user_id = 1) 给用户 (user_id = 2) 授权了对文章101的读权限 -- admin 给角色（role_id = 2，可能是 editor）授权了对文章101的写权限 INSERT INTO acl_entry (resource_type, resource_id, grantee_type, grantee_id, permission, created_by) VALUES ('article', 101, 'USER', 2, 'read', 1), ('article', 101, 'ROLE', 2, 'write', 1);
微服务架构-Outbox发件箱模式 背景：在现代微服务架构中，服务之间常常需要通过异步消息传递来解耦。例如，一个订单服务创建订单后，需要通知库存服务扣减库存。如何在不同服务间传递事件的同时保持数据一致性，成为了分布式系统设计的核心挑战之一。 本篇将介绍一种经典的一致性设计模式：Outbox（发件箱）模式。它能有效解决“写库成功但消息发送失败”所带来的数据不一致问题。 1. 为什么需要Outbox模式？ 考虑一个场景的场景：订单服务先写入数据库，再发送消息的MQ通知库存服务扣减库存。 public void createOrder(Order order) { orderRepository.save(order); // 写数据库 messageQueue.send("order_created", order); // 发消息 } 如果第2步消息发送失败（比如MQ挂掉了、网络波动），订单已经创建了，但是库存不会减少，就会导致业务不一致，可能这时我们就需要进行回滚或者补偿机制，但是我们这次介绍的Outbox设计模式提供了一个全新的思路，确保在第一次处理时就保证“本地事务+跨服务通信”下的数据一致性问题。 2. Outbox模式架构详解 2.1. 核心思想 在数据库中新增一个outbox表，用于保存待发送的事件。 在本地事务中，同时写入业务数据和outbox事件。 通过后台轮询工具或者定时脚本读取outbox表中的事件，找到状态为待发送的事件，发送消息。 2.2. Outbox模式流程图 3. 代码示例 3.1. 创建订单代码（写入Outbox） @Transactional public void createOrder(Order order) { // 1. 创建订单记录 orderRepository.save(order); // 2. 写入发件箱表 OutboxEvent event = new OutboxEvent("ORDER_CREATED", JsonUtil.toJson(order)); outboxRepository.save(event); } 3.2. 消息发送器（定时轮询版） @Scheduled(fixedDelay = 5000) public void dispatchEvents() { List<OutboxEvent> events = outboxRepository.findPendingEvents(); for (OutboxEvent event : events) { try { messageQueue.send(event.getType(), event.getPayload()); event.markAsSent(); outboxRepository.save(event); } catch (Exception e) { // 这里可以放补偿方法或者最终的回滚机制，也可以放重试机制 } } } 4. 适用场景与总结 Outbox模式适用于： 核心业务链路必须保证事件可靠传递 有本地数据库，且可控制写入事务（如 MYSQL） 可以接受最终一致性，不追求强一致性 不适合： 无本地数据库的场景（如纯计算服务） 要求毫秒级响应的一致性事务 总结 Outbox模式是解决分布式事务一致性的一个经典方法。通过“写数据库 + 发消息”组合的原子化改造，把事件的可靠投递交由后台发送器完成，极大地提升了系统的容错性和可靠性。 在电商订单、支付系统、库存调整等典型的微服务场景中，Outbox都是一个值得考虑的模式。
设计模式原则分析 1. 单一职责原则（Single Responsibility Principle） 1.1 定义 单一职责原则是指一个类应该只有一个责任（即一个功能），一个类应该只负责一项任务。换句话说，如果一个类承担了多个职责，这些职责之间会有不同的变化原因，改变其中某个职责时，可能会影响到其他职责，导致系统更加复杂和难以维护。 1.2 不符合SRP原则举例 假设有一个类同时处理数据存储和报告生成的功能。当存储方式变化时（如从数据库变为文件存储），我们需要修改这个类的存储相关代码；而当报告生成的需求变化时（如格式从 PDF 变为 Excel），我们也需要修改这个类的报告生成代码。此时，这个类有两个职责，它们各自有不同的变化原因，导致这个类的维护变得复杂。 // 违反 SRP 原则：一个类同时负责数据存储和报告生成 public class UserManager { public void saveUser(User user) { // 保存用户到数据库 System.out.println("Saving user to database"); } public void generateUserReport(User user) { // 生成用户报告 System.out.println("Generating report for user"); } } 1.3 遵守SRP原则举例 // 遵守 SRP 原则：将职责分离到不同的类中 public class UserRepository { public void saveUser(User user) { // 保存用户到数据库 System.out.println("Saving user to database"); } } public class UserReportGenerator { public void generateUserReport(User user) { // 生成用户报告 System.out.println("Generating report for user"); } } 在这个例子中，UserRepository 只负责数据存储，而 UserReportGenerator 只负责生成报告。这样，每个类只负责一个功能，系统的职责划分更加清晰，修改某一功能时只需要修改对应的类，而不影响其他部分。 2. 开闭原则（Open/Closed Principle） 2.1 定义 开放-封闭原则指的是软件实体（类、模块、函数等）应该对扩展开放，对修改封闭。这意味着当我们需要为系统添加新功能时，应该通过扩展已有的代码，而不是直接修改已有的代码。 2.2 不符合OCP原则举例 假设我们有一个计算薪水的程序，原本只考虑基本工资和奖金两个因素。如果以后需要加入更多的薪酬计算因素（如补贴、扣款等），如果我们每次都修改原有的计算逻辑，就会违反开放-封闭原则。 // 违反 OCP 原则：修改原有类以支持新的薪资计算方式 public class SalaryCalculator { public double calculateSalary(Employee employee) { // 计算基本工资和奖金 return employee.getBaseSalary() + employee.getBonus(); } } 假设现在我们需要新增一个“补贴”因素，每次添加新因素时都需要修改 SalaryCalculator 类的 calculateSalary 方法。随着需求变化，SalaryCalculator 类会变得越来越复杂，而且容易出错。这就是违反了开放-封闭原则。 2.3 符合OCP原则举例 为了遵守开放-封闭原则，我们可以引入抽象类或接口，将不同的薪资计算策略封装成不同的类，而不是修改 SalaryCalculator 类。 // 遵守 OCP 原则：将薪资计算逻辑通过策略模式封装，便于扩展 public interface SalaryCalculatorStrategy { double calculateSalary(Employee employee); } public class BaseSalaryAndBonusCalculator implements SalaryCalculatorStrategy { @Override public double calculateSalary(Employee employee) { // 计算基本工资和奖金 return employee.getBaseSalary() + employee.getBonus(); } } public class BaseSalaryBonusAndSubsidyCalculator implements SalaryCalculatorStrategy { @Override public double calculateSalary(Employee employee) { // 计算基本工资、奖金和补贴 return employee.getBaseSalary() + employee.getBonus() + employee.getSubsidy(); } } public class SalaryCalculator { private SalaryCalculatorStrategy strategy; public SalaryCalculator(SalaryCalculatorStrategy strategy) { this.strategy = strategy; } public double calculateSalary(Employee employee) { return strategy.calculateSalary(employee); } } 我们将计算薪水的逻辑提取到不同的策略类中，并定义了一个共同的 SalaryCalculatorStrategy 接口。每个薪资计算策略都实现了这个接口，分别处理不同的计算逻辑（如仅包含基本工资和奖金，或包含补贴等）。通过策略模式，我们能够灵活地扩展薪资计算方式，而无需修改现有的代码。如果以后还需要新增其他薪资计算方式，只需要新建一个实现 SalaryCalculatorStrategy 接口的类，而不需要修改 SalaryCalculator 类。 3. 里氏替换原则 (LSP) 3.1 定义 里氏替换原则是指子类对象应该能够替换父类对象，且程序的行为不受影响。换句话说，子类必须完全继承父类的行为，并且在替换父类实例时不会破坏父类的行为和功能。 3.2 符合LSP原则举例 假设我们有一个形状类 Shape，其中有一个方法 area() 来计算图形的面积。然后我们有两个子类：Rectangle 和 Square。根据几何学，矩形的面积是 宽 * 高，而正方形的面积是 边长 * 边长。但是，假如我们在正方形类中重写了 setWidth 和 setHeight 方法，使其不再符合父类 Rectangle 的行为，那么就违反了里氏替换原则。 // 违反 LSP 原则：正方形类破坏了矩形类的预期行为 class Rectangle { private double width; private double height; public void setWidth(double width) { this.width = width; } public void setHeight(double height) { this.height = height; } public double area() { return width * height; } } class Square extends Rectangle { @Override public void setWidth(double width) { super.setWidth(width); super.setHeight(width); // 强制宽度和高度相等，违反矩形的行为 } @Override public void setHeight(double height) { super.setHeight(height); super.setWidth(height); // 强制宽度和高度相等，违反矩形的行为 } } 正方形是矩形的一种特殊情况，但正方形并不等同于矩形。矩形的宽度和高度是独立的，而正方形的宽度和高度始终相等。如果我们让正方形继承矩形类，并且让 setWidth 和 setHeight 强制设置相等的值，这将破坏矩形类的预期行为。当我们将一个 Square 对象作为 Rectangle 类型的对象传递时，程序的行为会不符合预期，setWidth 和 setHeight 会强制设置相等的值，而这在矩形的情况下并不应该这样。 3.3 符合LSP原则举例 为了遵守里氏替换原则，我们应该避免让 Square 直接继承 Rectangle，因为它违反了矩形的行为。相反，我们可以通过创建一个新的抽象类 Shape 来统一接口，Rectangle 和 Square 各自继承并实现具体的行为。 // 遵守 LSP 原则：通过抽象类和接口来正确设计 abstract class Shape { public abstract double area(); } class Rectangle extends Shape { private double width; private double height; public Rectangle(double width, double height) { this.width = width; this.height = height; } public double area() { return width * height; } } class Square extends Shape { private double side; public Square(double side) { this.side = side; } public double area() { return side * side; } } Shape 类作为一个抽象类，定义了所有图形的公共接口 area() 方法。 Rectangle 和 Square 都继承 Shape 类，但它们分别根据自己的特点实现 area() 方法，而不再强行使用不合适的继承关系。 正确的继承：Rectangle 和 Square 的行为各自独立，满足了不同的需求。正方形和矩形之间的差异得到了合理的隔离。 4. 接口隔离原则（ISP) 4.1 定义 接口隔离原则指的是不应该强迫一个类去依赖它不需要的接口。换句话说，接口应该尽可能地小，避免设计过于庞大和臃肿的接口。客户端不应该被迫实现它不使用的方法。 简单理解： 如果一个类依赖了一个接口，并且这个接口中有一些方法是该类不需要的，那么就违反了接口隔离原则。 应该将一个大接口拆分成多个小接口，每个接口承担一个独立的职责，这样类只需要实现它需要的接口，而不是实现整个庞大的接口。 4.2 不符合ISP原则举例 假设我们有一个 Worker 接口，包含了多种不同的功能方法，像是 work、eat、sleep 等。然而，某些具体的实现类（比如 Robot）并不需要所有这些方法，比如它不需要 eat 和 sleep 方法。这个接口设计就违反了接口隔离原则。 // 违反 ISP 原则：接口包含了不需要的方法 public interface Worker { void work(); void eat(); void sleep(); } public class Human implements Worker { @Override public void work() { System.out.println("Human working..."); } @Override public void eat() { System.out.println("Human eating..."); } @Override public void sleep() { System.out.println("Human sleeping..."); } } public class Robot implements Worker { @Override public void work() { System.out.println("Robot working..."); } @Override public void eat() { // Robot doesn't need to implement this method } @Override public void sleep() { // Robot doesn't need to implement this method } } 在这个例子中，Worker 接口定义了 eat 和 sleep 方法，但对于 Robot 类而言，这些方法并不适用，因此需要实现这些无用的方法，违反了接口隔离原则。Robot 类并不需要 eat 和 sleep 方法，但是由于接口设计不合理，它不得不提供这些空实现，从而导致了不必要的代码。 4.3 符合ISP原则举例 为了解决这个问题，我们可以将接口拆分成多个小接口，每个接口仅包含客户端需要的方法。Human 类实现包含 work、eat、sleep 的接口，而 Robot 类只实现 work 方法相关的接口。 // 遵守 ISP 原则：将接口拆分成多个小接口 public interface Workable { void work(); } public interface Eatable { void eat(); } public interface Sleepable { void sleep(); } public class Human implements Workable, Eatable, Sleepable { @Override public void work() { System.out.println("Human working..."); } @Override public void eat() { System.out.println("Human eating..."); } @Override public void sleep() { System.out.println("Human sleeping..."); } } public class Robot implements Workable { @Override public void work() { System.out.println("Robot working..."); } } 5. 依赖倒置原则（Dependency Inversion Principle） 5.1 定义 依赖倒置原则是指高层模块不应该依赖低层模块，二者都应该依赖于抽象；抽象不应该依赖细节，细节应该依赖于抽象。 简单来说，依赖倒置原则强调的是通过依赖于抽象（如接口或抽象类），而不是依赖于具体实现，从而实现高层模块和低层模块的解耦。 5.2 不符合DIP原则举例 假设我们有一个 UserService 类，它依赖于 UserRepository 类来进行数据存储。如果 UserService 直接依赖于 UserRepository 类，这样就违反了依赖倒置原则，因为高层模块（UserService）直接依赖于低层模块（UserRepository）。 // 违反 DIP 原则：高层模块直接依赖低层模块 class UserRepository { public void save(User user) { // 保存用户到数据库 System.out.println("User saved to database."); } } class UserService { private UserRepository userRepository; public UserService() { userRepository = new UserRepository(); // 直接依赖具体实现 } public void registerUser(User user) { // 注册用户 userRepository.save(user); } } UserService 直接依赖于 UserRepository 的实现。这样，如果我们需要更改 UserRepository 的实现，比如从数据库改为从文件中保存数据，UserService 就需要修改其代码，违反了高层模块不应该依赖低层模块的原则。 如果以后需要实现不同类型的存储方式，比如 FileRepository 或 MongoDBRepository，每次都需要修改 UserService，导致代码不容易维护和扩展。 5.3 符合DIP原则举例 为了遵守依赖倒置原则，我们应该引入一个抽象接口，UserRepository 应该是一个接口，而 UserService 只依赖这个接口，而不依赖具体实现类。具体的实现类可以通过依赖注入的方式传入。 // 遵守 DIP 原则：通过接口隔离高层模块和低层模块 interface UserRepository { void save(User user); } class UserRepositoryImpl implements UserRepository { @Override public void save(User user) { // 保存用户到数据库 System.out.println("User saved to database."); } } class FileRepository implements UserRepository { @Override public void save(User user) { // 保存用户到文件 System.out.println("User saved to file."); } } class UserService { private UserRepository userRepository; // 通过构造器注入依赖 public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public void registerUser(User user) { // 注册用户 userRepository.save(user); } } 6. 合成复用原则（Composite Reuse Principle） 6.1 定义 合成复用原则是指优先使用对象的合成（组合）而非继承来实现代码复用。这意味着，通过将已有的类作为成员对象嵌入到新类中，来增强新类的功能，而不是通过继承扩展原有类的功能。 组合优于继承：当需要复用某些功能时，尽量通过组合来实现，而不是通过继承。这是因为继承在很多情况下会导致类之间的耦合性增加，而组合则更灵活且易于扩展。 6.2 不符合CRP原则举例 假设我们有一个 Bird 类和一个 Penguin 类，Penguin 是 Bird 的子类。我们在 Bird 类中定义了一些行为（如飞行），但是企鹅是不能飞的。如果我们用继承来实现复用，这样就会导致不合理的设计。 // 违反合成复用原则：企鹅类继承了鸟类，导致不适用的行为 class Bird { public void fly() { System.out.println("Bird is flying."); } } class Penguin extends Bird { // Penguin 是鸟类的一种，但企鹅不能飞 @Override public void fly() { // 企鹅不能飞，但继承了飞行行为，导致设计不合理 System.out.println("Penguins can't fly."); } } 6.3 符合CRP原则举例 为了遵守合成复用原则，我们可以将飞行行为抽象出来，并将其作为一个接口或者类的成员。这样，企鹅类就不再需要继承鸟类，而是根据实际需要来组合不同的行为。 // 遵守合成复用原则：通过组合来复用功能 interface Flyable { void fly(); } class Bird implements Flyable { @Override public void fly() { System.out.println("Bird is flying."); } } class Penguin { // 企鹅并不具备飞行能力，不需要继承 Bird 类 // 如果需要飞行，可以通过组合一个 Flyable 对象 private Flyable flyable; public Penguin(Flyable flyable) { this.flyable = flyable; } public void tryToFly() { // 企鹅不能飞，但如果需要，可以选择其他飞行方式 if (flyable != null) { flyable.fly(); } else { System.out.println("Penguins can't fly."); } } } class Test { public static void main(String[] args) { // 创建一个可以飞的鸟类 Bird bird = new Bird(); Penguin penguin = new Penguin(null); // 企鹅没有飞行能力 penguin.tryToFly(); // 输出：Penguins can't fly. // 如果需要，可以通过组合飞行的能力 Penguin flyingPenguin = new Penguin(bird); flyingPenguin.tryToFly(); // 输出：Bird is flying. } } 7. 最少知识原则（迪米特法则） 7.1 定义 最少知识原则，又称迪米特法则，其核心思想是一个对象应该对其他对象有尽可能少的了解。也就是说，一个对象应该只与自己直接的朋友（即直接相关的对象）进行交互，而不应该与“陌生人”或“陌生人的朋友”进行交互。 7.2 不符合迪米特法则举例 假设我们有一个 Customer 类，它有一个 Order 类属性，而 Order 类又包含 Product 类。如果 Customer 直接访问 Product 类的属性，这样就违反了最少知识原则。 class Product { public String name; public double price; } class Order { private Product product; public Order(Product product) { this.product = product; } public Product getProduct() { return product; } } class Customer { private Order order; public Customer(Order order) { this.order = order; } // 违反最少知识原则：直接访问了 Product 对象的属性 public void printProductName() { System.out.println(order.getProduct().name); // 访问了 order -> product -> name } } 7.3 符合迪米特法则举例 class Product { private String name; private double price; public Product(String name, double price) { this.name = name; this.price = price; } public String getName() { return name; } } class Order { private Product product; public Order(Product product) { this.product = product; } public String getProductName() { return product.getName(); } } class Customer { private Order order; public Customer(Order order) { this.order = order; } // 遵守最少知识原则：只通过 Order 类与 Product 交互 public void printProductName() { System.out.println(order.getProductName()); } }
观察者模式 观察者模式定义了一种一对多的依赖关系。当一个对象的状态发生变化时，所有依赖于它的对象都会收到通知并自动更新。它通常用于事件处理系统，允许对象之间实现松耦合，便于扩展和维护。 观察者模式的结构 Subject（主题/被观察者）：管理所有的观察者对象，提供注册和删除观察者的方法。当自身状态改变时，它负责通知所有观察者。 Observer（观察者）：定义一个更新接口，用于接收来自被观察者的通知。 ConcreteSubject（具体主题）：具体的被观察者对象，状态发生变化时通知观察者。 ConcreteObserver（具体观察者）：具体的观察者对象，实现了观察者接口，并在接收到通知时做出反应。 举例 假设我们有一个股票市场系统，股票价格变动时需要通知所有的投资者。我们可以用观察者模式来实现这个场景。 package com.example.demo.demos.test; import java.util.ArrayList; import java.util.List; public class test01 { public static void main(String[] args) { Stock stock = new Stock(); Investor investor1 = new Investor("Alice"); Investor investor2 = new Investor("Bob"); Investor investor3 = new Investor("Charlie"); stock.registerObserver(investor1); stock.registerObserver(investor2); stock.registerObserver(investor3); stock.setPrice(120.0f); stock.removeObserver(investor2); stock.setPrice(130.0f); } } interface Observer{ void update(float price); } interface Subject{ void registerObserver(Observer o); void removeObserver(Observer o); void notifyObservers(); } class Stock implements Subject{ private List<Observer> observers ; private float price; public Stock(){ observers = new ArrayList<>(); } @Override public void registerObserver(Observer o) { observers.add(o); } @Override public void removeObserver(Observer o) { observers.remove(o); } @Override public void notifyObservers() { for(Observer o: observers){ o.update(price); } } public void setPrice(float price){ this.price = price; notifyObservers(); } } class Investor implements Observer{ private String name; public Investor(String name){ this.name = name; } @Override public void update(float price) { System.out.printf("%s: The stock price has changed to %.2f%n", name, price); } } 输出结果如下： Alice: The stock price has changed to 120.00 Bob: The stock price has changed to 120.00 Charlie: The stock price has changed to 120.00 Alice: The stock price has changed to 130.00 Charlie: The stock price has changed to 130.00
UML建模语言 1. UML概述 ​ UML(Unified Modeling Language) 是软件界第一个统一的建模语言，该方法结合了Booch，OMT,和OOSE方法的优点，统一了符号体系，并从其他的方法和工程实践中吸收了许多经过实际检验的概念和技术 ​ 他是一种标准的表示，已成为国际软件界广泛承认的标准。他是第三代面向对象的开发方法，是一种基于面向对象的可视化的通用建模语言。为不同领域的用户提供了统一的交流标准--UML图 ​ UML应用领域很广泛，可用于软件开发建模的各个阶段，商业建模，也可用于其他类型的系统 1.1 什么模型？为什么要建模？ ​ 模型是一个系统的完整的抽象。人们对某个领域特定问题的求解及解决方案，对他们的理解和认识都蕴含在模型中。 ​ 通常，开发一个计算机系统是为了解决某个领域的特定问题，问题的求解过程，就是领域问题到计算机系统的映射。 UML作为一种可视化的建模语言，提供了丰富的基于面向对象概念的模型元素及其图形表示元素 1.2 UML的形成 九十年代中，面向对象方法已经成为软件分析和设计方法的主流。 1994年10月Jim Rumbaugh和Grady Booch共同合作把他们的OMT和Booch方法统一起来，到1995年成为“统一方法”(Unified Method）版本0.8。随后，IvarJacobson加入，并采用他的用例(User case)思想,到1996年，成为“统一建模语言”版本0.9。 1997年1月，UMIL版本1.0被提交给OMG组织，作为软件建模语言标准的候选。其后的半年多时间里，一些重要的软件开发商和系统集成商都成为“UML伙伴”，如IBM,Mircrosoft,HP等.1997年11月7日被正式采纳作为业界标准。 1.3 UML的主要内容 UML的定义包括UML语义和UML表示法两个部分 （1）UML语义 描述基于UML的精确元模型定义。元模型为UML的所有元素在语法和语义上提供了简单、一致、通用的定义性说明，使开发者能在语义上取得一致，消除了因人而异的表达方法所造成的影响。此外UML还支持对元模型的扩展定义。 UML支持各种类型的语义。如布尔、表达式、列表、阶、名字、坐标、字符串和时间等，还允许用户自定义类型。 （2）UML表示法 定义UML符号的表示法，为开发者或开发工具使用这些图形符号和文本语法为系统建模提供了标准。这些图形符号和文字所表达的是应用级的模型，在语义上它是UML元模型的实例 1.4 视图 一个系统应从不同的角度进行描述，从一个角度观察到的系统称为视图。 视图由多个图构成，他不是一个图表，而是在某一个抽象层上，对系统的抽象表示。 如果要为系统建立一个完整的模型图，需定义一定数量的视图，每个视图表示系统的一个特殊方面。另外，视图还把建模语言和系统开发时选择的方法或过程连接起来。 1.5 图 UML语言定义了5种类型和9种不同的图，把他们有机的结合起来就可以描述系统的所有视图。 用例图（user case diagram）从用户角度描述系统功能，并指出各功能得操作者 静态图（static diagram），表示系统的静态结构。包括类图、对象图、包图 行为图（behavior diagram），描述系统的动态模型和组成对象间的交互关系。包括状态图、活动图 交互图（interactive diagram），描述对象间的交互关系。包括顺序图、合作图 实现图（implementation diagram）用于描述系统的物理实现。包括构件图、部署图 1.6 模型元素 代表面向对象中的类，对象，关系和消息等概念，是构成图的最基本的常用的元素。一个模型元素可以用在多个不同的图中，无论怎样使用，他总是具有相同的含义和相同的符号表示。 1.7 通用机制 用于表示其他信息，比如注释，模型元素的语义等。 另外，为了适应用户的需求，他还提供了扩展机制，包括构造型、标记值和约束。使用UML语言能够适应一个特殊的方法（或过程），或扩充至一个组织或用户 1.8 UML的特点 （1）统一标准：UML统一了Booch、OMT和OOSE等方法中的基本概念，已成为OMG的正式标准，提供了标准的面向对象的模型元素的定义和表示。 （2）面向对象：UML还吸取了面向对象技术领域中的其他流派的长处。UML符号表示考虑了各种方法的图形表示，删掉了大量易引起混乱的、多余的和极少使用的符号，也添加了一些新符号。 （3）可视化、表示能力强 系统的逻辑模型或实现模型都能用UML模型清晰的表示，可用于复杂软件系统的建模 （4）独立于过程：UML是系统建模语言，独立于开发过程 2. 通用模型元素 模型元素是UML构造系统的各种元素，是UML构建模型的基本单位。模型元素代表面向对象中的类，对象，关系和消息等概念，是构成图的最基本的常用的概念。分为一下两类： 基元素 是已由UML定义的模型元素。如：类、结点、构件、注释、关联依赖和泛化等。 构造型元素 在基元素的基础上构造的新的模型元素,是由基元素增加了新的定义而构成的，如扩展基元素的语义(不能扩展语法结构）,也允许用户自定义。构造型用括在双尖括号《》中的字符串表示。 目前UML提供了40多个预定义的构造型元素。如使用《Use》、扩展《Extend 》 2.1 模型元素 可以在图中使用的概念统称为模型元素。模型元素在图中用其相应的视图元素（符号）表示，图给出了常用的元素符号:类、对象、结点、包和组件等。 模型元素与模型元素之间的连接关系也是模型元素，常见的关系有关联（ association）、泛化（generalization）、依赖(dependency)和聚合(aggregation)，其中聚合是关联的一种特殊形式。这些关系的图示符号如图所示。 关联：连接模型元素及链接实例 依赖：表示一个元素以某一种方式依赖于另一种元素 泛化：表示一般与特殊的关系，即“一般”元素是“特殊”关系的泛化。 聚合：表示整体与部分的关系 2.2 关联和链接 关联是两个或多个类之间的一个关系。链是关联的具体体现。关联有二元关联、三元关联、多元关联 2.3 约束 UML中提供了一种简便的、统一的和一致的约束，是各种模型元素的一种语义条件或限制。一条约束只能应用于同一类元素。 约束的表示： 如果约束应用于一种具有相应视图元素的模型元素，它可以出现在它所约束元素视图元素的旁边。 通常一个约束由一对花括号括起来（{constraint}），花括号中为约束内容。 对泛化的约束：应用于泛化的约束,显示在大括号里,若有多个约束，用逗号隔开。如果没有共享，则用一条虚线通过所有继承线,并在虚线的旁边显示约束,如图所示: 关联的约束：如xor关联，某时刻只有一个对象关联 2.4 依赖 ·依赖关系描述的是两个模型元素（类，组合,用例等)之间的语义上的连接关系，其中一个模型元素是独立的，另一个模型元素是非独立的(或依赖的）。如图表示类A依赖干类B的一个友元依赖关系。 依赖的形式可能是多样的，针对不同的依赖的形式，依赖关系有不同的变体。 3. 用例图 1992年由Jacobson提出了Use case的概念及可视化的表示方法—Use case图，受到了IT界的欢迎，被广泛应用到了面向对象的系统分析中。用例驱动的系统分析与设计方法已成为面向对象的系统分析与设计方法的主流。 用例模型由Jacobson在开发AXE系统中首先使用,并加入由他所倡导的OOSE和Objectory方法中。用例方法引起了面向对象领域的极大关注。自1994年IvarJacobson的著作出版后,面向对象领域已广泛接纳了用例这一概念,并认为它是第二代面向对象技术的标志。 3.1 用例建模概述 用例建模技术，用于描述系统的功能需求。在宏观上给出模型的总体轮廓。通过对典型用例的分析，使开发者能够有效地了解用户的需求。 3.2 用例模型 用例模型描述的是外部执行者(Actor)所理解的系统功能。它描述了待开发系统的功能需求。 它驱动了需求分析之后各阶段的开发工作,不仅在开发过程中保证了系统所有功能的实现,而且被用于验证和检测所开发的系统,从而影响到开发工作的各个阶段和UML的各个模型。 用例模型由若干个用例图构成，用例图中主要描述执行者和用例之间的关系。在UML中，构成用例图的主要元素是用例和执行者及其它们之间的联系。 一 、执行者：执行者是指用户在系统中所扮演的角色。执行者在用例图中是用类似人的图形来表示，但执行者可以是人，也可以是一个外界系统。 注意:用例总是由执行者启动的。 二、 用例：从本质上讲,一个用例是用户与计算机之间的一次典型交互作用。在UML中,用例被定义成系统执行的一系列动作(功能) 用例有以下特点： 用例捕获某些用于可见的需求，实现一个具体的用户目标 用例由执行者激活，并将结果值返回给执行者 用例必须具有功能上的完整描述 3.3 用例图 用例图描述了系统的功能需求,它是从执行者的角度来理解系统，由“执行者”、“用例”和“用例之间的关系”3类模型元素构成。 图中还有另外两种类型的连接,即《使用》和《扩展》关系，是两种不同形式的泛化关系。 《Use》表示一个用例使用另一个用例。 《Extend》通过向被扩展的用例添加动作来扩展用例。 例如金融管理系统：(逐渐细化) 4. 静态建模 任何建模语言都以静态建模机制为基础,标准建模语言UML也不例外。所谓静态建模是指对象之间通过属性互相联系，而这些关系不随时间而转移。 UML静态建模机制包括： 用例图 类图 对象图 包图 构件图 配置图 4.1 类图与对象图 对象类简称类,是面向对象模型的最基本的模型元素，用类图来描述。类图(Class diagram)由系统中使用的类以及它们之间的关系组成，是描述系统的一种图式，分为长式和短式。类及类型名均用英文大写字母开头,属性及操作名为小写字母开头。常见类型有: Char,Boolean,Double,Float,Integer, Object,Short,String等。类图是构建其它图的基础。 对象是对象类的实例(instance),用对象图来描述。对象图亦分长式和短式。 （1）属性 属性用来描述类的特征，表示需要处理的数据 属性有 “可见性”，“属性名”，“类型”，以及“约束特性”等组成 其中：可见性（visibility)表示该属性对类外的属性是否可见 分为： public（+）公有，private（-）私有，protected（#）受保护。如果可见性未申明，表示其可见性不确定 （2）操作 对数据的具体处理方法的描述则放在操作部分，操作说明了该类能做些什么工作。操作通常称为函数，他是类的一个组成部分，只能作用于该类的对象上。 操作有 “可见性”，“操作名“，”参数列表“，”返回类型“，以及”约束特性“等组成 4.2 UML中类之间的关系 4.2.1 关联关系 常规关联 多元关联 有序关联 受限关联 或关联 关联类 其他关联：递归关联 4.2.2 聚集 聚集关系描述的是部分与整体关系的关联。简单的说，它将一组元素通过关联组成一个更大，更复杂的单元，这种关联关系就是聚集。聚集关系描述了“has a”的关系。 在UML中，它用带有空菱形的线段来表示，空菱形与聚集类相连，其中头部指向整体。 组成关系则是一种更强形式的关联，在整体中拥有管理部分的特有职责，有时也被称为强聚合关系。 在组合中，成员对象的生命周期取决于聚合的生命周期，聚合不仅控制着成员对象的行为，而且控制者成员对象的创建和结束。 在UML中，组合关系使用带实心菱形头的实线来表示，头部指向整体。 4.2.3 泛化 泛化指出类之间的“一般与特殊关系”，即继承关系。父类与子类之间构成类的分层结构 4.3 包图 一个最古老的软件方法问题是:怎样将大系统拆分成小系统。UML中解决该问题的思路之一是将许多类集合成一个更高层次的单位,形成一个高内聚、低耦合的类的集合。UML中这种分组机制叫包(Package)。引入包是为了降低系统的复杂性。 包是一种组合机制，把各种各样的模型元素通过内在的语义连在一起成为一个整体就叫包，构成包的模型元素称为包的内容，包通常用于对模型的组织管理，因此有时又将包称为子系统(subsystem)。包拥有自己的模型元素，包与包之间不能共用一个相同的模型元素，包的实例没有任何语义(含义)。仅在模型执行期间包才有意义。 包的内容：可以是类的列表，也可以是另一个包图，还可以是一个类图 包之间的关系有依赖和泛化
Docker 1. docker概述 docker三件套：镜像，容器，仓库 容器与镜像的关系类似于面向对象编程中的对象与类 Docker 面向对象 容器 对象 镜像 类 从面向对象角度 Docker利用容器独立运行的一个或一组应用，应用程序或服务运行在容器里面，容器就类似于一个虚拟化的运行环境，容器是用镜像创建的运行实例。就像是Java中的类和实例对象一样，镜像是静态的定义，容器是镜像运行时的实体。容器为镜像提供了一个标准的和隔离的运行环境，它可以被启动、开始、停止、删除。每个容器都是相互隔离的、保证安全的平台。 从镜像容器的角度 可以把容器看做是一个简易版的Linux环境（包括root用户权限、进程空间、用户空间和网络空间等）和运行在其中的应用程序 关于docker安装可自行上CSDN搜索，比较简单。注意配置国内镜像仓库 2. 为什么docker会比虚拟机快 docker有着比虚拟机更少的抽象层 由于docker不需要Hypervisor实现硬件资源虚拟化，运行在docker容器上的程序直接使用的都是实际物理机的硬件资源。因此在CPU、内存利用率上docker将会在效率上有明显的优势 docker利用的是宿主机内核，而不需要加载操作系统OS的内核 当新建一个容器时，docker不需要和虚拟机一样重新加载一个操作系统内核。进而避免引寻、加载操作系统内核返回等比较费时费资源的过程，当新建一个虚拟机时，虚拟机软件需要加载OS，返回新建过程是分钟级。而docker由于直接利用宿主机操作系统，则省略了返回过程，因此新建一个docker容器只需要几秒钟 Docker容器 虚拟机（VM） 操作系统 与宿主机共享OS 宿主机OS上运行虚拟机OS 存储大小 镜像小，便于存储与传输 镜像庞大（vmdk、vdi等） 运行性能 几乎无额外性能损失 操作系统额外的CPU、内存消耗 移植性 轻便、灵活、适应于Linux 笨重，与虚拟化技术耦合度高 硬件亲和性 面向软件开发者 面向硬件运维者 部署速度 快速，秒级 较慢，10s以上 3. Docker常用命令 3.1 帮助启动类命令 启动docker：systemctl start docker 停止docker： systemctl stop docker 重启docker：systemctl restart docker 查看docker状态 systemctl status docker 开机启动：systemctl enable docker 查看docker概要信息：docker info 查看docker总体帮助文档：docker --help 查看docker命令帮助文档： docker具体命令 --help 3.2 镜像命令 docker images-》 列出本地主机上的镜像 docker search 镜像名 docker pull 镜像名 docker system df-》 查看镜像/容器/数据卷所占的空间 docker rmi 镜像ID ps：仓库名和标签名都是None的镜像称为虚悬镜像 3.3 容器命令 只要记住--help现查就可以 本次以Ubuntu镜像演示 启动ubuntu docker run -it ubuntu bash 参数说明： -i ：交互式操作 -t： 终端 Ubuntu：Ubuntu镜像 bash：放在镜像名后的是命令，这里我们希望有个交互式Shell，因此用bash（/bin/bash） exit退出终端（ctrl+p+q退出不会停止容器） 列出正在运行的容器 docker ps 重启容器 docker restart容器ID或容器名 停止容器 docker stop 容器ID或容器名 强制停止容器 docker kill 容器ID或容器名 启动已停止的容器 docker start 容器ID或容器名 启动守护式容器（后台服务器） 说明：Docker容器后台运行，就必须有一个前台进程。容器运行的命令如果不是那些一直挂起的命令（比如top，tail），就会自动退出（交互式容器），如 docker run -d ubuntu ​ 我们可以看到虽然启动了一个容器，但是没有正在运行的容器，代表在启动一瞬间就自己退出了。 ​ 下面用redis前后台启动演示case ​ 前台交互式启动 docker run -it redis:latest 后台守护式启动 docker run -d redis ​ 此时并没有自动退出，因为redis是守护式容器 ​ 可以通过docker logs+容器ID查看前台输出 查看容器内运行的进程 docker top 容器ID 查看容器内部细节 docker inspect 容器ID 进入后台容器并以命令行交互 docker exec -it 容器ID bash docker attach 容器ID 推荐使用docker exec命令，因为推出容器终端，不会导致容器停止 attach直接进入容器启动命令的终端，不会启动新的进程，用exit推出，会导致容器的停止 exec是在容器中打开新的终端，并且可以启动新的进程，用exit推出，不会导致容器的停止 拷贝容器文件到主机上 docker cp 容器ID:容器内路径 目的主机路径 导入导出容器 docker export 容器ID > 文件名.tar cat 文件名.tar | docker import - 镜像用户/镜像名：镜像版本号 4. docker镜像底层原理 UnionFS（联合文件·系统）：Union文件系统（UnionFS）是一种分层、轻量级并且高性能的文件系统，它支持对文件系统的修改作为一次提交来一层层的叠加，同时可以将不同目录挂载到同一个虚拟文件系统下。Union文件系统是Docker镜像的基础。镜像可以通过分层来进行继承，基于基础镜像（类比Java中的Object类），可以制作各种具体的镜像。 特性：一次同时加载多个文件系统，但从外面看起来，只能看到一个文件系统，联合加载会把各层文件系统叠加起来，这样最终的文件系统会包含所有的底层和目录。 4.1 docker镜像加载原理 docker镜像实际上由一层一层的文件系统组成，这种层级的文件系统UnionFS。 bootfs（boot file system）主要包含bootloader和kernel，bootloader主要是引导加载kernel，Linux刚启动时会加载bootfs文件系统，在Docker镜像的最底层是引导文件系统bootfs。这一层与我们典型的Linux/Unix系统是一样的，包含boot加载器和内核。当boot加载完成之后整个内核就在内存中了，此时内存的使用权已由botofs转交给内核，此时系统也会卸载bootfs。 rootfs（root file system），在bootfs之上。包含的就是典型Linux系统中的/dev,/proc,/bin,/etc等标准目录和文件。rootfs就是各种不同操作系统发行版，比如Ubuntu，Centos等等。对于一个精简的OS，rootfs可以很小，只需要包括最基本的命令、工具和程序库就可以了，因为底层直接用Host的kernel，自己只需要提供rootfs就行了。由此可见对于不同的linux发行版，bootfs基本是一致的，rootfs会有差别，因此不同的发行版可以公用bootfs。 4.2 镜像分层好处及注意点 镜像分层的最大一个好处就是共享资源，方便复制迁移，就是为了复用。比如说有多个镜像都从相同的base镜像构建而来，那么Docker Host只需要在磁盘上保存一份base镜像；同时内存中也只需要加载一份base镜像，就可以为所有容器服务了。而且镜像的每一层都可以被共享。 ps：Docker镜像层都是只读的，容器层是可写的。当容器层启动时，一个新的可写层被加载到镜像的顶部。这一层通常被称作“容器层”，“容器层”之下的都叫“镜像层”。所有对容器的改动 - 无论添加、删除、还是修改文件都只会发生在容器层。只有容器层是可写的，容器层下面的所有镜像层都是只读的。 5.docker commit操作案例 本案例以ubuntu镜像添加 vim 命令为例 docker commit //提交容器副本使之成为一个新的镜像 docker commit -m="提交的描述信息" -a="作者" 容器ID 要创建的目标镜像名:[标签名] 如图，原始默认的ubuntu镜像是不带 vim 命令的 首先升级apt apt update 安装vim apt install -y vim 安装完成后就可以使用vim命令 打包成镜像，注意等号‘=’前后不要加空格 docker commit -m="add vim cmd" -a="yellowswimming" 6aad128010e3 hy/myubuntu:1.0 打包后可以查看到我们发布的镜像以及大小 现在重新启动打包好的镜像 现在就有vim命令了，不用再去额外安装，且刚才写入的内容依旧存在 6. 远程仓库和本地仓库 6.1 远程仓库 这里我使用的是阿里云容器镜像服务 创建命名空间 创建仓库（以添加了vim的ubuntu镜像为例） 根据操作显示指令 如果镜像仓库建立完毕，则会在网页上显示如何推送和拉取本地镜像（如果在创建仓库时选择其他代码源可忽略） 本地推送成功后，则会在阿里云镜像版本中看到对应的镜像 6.2 拉取远程镜像 现在我先删除已有的ubuntu镜像 从远程仓库拉取镜像 可以看到这里已经被拉取下来了 测试ubuntu之前的1.txt文件是否仍然存在 7. docker私有仓库搭建 官方Docker Hub地址：https://hub.docker.com/,中国大陆访问过慢并且有被阿里云替代的趋势 但是阿里云这样的镜像仓库也可能不太方便，涉及机密的公司不可能提供镜像给公网，所以需要搭建一个本地私人仓库给团队使用 Docker Registry是官方提供的工具，可以用于构建私有镜像仓库 下载registry镜像 运行容器 docker run -d -p 5000:5000 -v /hyuse/myregistry/:/tmp/registry --privileged=true registry 在官方ubuntu上添加ifconfig命令 apt update apt install net-tools commit镜像 docker commit -m="ifconfig cmd add" -a="hy" 8e87c4a29c2f myubuntu:1.1 curl验证私服上存在哪些镜像 curl -XGET http://10.0.0.16:5000/v2/_catalog 将新镜像myubuntu:1.1 修改为符合私服规范的tag docker tag myubuntu:1.1 10.0.0.16:5000/myubuntu:1.1 修改配置文件使之支持http(修改配置文件后需要重启docker) push推送到私服库(提前再次启动registry容器) docker push 10.0.0.16:5000/myubuntu:1.1 再次查看私服镜像 拉取私服镜像 docker pull 10.0.0.16:5000/myubuntu:1.1 8. 数据卷 卷就是目录或文件，存在于一个或多个容器中，由docker挂载到容器，但不属于联合文件系统，因此能够绕过Union File System提供的一些用于持续存储或共享数据的特性： 卷的设计目的就是数据的持久化，完全独立于容器的生存周期，因此Docker不会在容器删除时删除其挂载的数据卷。 命令： docker run -it/-d --privileged=true -v /宿主机绝对路径目录:/容器内目录 镜像名 容器卷特点： 数据卷可在容器之间共享或重用数据 卷中的更改可以直接实时生效 数据卷中的更改不会包含在镜像的更新中 数据卷的生命周期一直持续到没有容器使用它为之 下面以ubuntu演示 docker run -it --privileged=true -v /tmp/host_data:/tmp/docker_data --name=u1 ubuntu 如图，可以实现两端数据的同步 可通过docker inspcet 容器ID 查看容器详细信息 若限制容器内只读，则 docker run -it --privileged=true -v /tmp/host_data:/tmp/docker_data:ro --name=u1 ubuntu 这样，容器内修改文件就禁止了 数据卷的继承： 命令：docker run -it --privileged=true --volumes-from 父类容器名 --name u2 ubuntu docker run -it --privileged=true --volumes-from u1 --name u2 ubuntu 如图，u1的容器卷已经被继承过来了，并且u1和u2的修改都会被同步到对方以及主机目录(已经不是简单的继承了) 9.DockerFile DockerFile是用来构建Docker镜像的文本文件，是由一条条构建镜像所需的指令和参数构成的脚本文件 9.1 DockerFile内容的基础知识 每条保留字指令都必须为大写字母且后面要跟随至少一个参数 指令按照从上到下，顺序执行 表示注释 每条指令都会创建一个新的镜像层并对镜像进行提交 9.2 docker执行DockerFile的大致流程 docker从基础镜像运行一个容器 执行一条指令并对容器进行修改 执行类似docker commit的操作提交一个新的镜像层 docker再基于刚提交的镜像运行一个新容器 执行dockerfile的下一条指令直到所有指令都执行完成 9.3 DockerFile常见保留字 FROM 基础镜像，当前新镜像是基于哪个镜像的，指定一个已经存在的镜像作为模板，第一条必须是from MAINTAINER 镜像维护者的姓名和邮箱地址 RUN 容器构建时需要运行的命令。有shell和exec格式，RUN是在docker build 时运行 EXPOSE 当前容器对外暴露出的端口 WORKDIR 指定在创建容器后，终端默认登陆进来的工作目录，一个落脚点 USER 指定该镜像以什么样的用户去执行，如果都不指定，默认是root ENV 用来在构建镜像过程中设置环境变量 VOLUME 用来做数据保存和持久化工作 ADD 将宿主机目录下的文件拷贝进镜像且会自动处理URL和解压tar压缩包 CMD 指定容器启动后要干的事情。DockerFile可以有多个CMD指令，但只有最后一个生效，CMD会被docker run 之后的参数替换。它和前面RUN命令区别：1.CMD是在docker run 时运行 2.RUN是在docker build时运行 ENTRYPOINT 10. centos之dockerFile案例 要求： vim jdk8 ifconfig jdk下载地址：mirrors.yangxingzhen.com/jdk/ 首先编写Dockerfile文件 新建文件夹 myfile 上传刚下载的jdk8并且编辑Dockerfile FROM centos:7 MAINTAINER hy<yellowswimming@126.com> ENV MYPATH /usr/local WORKDIR $MYPATH #安装vim编辑器 RUN yum -y install vim #安装ifconfig命令查看网络IP RUN yum -y install net-tools #安装java8及lib库 RUN yum -y install glibc.i686 RUN mkdir /usr/local/java ADD jdk-8u171-linux-x64.tar.gz /usr/local/java/ #配置java环境变量 ENV JAVA_HOME /usr/local/java/jdk1.8.0_171 ENV JRE_HOME $JAVA_HOME/jre ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib:$CLASSPATH ENV PATH $JAVA_HOME/bin:$PATH EXPOSE 80 CMD echo $MYPATH CMD echo "success run centos by Dockerfile" CMD /bin/bash 下面开始构建 docker build -t centosjava8:1.0 . 最后那个. 代表当前目录下寻找Dockerfile文件 开始运行新镜像 现在已经实现了所有功能 11. 虚悬镜像 查看本地所有虚悬镜像 docker images ls -f dangling=true 一般来说，用了FROM 基本镜像后会生成虚悬镜像，此时我们需要把生成的虚悬镜像删除掉 移除所有虚悬镜像 docker image prune 12. Docker之network ​ 列出docker网络 docker network ls 通过 docker network --help 可以查看所有命令 作用： 容器间的互联和通信以及端口映射 容器IP变动时候可以通过服务名直接网络通信而不受到影响 12.1. network常见网络模式 网络模式 简介 bridge 为每一个容器分配、设置IP等，并将容器连接到一个docker0。虚拟网桥，默认为该模式 host 容器将不会虚拟出自己的网卡，配置自己的IP等，而是使用宿主机的IP和端口 none 容器有独立的Network namespace，但并没有对其进行任何网络设置，如分配veth pair 和网桥连接，IP等 container 新创建的容器不会创建自己的网卡和配置自己的IP，而是和一个指定对的容器共享IP、端口范围等 bridge模式：使用 --network bridge 指定， 默认使用docker0 host模式：使用 --network host 指定 none模式：使用 --network none 指定 container模式：使用 --network container:NAME或者容器ID指定 12.2. 底层ip和容器映射变化 这里IP是docker内部自动分配的 12.3 docker网络之bridge Docker使用Linux桥接，在宿主机虚拟一个Docker容器网桥（docker0），Docker启动一个容器时会根据Docker网桥的网段分配给容器一个IP地址，称为Container-IP，同时Docker网桥是每个容器的默认网关。因为在同一宿主机内的容器都接入同一个网桥，这样容器之间就能够通过容器的Container-IP直接通信 整个宿主机的网桥模式都是docker0，类似一个交换机有一堆接口，每个接口叫veth，在本地主机和容器内分别创建一个虚拟接口，并让它们彼此连通，这样一对接口叫veth pair 每个容器实例内部也有一块网卡，每个接口叫eth0 docker0上面的每个veth匹配某个容器实例内部的eth0，两两配对，一一匹配 通过上述，将宿主机上的所有容器都连接到这个内部网络上，两个容器在同一个网络下，会从这个网关下各自拿到分配的ip，此时两个容器的网络是互通的。 12.4 docker网络之host 直接使用宿主机的IP地址与外界进行通信，不需要额外的NAT转换 容器将不会获得一个独立的Network Namespace，而是和宿主机共用一个Network Namespace。容器将不会虚拟出自己的网卡而是使用宿主机的IP和端口。 12.5 docker网络之none 在none模式下，并不为Docker容器进行任何网络配置。也就是说，这个Docker容器没有网卡、ip、路由等信息，只有一个lo。需要我们自己为Docker容器添加网卡、配置IP等。 12.6 docker网络之container 新建的容器和已经存在的一个容器共享一个网络ip配置而不是和宿主机共享。新创建的容器不会创建自己的网卡，配置自己的IP，而是和一个指定的容器共享IP、端口范围等。同样，两个容器除了网络方面，其他的如文件系统、进程列表等还是隔离的。 启动命令例如 docker run -d -p 8081:8080 --network container:tomcat82 --name tomcat81 billygoo/tomcat8-jdk8 ps：如果被依赖的docker容器关闭了，那么自己也会只有一个lo了 12.7 docker网络之自定义网络 如果想让容器之间通过服务名相互访问，而不是简单通过ip地址访问（ip地址可能会发生变化），此时就需要加入自定义网络 docker network create customnetwork 接着启动两台tomcat容器，分别为tomcat81和tomcat82，注意启动时加入自定义网络 进入tomcat81容器，然后直接ping服务名，如果没有安装ping命令，需要手动安装 13. Docker-compose容器编排 Compose是Docker公司推出的一个工具软件，可以管理多个Docker容器组成一个应用。你需要定义一个yaml格式的配置文件docker-compose.yml，写好多个容器之间的调用关系。然后，只要一个命令，就能同时启动/关闭这些容器 13.1 首先按照官方文档下载docker-compose 13.2 docker-compose核心概念 编写Dockerfile定义各个微服务应用并构建出对应的镜像文件 使用docker-compose.yml定义一个完整的业务单元，安排好整体应用中的各个容器服务。 随后，执行docker-compose up 命令来启动并运行整个应用程序，完成一键部署上线 14. 容器可视化工具 Portainer是一款轻量级的应用，它提供了图形化界面，用于方便地管理Docker环境，包括单机环境和集群环境 使用如下命令下载Portainer docker run -d -p 8000:8000 -p 9000:9000 --name portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer 然后访问linux服务器9000端口，注册登录好admin后即可见到如下界面
FAERY: An FPGA-accelerated embedding-based Retrieval System
FAERY: An FPGA-accelerated embedding-based Retrieval System 参考文献 参考文献 1. 摘要 基于嵌入的检索（EBR)广泛用于推荐系统，以从包含百万个或者更多项目的大型语料库中检索数千个相关候选项。一个好的EBR系统需要同时实现高通量和低延迟，因为高吞吐量意味着节省成本，而低延迟可以改善用户体验.不幸的是，现有的基于CPU和GPU的的EBR的性能由于其固有的架构限制而远非最佳。在下文中，我将首先介绍一个理想而实用的EBR系统是如何工作的，然后基于ideal状况，人们提出并设计的FPGA加速的EBR-FAERY，它实现了实用的理想EBR系统的最佳性能。FAERY由三个关键组件组成：它使用高带宽HBM进行内存带宽密集型语料库扫描，使用数据并行方法进行相似度计算，以及基于管道的方法进行K选择。为了进一步减小硬件资源，FAERY引入了一个过滤器来提前丢弃非Top-K项目。实验表明，在GPU相同显存带宽的情况下，降级后在目标为10ms的延迟目标下，与基于GPU的EBR相比，FAERY仍能达到1.21~12倍延迟低于其以及4.29倍吞吐率高于其的目标。 2. 介绍 2.1 检索与排序 推荐系统已经在许多在线服务中得到广泛采用。为了从包含数百万候选项目的大型语料库中进行推荐，工业大规模推荐系统通常分为两层，即检索和排序。如图所示 （图1) 检索通过简单的算法从庞大的语料库中快速选择数千个相关项目，而排序则利用复杂的算法对检索结果进行更精确的排序，然后从排序的项目中选择数十个。 现实世界的检索系统进行多通道检索：他利用不同通道中的不同策略来检索不同的候选者，然后将其合并和过滤以生成最终的检索结果。在多通道检索策略中，基于嵌入的检索（EBR) 越来越受欢迎。 2.2 EBR检索大致流程 （图2) 具体来说，如图1所示，EBR算法涉及评分，它扫描语料库以获取所有项目并计算每个项目嵌入和给定查询嵌入之间的相似度分数(例如，通过内积），以及 K-selection ,它根据相似度得分返回Top-K项目。返回的 EBR的Top-K项通常被排序，以简化来自多个通道的合并和过滤检索候选。这种EBR系统的性能很重要。一方面，增加每台EBR服务器的吞吐量会降低整体服务器成本，因为只需要更少的服务器来提供目标每秒查询数(QPS)。另一方面，降低每个EBR服务器的延迟会减少检索时间，这既可以缩短用户的整体等待时间，也可以为排名计算留出更多时间以获得更好的推荐结果[11]。因此，延迟猎杀的吞吐量成为EBR系统的关键指标。 2.3 理想架构 为了实现高延迟限制吞吐量，我们对图1中所示的EBR算法进行了表征，并派生了一个实际理想的EBR硬件架构。具体来说，语料库扫描是一种内存密集型操作，需要大的外部内存容量和高内存带宽。相似度计算和K选择都是计算密集型的。它们应该将内存带宽与跨多个算子实例的数据并行架构相匹配。此外，为了与步骤或操作符之间的计算重叠通信，在K-selection内部和整个EBR数据流中都需要管道并行性。然后，我们通过在批量查询之间共享语料库扫描并提供单独的计算管道来扩展理想架构以支持批量查询.因此，理想的架构实现了最佳的查询延迟，并随着批量大小线性扩展延迟限制的吞吐量 2.4 现状 通过将现有的基于CPU和GPU的EBR与理想架构进行比较，我们意识到，不幸的是，由于其固有的架构限制，现有方法都没有实现最佳性能。首先,尽管内存容量很大，但由于内存带宽低，CPU在语料库扫描中表现不佳，并且由于内核数量有限，无法同时很好地支持所需的并行范式。其次，虽然GPU为数据并行提供了更高的内存带宽和海量计算核心，但由于明确的资源边界，GPU并未针对K-selection和整个EBR 数据流所需的管道并行进行优化。 我们观察到FPGA 是一种可编程硬件设备,在一些超大规模云提供商中很容易获得，它具有实际理想EBR架构的所有所需特性。一些现代FPGA配备了大容量高带宽存储器(HBM)，非常适合语料库扫描。此外，FPGA 提供足够的片上存储器和完全可编程的计算元件，以便为各种操作员启用适当的并行范式。 2.5 FAERY的优势 FAERY ，这是一种基于FPGA加速嵌入的检索系统，它是理想EBR架构的体现，并实现了高性能。具体来说，FAERY将语料库存储在FPGA的HBM中，为内存带宽密集型的语料库扫描提供了高带宽。FAERY利用语料库管理器在运行时最大化HBM带宽利用率，同时保留内存高效存储并启用在线语料库更新。FAERY遵循理想架构设计数据并行性的相似性计算和管道并行性的K-selection。与理想架构不同的是，FAERY只需要一个K-selection流水线，并根据在K-selection流水线中观察到的独特属性，在其前面添加一个过滤器以显着降低其吞吐量要求。与理想架构相比，过滤器优化通过消除多个K-selection管道降低了FAERY的资源需求。 上述思路使单个基于FPGA的EBR加速器性能良好。为了进一步增强其功能，可以将多个此类加速器卡插入FAERY服务器并协同工作。当一个语料库可以放入单个卡，我们可以通过在多张卡之间复制语料库来扩展聚合查询吞吐量。当语料库太大而无法放入单张卡片时，我们可以将其平均分片到多张卡片中。FAERY支持复制和分片模式，并利用软件前端来调度查询并合并多个加速卡的检索结果。 实验表明，与Nvidia T4 GPU加速的EBR系统相比，在 10毫秒的延迟目标下，具有相同GPL内存带宽的降级FAERY可实现低1.21 x-12.27倍的延迟和高达4.29 倍的吞吐量。 3. 背景与动机 3.1 EBR算法：KNN与ANN EBR表示带有嵌入的用户查询和候选项目，并将检索问题转换为向量空间中的K-最近邻(KNN)或近似最近邻(ANV)搜索问题。基于KNN的EBR从语料库中搜索准确的k-最近项嵌入，而基于ANN 的EBR通过使用索引等技术牺牲准确性以提高效率来检索近似的最近项嵌入和量化 。 CPU提供有限的内存带宽和计算能力，由于内存访问和计算的巨大成本，CPU在大型语料库上执行KNN搜索具有挑战性。因此，ANN搜索是在业界广泛应用于基于CPU的EBR。相比之下，加速器，例如GPU和FPGA，提供了更高的内存带宽和计算能力，因此这些加速器通常采用KNN搜索来交换内存带宽和计算能力以获得更高的准确性，从而获得更好的推荐质量. 3.2 几乎理想的EBR架构 （图3) 为了最大化延迟限制的吞吐量，理想的架构应该首先实现每个单独查询的最小延迟（相当于批量大小为1的最大吞吐量)，然后随着批量大小的增加线性扩展吞吐量，同时保持一致的最小延迟。 在理论上理想的架构中，对于每个查询，我们使用并行的所有项目嵌入进行相似度计算，并在0(1）时间内完成该运算符，然后进行完美的K选择以匹配并行度。这显然是不切实际的，因为它需要并行进行数百万个项目访问和数百万个相似度计算（例如内积)运算符，更不用说K-selection的设计挑战来匹配这种极端并行性。一个实际理想的EBR 应该考虑到现实的硬件限制和我们在下面讨论的EBR特性。 语料库扫描是一个内存密集型操作符。工业语料库的大小高达几GB，扫描如此大的语料库会导致单个查询的数百万次内存访问。因此，语料库存储和扫描需要大的外部内存和高内存带宽。相似度计算是一个计算密集型运算符，它计算用户之间的相似度分数查询和所有项目嵌入。由于不同项目嵌入的计算是独立的，理想的架构应该执行具有数据并行性的相似性计算，以匹配语料库扫描的吞吐量。 K-selection是另一个计算密集型运算符。为了匹配多个相似度计算实例的吞吐量，K-selection还需要多个实例的数据并行性。在单个实例中，K-selection可以通过多种算法实现，其中一种常见的做法是将这个复杂的任务划分为多个步骤，并以流水线方式组织它们。基于这些特性，实现最佳延迟的实际理想EBR架构应该具有用于语料库存储和扫描的大容量和高带宽内存，EBR操作员的适当并行性以使其吞吐量与内存带宽相匹配，以及通信和通信之间的完美重叠。整个管道中的算子计算，以最大限度地减少延迟。 理想的架构可以扩展到支持批量查询，以线性增加延迟限制的吞吐量，如图3所示。关键是在批量中的多个查询之间共享语料库扫描（即，每批次只扫描一次语料库），并以数据并行的方式使用单独的计算管道处理多个查询。通过这种方式，延迟保持不变，如公式1所示，并且延迟限制的吞吐量随批量查询的数量线性增长。实际上，由于资源限制，批量大小不能无限制地增加，因此最大延迟限制吞吐量将受所选平台的可用硬件资源限制。 3.3 FPGA的机会 我们观察到FPGA具有满足理想EBR架构要求的一下特性 与GPU类似，高端 FPGA配备了大容量的 HBM（通常为8到32 GB)。典型的HBM是由32个并行DRAM通道组成的堆栈（而CPU中多达8个DDR通道)，提供并行内存访问，从而提供高带宽（460 GB/s ) ,从根本上消除了基于CPU的最大内存带宽瓶颈EBR。 GPU为每个SM提供专有的小型片上存储器不同，FPGA 提供了足够的片上存储器（总共数十MB)，所有计算元素都可以访问这些存储器。与仅针对数据并行优化的SIMT内核的GPU不同，FPGA中的海量计算元素和它们之间的互连是完全可编程的，因此它们可以在任何并行策略（数据并行或流水线并行）中进行编排。 （图4) 4. FAERY （图5) 我们按照理想EBR架构的最理想特性设计FAERY加速器，并进行了一些额外的优化。图5展示了FAERY加速器的架构，包括HBM、语料库管理器、相似度计算、过滤器和K选择等几个主要组件。FAERY将语料库存储在HBM中，并使用语料库管理器进行语料库扫描和更新。FAERY跨多个相似性计算单元应用数据并行性，并在K选择中应用管道并行性 。与理想的EBR架构不同，FAERY不需要多个具有数据并行性的K-selection流水线，这要归功于在K-selection流水线之前插入了一个新的过滤器运算符以降低其吞吐量要求。 4.1 语料库管理器 FAERY将语料库存储在HBM中，并使用语料库管理器进行语料库扫描和更新。语料库管理器旨在满足HBM高带宽利用率的两个目标:最大化单通道性能和最大化多通道并行性。 嵌入压缩以最大化单通道性能 水平HBM划分以最大化多通道并行性 4.2 相似度计算 相似度计算同时从多个HBM 通道接收多个项目嵌入。为了匹配HBM 的带宽，我们在相似度计算中应用数据并行性，其中多个评分单元(SU)被实例化以并行工作，并且每个评分单元执行相似度计算，例如，在单独的项目嵌入和给定之间的内积查询嵌入。所需的并行SU数量是HBM 通道总数与通道宽度内的最大项目嵌入数的乘积，由于嵌入压缩，可能包含多个项目嵌入。 4.3 K-选择 4.4 筛选 （图6) K-selection管道在运行的Top-K中维护，它不断更新Top-K的所有过去分数,直到当前点。我们观察到，如果K-selection的输入分数不大于运行Top-K 的最小分数，则输入不会改变内部运行的Top-K，因此可以丢弃。基于这一观察，我们设计了一个过滤器来提前丢弃非Top-K分数，这显着减少了发送到K-selection的分数数量。图6显示了FAERY的吞吐量模型，其中语料库扫描和相似度计算设计了数据并行性以匹配 HBM吞吐量，K-selection仅提供单个管道以节省资源，过滤器弥合吞吐量错误-多通道相似度计算与单通道K-选择之间的匹配 4.5 FAERY数据流的完美重叠 （图7) 如上节所述，所有操作员都在一个 流方式，即所有算子在数据开始流入时立即开始处理，运算符与计算完全重叠。因此，该架构中的数据流表现出完美的 重叠，如图7所示。收到查询后，语料库管理器开始语料库扫描，并从32个HBM 通道中获取嵌入的多流，然后在后续循环中进行相似度计算和过滤流。过滤器操作员提前丢弃大部分分数，以便将单个分数流发送到K-selection。随着分数开始 流到K一selection中，运行的Top-K被更新，并在最后一个分数注入K-selection管道后不久作为最终的Top-K结果输出。 FAERY以与理想架构相同的方式支持批处理。多个查询的数据流从同一点开始。因此，延迟保持不变，并且吞吐量与批量大小成线性关系。 5. FAERY server 图11展示了FAERY服务器架构，其中包括主机CPU上的软件前端和插入服务器PCie插槽的多个FAERY加速器。 多个FAERY 加速器可以协同工作，以两种模式增强单个加速器的能力:复制和分片。在复制模式下，这些加速器存储同一语料库的不同副本，同时服务不同的查询，以提高查询吞吐量。在分片模式下，多个加速器存储同一个语料的不同分片，同时服务同一个查询，以增加支持的语料大小。运行在主机CPU中的前端负责通过调度器模块进行查询调度和通过合并模块在上述两种模式中合并结果。 当服务器收到新的语料库时，软件前端的管理器模块会处理这个更新请求。它根据上面列出的工作模式确定是否需要进行语料复制或分片，然后将语料副本（或分片)通过PCie发送到相应的加速器。 6. 评估 我们评估FAERY实现的性能，并将其分别与基于CPU和GPU的EBR进行比较。实验表明： FAERY 接近最优查询延迟，并实现 延迟分别比基于CPU和 GPU的EBR低98.09x-118.99x和1.85x-18.81x。与GPU相同显存带宽的降级FAERY仍能达到l。延迟比基于GPU的EBR低2l x -12.27x。 在数以十万计的延迟限制(:s lo ms)吞吐量中，FAERY和降级的FAERY比基于GPU的EBR高1倍。分别为33x -6.58x 和0.87 x -4.29x ，而基于CPU的EBR未能满足10 ms延迟目标。 FAERY达到 l。与基于GPU的EBR相比，能效提高66 x -8.20 x，成本效率提高1.31 x -6.46x。 带有两个加速器的 FAERY服务器在复制模式下提供⒉倍的查询吞吐量，在分片模式下提供2倍的语料库容量，延迟增加不到1.1%。 7. 结论 FAERY是用于基于嵌入的检索(EBR)的领域特定加速器(DSA)。FAERY 的组件:语料库扫描、相似性计算和K选择是使用理想EBR架构所需的适当并行技术安排的。因此，FAERY 没有现有基于CPU和GPU的EBR方法的缺点和性能损失。与基于CPU的EBR 相比，FAERY不仅提供了低延迟和高吞吐量，而且在延迟限制吞吐量方面优于基于GPL的EBR。