MCP

RAG的局限性

对于AI来说,RAG仅仅是外部知识库,AI只起到一个总结效果。而总结的效果取决于向量相似度匹配,可能遗漏关键信息。

direction: right
结构化数据 -> 文本块
非结构化数据 -> 文本块

文本块 -> 向量数据库 -> 检索文本块 -> 生成最终响应
  • 生成内容不完整:RAG处理的是文本的切片,因此无法看到整篇文档信息。
  • RAG无法判断需要多少切片才能解决问题。
  • 多轮检索能力弱。

MCP基础

Function Calling

Coze的Agent就是基于Function Calling思路封装的。

不过Function Calling成本比较高,需要模型经过专门训练微调才能稳定支持。

这导致有些模型不支持某些插件的调用(例如Trae只有选择Sonnet、GPT等模型才可以处理图片)。

另外,Function Calling不是一项标准,许多模型的实现细节不一样。

Model Context Protocol

MCP是一项标准协议,简单来说就是通用的接口,使AI-外部工具/数据源交互标准化、可复用。

Claude Desktop、Cursor这样的工具在内部实现MCP Client,这个Client通过MCP协议与MCP Server(由服务提供公司自己开发,实现访问数据、浏览器、本地文件等功能,最终通过MCP返回标准格式)交互,最终在MCP Host上展示。

MCP 传输方式

STDIO,本地环境
SSE,并发量不高,单向通信
Streamable HTTP,高并发,需要维护长连接

指标 Function Calling Model Context Portocol
协议 私有协议 开放协议
场景 单次函数调用 多工具协同 + 数据交互
接入方式 函数直接接入 需要MCP Server + MCP Client
耦合度 工具与模型绑定 工具开发与Agent开发解耦
调用方式 API Stdio/SSE
阅读全文 »

文本格式转换

pandoc --from markdown --to docx source.md -o dest.docx
pandoc -f markdown source.md -t docx -o dest.docx
pandoc source.md -o dest.docx --ignore-args # 忽略参数

注意:为了最佳转换效果,markdown文件每行后都要空行

模板

pandoc --reference-doc template.docx source.md -o dest.docx

md2epub

# 首先把所有的md文件列出来
## 递归查找所有 .md 文件(排除 README.md 和 SUMMARY.md)
find . -name "*.md" ! -name "README.md" ! -name "SUMMARY.md" | sort > filelist.txt
## 然后编辑 `filelist.txt`,确保文件顺序正确(例如按 `SUMMARY.md` 的目录结构排序)。

pandoc --standalone --toc \
--metadata title="MIT6.824 分布式系统" \
--metadata author="Robert Morris" \
-o output.epub $(cat filelist.txt)

注意:对于gitbook,pandoc可能不能正确处理路径,推荐使用honkit。

honkit

// book.json
{
"title": "MIT6.824 分布式系统",
"author": "Robert Morris",
"plugins": ["hints"],
"pluginsConfig": {
"hints": {
"info": "fa fa-info-circle",
"warning": "fa fa-exclamation-triangle"
}
}
}
# 安装honkit
npm install honkit --save-dev
# 需要calibre转换
ebook-convert --version

npm init -y
npx honkit epub ./ ./mybook.epub

代理

代理对象通过invoke,实现类与非核心功能的解耦。

public static void main(String[] args) {
Payment payment = new Payment("AliPay");
Pay proxy = ProxyUtil.createProxy(payment);

proxy.pay(amount);
}

class Payment implements Pay {

@Overide
public payResp pay(BigDecimal payAmount) {
// Payment Business...
}
}

interface Pay {

abstract payResp pay(BigDecimal payAmount);
}

class ProxyUtils {
public static Pay createProxy(Payment payment) {
return (Pay) Proxy.newProxyInstance(
ProxyUtil.class.getClassLoader(),
new Class[]{ Pay.class },
new InvocationHandler() {

@Overide
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Payment Environment Init...
// Payment Safe Guard...
return method.invoke(payment, args);
}
}
);
}
}

反射

反射允许对成员变量、成员方法、构造方法信息进行编程访问。

例如,IDE的智能补全、参数提示,就是使用反射实现。

Class字节码获取

Class clazz;
clazz = Class.forName("com.yourpackage.TargetClass");
// 参数
clazz = TargetClass.class;
// 有实例时
clazz = instance.getClass();

场景

反射可以与配置文件结合,从而动态地创建对象。例如application.yml里数据库的配置、端口号等。

Properties prop = new Properties();
FileInputStream fis = new FileInputStream(ROOT + "src/main/resources/application.properties");
String dataSourceUrl = (String) perp.get("dataSource");
String dbName = (String) extractDbName(dataSourceUrl);

Class clazz = Class.forName(dbName);
Constructor con = clazz.getDeclaredConstructor();
Object o = con.newInstance();

...

HashMap

jdk1.7

jdk1.7的HashMap数据结构是:数组 + 单向链表

当哈希后,得到的数组槽位已经存放了其他元素,这时候就需要运用指针在同一个槽位存放多个元素。

头插法

jdk1.7使用的方法是头插法,这样就不需要遍历到链表尾部再插入,性能高。

void createEntry(int hash, K key, V value, int bucketIdx) {
Entry<K, V> e = table[bucketIdx];
// 这里使用头插法,在槽位头部插入新元素,并指向e,成为新的槽位引用
table[bucketIdx] = new Entry<>(hash, key, value, e);
size++;
}

public Entry(int h, K k, V v, Entry<K, V> n) {
this.value = v;
this.next = n;
this.key = k;
this.hash = h;
}

这种方法需要最终更新槽位指向新插入的节点,否则单向链表找不到新插入的元素。

利用2次方机器特性

阅读全文 »

Mysql体系结构

DB与Instance

DB:数据库可以是ibd文件、放在内存的文件,是物理操作系统文件或其他形式文件类型的集合。
Instance:Mysql数据库由后台线程及一个共享内存区组成。

数据库实例才是真正操作数据库文件的。在集群情况下,可能存在一个DB被多个Instance使用的情况。

Mysql被设计为单进程多线程,在OS上的表现是一个进程。

插件式表存储引擎

存储引擎基于表,而不是DB。

存储引擎对开发人员透明。

索引原理

MySQL使用的是B+树作为索引的数据结构

B树是一个分支内按顺序存放多个节点数据的数据结构;而B+树在此基础上,在分支内只存储索引,只在叶子节点存储数据(这样每一层可以存储更多索引,减少层数),并且在叶节点之间用指针互相连接,提高访问效率。

索引分类

数据结构

  • B+树索引
  • 哈希索引
  • 红黑树索引

功能

  • 主键索引
  • 唯一索引
  • 普通索引(一般我们为优化sql建立的索引)
  • 全文索引
  • 联合索引

存储方式

  • 聚簇索引
  • 非聚簇索引(索引与数据分开,需要回表)
阅读全文 »

事务

原子性 Atomicity

BUSINESS
sql语句1
sql语句2
COMMIT

原子性:事务操作要么同时发生,要么同时失败,不存在中间情况

通过Undo Log回滚实现

一致性 Consistency

账户500元 -> 扣除1000元 -> 账户-500元
-- 非法操作

一致性:每个操作都必须是合法的,账户信息应该从一个有效状态到另一个有效状态。

隔离性 Isolation

商户1转账500元 -> 余额更新为500元
商户2转账500元 -> 余额更新为500元
-- 没有隔离性

隔离性:两个操作对同一个账户并发操作时,应该表现为不相互影响类似串行的操作。

持久性 Durability

转账500元到余额 --服务器宕机--> 余额0元

持久性:操作更新成功后,更新的结果应该永久地保留下来,不会因为宕机等问题而丢失。

阅读全文 »

Spring核心思想

Spring的核心是为Class创建代理对象实现一些AOP切面操作,从而支持方便的注解、事务、自动注入等功能。
为了创建代理对象,需要将对象创建移交给Spring完成,因此需要IoC容器。

IoC

Inversion of Control
Spring通过控制反转,将对象创建交给IoC容器完成。
IoC容器实际上就是一个工厂,通过读取xml配置文件,使用反射创建对象。

<bean id="userDao" class="com.site.UserDao"></bean>
class UserFactory {
public static UserDao getDao() {
String classValue = context.getProperty("userDao");
Class clazz = Class.forName(classValue);
}
}

当我们的Dao文件路径改变时,只需要修改xml配置一处即可完成全部修改。
如果只用工厂模式,那需要导入很多包,也不直观。因此使用xml与反射,将工厂方法与配置解耦。

  • BeanFactory:IoC容器基本使用,Spring内部使用
    对象懒创建
  • ApplicationContext:BeanFactory子接口,暴露给开发者使用
    加载配置就会创建对象

Bean生命周期

类class -> 无参构造方法 -> 普通对象 -> 依赖注入 -> "@PostConstruct" -> 初始化 -> AOP -> 代理对象 -> Bean

Spring Framework

AnnotationConfigApplicationContext context = 
new AnnotationConfigApplicationContext(AppConfig.class);
// resource/application.xml
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("application.xml")

UserService userService = (UserService) context.getBean("userService");

Dependency Injection

Spring首先是调用对象自身的构造方法创建对象,然后通过依赖注入(@Autowired属性赋值)来得到Bean

for (Field field: userService.getClass().getFields()) {
if (field.isAnnotationPresent(Autowired.class)) {
field.set(userService, value);
}
}

PostConstruct

这个注解可以让Spring在初始化时调用此方法,从而实现一些初始化操作(如从数据库查询信息映射到实体类)。

@PostContruct
public init() {
...
}

for (Method method: userService.getClass().getMethods()) {
if (method.isAnnotationPresent(PostConstruct.class)) {
method.invoke(userService, null);
}
}

AOP

AOP后,得到一个代理对象,然后Spring会在代理对象内部增加一个属性UserService target,并将经过依赖注入的普通对象赋值给target,然后调用target.method(),从而保留对象的所有Field的同时,可以通过代理在切面上做一些额外操作。

注册中心

Eureka能够自动注册并发现微服务,然后对服务的状态、信息进行集中管理。当我们需要获取其他服务的信息时,只需要向Eureka进行查询。

a: 微服务1
b: 微服务2
c: 微服务3
E: Eureka注册中心

a -> E: 注册
b -> E: 注册
c -> E: 注册

依赖

父项目

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2024.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>

Eureka模块

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>

配置

eureka:
fetch-registry: false
registry-with-eureka: false
service-url:
defaultZone: http://yourhost:port/eureka
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}

注册服务

首先在需要注册的微服务下导入Eureka依赖:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

然后修改配置appllication.yml

spring:
application:
name: yourservice
eureka:
client:
# 跟上面一样,需要指向Eureka服务端地址,这样才能进行注册
service-url:
defaultZone: http://yourhost:port/eureka

服务发现

注册RestTemplate

@Configuration
public class BeanConfiguration {

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

使用spring-application-name代替URL

@Override
public UserBorrowDetail getUserBorrowDetailByUid(int uid) {
List<Borrow> borrow = borrowMapper.getBorrowByUid(uid);
RestTemplate restTemplate = new RestTemplate();
User user = restTemplate.getForObject("http://userservice/user/"+uid, User.class);
List<Book> bookList = borrow
.stream()
.map(b -> restTemplate.getForObject("http://bookservice/book/"+b.getBid(), Book.class))
.collect(Collectors.toList());
return new UserBorrowDetail(user, bookList);
}

负载均衡

同一个服务可以注册多个端口,Eureka会为同一服务的多个端口分别进行注册。
使用上面的代码,Eureka会自动地均衡分发请求到不同端口上。

负载均衡保证了服务的安全性,只要不是所有端口的微服务都宕机,Eureka就能够分配请求到可用的端口。

Eureka高可用集群

E: Eureka高可用集群
E: {
E1: Eureka服务器1
E2: Eureka服务器2
E3: Eureka服务器3

E1 -> E2
E2 -> E3
E3 -> E1
}

a: 微服务1
b: 微服务2
c: 微服务3
a -> E: 注册
b -> E: 注册
c -> E: 注册

编写多个application.yml

# application-01.yml
server:
port: 8801
spring:
application:
name: eurekaserver
eureka:
instance:
# 由于不支持多个localhost的Eureka服务器,但是又只有本地测试环境,所以就只能自定义主机名称了
# 主机名称改为eureka01
hostname: eureka01
client:
fetch-registry: false
# 去掉register-with-eureka选项,让Eureka服务器自己注册到其他Eureka服务器,这样才能相互启用
service-url:
# 注意这里填写其他Eureka服务器的地址,不用写自己的
defaultZone: http://eureka01:8802/eureka

# application-02.yml
server:
port: 8802
spring:
application:
name: eurekaserver
eureka:
instance:
hostname: eureka02
client:
fetch-registry: false
service-url:
defaultZone: http://eureka01:8801/eureka

微服务写入所有Eureka服务器的地址

eureka:
client:
service-url:
# 将两个Eureka的地址都加入,这样就算有一个Eureka挂掉,也能完成注册
defaultZone: http://localhost:8801/eureka, http://localhost:8802/eureka

LoadBalance 随机分配

默认的LoadBalance是轮询模式,想修改为随机分配,需要修改LoadBalancerConfig(注意,不需要@Configuration注解)并在BeanConfiguration中启用

public class LoadBalancerConfig {
//将官方提供的 RandomLoadBalancer 注册为Bean
@Bean
public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory){
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}

@Configuration
@LoadBalancerClient(value = "userservice",
configuration = LoadBalancerConfig.class)
public class BeanConfiguration {

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

OpenFeign 更方便的HTTP客户端请求工具

OpenFeign和RestTemplate有一样的功能,但是使用起来更加方便

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

使用方法与Mybatis非常类似。

  1. 首先,启用OpenFeign
@SpringBootApplication
@EnableFeignClients
public class SomeApplication {
public static void main(String[] args) {
SpringApplication.run(SomeApplication.class, args);
}
}
  1. 接下来注册一个interface
@FeignClient("userservice")   // 声明为userservice服务的HTTP请求客户端
public interface UserClient {

// 路径保证和UserService微服务提供的一致即可
@RequestMapping("/user/{uid}")
User getUserById(@PathVariable("uid") int uid); // 参数和返回值也保持一致
}
  1. 直接注入使用
@Resource
UserClient userClient;

@Override
public UserBorrowDetail getUserBorrowDetailByUid(int uid) {

// RestTemplate方法
RestTemplate template = new RestTemplate();
User user = template.getForObject("http://userservice/user/"+uid, User.class);
// OpenFeign方法,更直观的方法调用
User user = userClient.getUserById(uid);

}

请求携带token

@Configuration
public class FeignConfig implements RequestInterceptor {

@Override
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
requestTemplate.header("token", request.getHeader("token"));
}
}

@FeignClient(value = "SomeService", configuration = FeignConfig.class)
public interface SomeService {

新增配置

sudo vim /etc/nginx/sites-available/yourdomain.conf
# 符号链接
sudo ln -s /etc/nginx/sites-available/yourdomain.conf /etc/nginx/sites-enabled/

port2domain

后端服务

server {
listen 80;
server_name yourdomain.com www.yourdomain.com;

location / {
proxy_pass http://127.0.0.1:9000; # 项目运行的端口
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

# 日志配置(可选)
error_log /var/log/nginx/yourdomain.error.log;
access_log /var/log/nginx/yourdomain.access.log;
}

启用

sudo nginx -t  # 检查配置是否正确
sudo nginx -s reload

前端打包文件

location / {
root /var/www/railcloud/dist;
index index.html;
try_files $uri $uri/ /index.html; # 支持SPA路由
}

启用

# ③ 设置正确权限
sudo chown -R www-data:www-data /var/www/railcloud
sudo chmod -R 755 /var/www/railcloud

sudo nginx -t && nginx -s reload

分布式锁

Redission

使用原生Redis设置锁的问题:

  1. 服务器拿到锁后宕机,锁不能释放,导致阻塞。
    设置锁失效时间可以解决上面的问题,但是会导致新的问题:
  2. 设置锁失效时间,在服务器负载过高的时候,会发生锁失效业务还没完成的情况,导致业务代码不互斥。

0信任:不要期待网络服务器按照理想情况运行。

使用Redission自动为锁续命,可以解决上述问题。

String lockKey = req.getBusinessUniqueKey() + "-" + req.getBusinessCode();
RLock lock = null;
try {
lock = redissonClient.getLock(lockKey);
boolean tryLock = lock.tryLock(0, TimeUnit.SECONDS);
if (!tryLock) {
LOG.info("获取锁失败");
throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
}

// Business Code

} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
LOG.info("释放锁");
if (lock != null && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}

锁设计原则:谁持有锁,谁才有资格释放锁。

不过,使用这个方案有问题:Redis Master加锁后宕机,新的Master没有同步到加锁的数据,就会存在多把锁。
这时我们需要引入ZooKeeper保持强一致性;或者可以使用RedLock,即集群半数以上机器加锁成功,才是真的加锁成功。
然而,这两种方法都会带来性能开销。

对于难以解决的棘手问题,应该思考如何避免问题发生。

高性能分布式锁:分段锁

针对不同段进行加锁,这样就能允许多把锁存在,从而获得一定并发性能。

基本操作

General

# 返回给定模式的keys
KEYS patter
KEYS * # 返回全部
KEYS set* # 返回set开头的keys
EXISTS key
TYPE key
DEL key

String

SET key value
GET key
# Set Extend Time
SETEX key seconds value
# Set When Key Not Exist
SETNX key value

Hash

HSET key field value
HGET key field
HDEL key field
# Get All Fields
HKEYS key
# Get All Values
HVALS key
flowchart LR
key[key]
item[
field1: value1
field2: value2
]
key --> item

List

LPUSH key value1 value2
# Get Key From Start To Stop
LRANGE key start stop
# Right POP
RPOP key
# List Length
LLEN key

典型场景

阅读全文 »

程序如何装载

Main\.java, Minor\.java -> jar包.java Main\.main(): 编译打包
jar包.java Main\.main() -> 验证: 加载
jar包.java Main\.main() -> Minor\.class: 使用
Minor\.class -> JVM: 加载
验证 -> 准备 -> 解析 -> 初始化 -> JVM

加载:从磁盘加载到内存。(懒加载,用到类才加载,如main方法或new对象)
验证:验证字节码是否正确、是否可识别。
准备:初始化静态(static,不包括常量)变量、赋初值(默认值)。
解析:符号引用 -> 直接引用。静态方法(如main) -> 指向数据所在内存的指针。这是静态链接,在类加载期间完成;而动态链接在程序运行期间完成。
初始化:为静态变量赋值,执行静态代码块。

类加载器

加载过程由类加载器实现,有几种类加载器:

  1. 引导类加载器(C++):JRE核心lib的jar类包
  2. 扩展类加载器:JRE拓展lib(ext)jar类包
  3. 应用程序类加载器:ClassPath路径下的类包(自己编写的类)
  4. 其他加载器:加载自定义路径下的类包
java com\.site\.jvm\.Math\.class -> java\.exe调用底层jvm\.dll创建Java虚拟机 -> 创建引导类加载器实例
创建引导类加载器实例 -> sum\.misc\.Launcher\.getLauncher(): C++调用Java代码,创建JVM启动器实例,这个实例负责创建其他类加载器
sum\.misc\.Launcher\.getLauncher() -> launcher\.getClassLoader(): 获取运行类自己的加载器ClassLoader(AppClassLoader实例)
launcher\.getClassLoader() -> classLoader\.loadClass("com\.site\.jvm\.Math"):调用loadClass加载即将要运行的类
classLoader\.loadClass("com\.site\.jvm\.Math") -> Math\.main(): 加载完成后,JVM执行Math.main()
创建引导类加载器实例 -> Math\.main(): C++发起调用
Math\.main()-> JVM销毁: Java程序运行结束
阅读全文 »
0%