代理

代理对象通过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次方机器特性

阅读全文 »

注册中心

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

典型场景

阅读全文 »

单线程网络IO、KV读写

Redis的网络IO和KeyValue读写是由一个线程来完成的。
而Redis的持久化、异步删除、集群数据同步是额外的线程执行。

也由于Redis是单线程的,所以要特别小心耗时的操作,这些操作会阻塞后续指令。

简单来说就是处理事务一套、前台接待一套。不会因为前面办事导致人均等待时间太久。

Redis使用IO多路复用(epoll),将连接信息、事件放到队列中,使其能够处理并发的客户端连接。

socket: {
s0
s1
s2
s3
"..."
}

IO多路复用: {
s3 -> s2 -> s1 -> s0
}

事件处理器: {
连接处理器
命令请求处理器
命令回复处理器
}

socket -> IO多路复用 -> 文件事件分派器 -> 事件处理器

详解GET key

Redis相当于HashMap,也由于Hash是无序的,因此scan这样的流式查询,在查改场景中,可能会漏扫中途插入到前面下标的元素。

Redis持久化

RDB Snapshot

默认情况下,Redis将内存数据快照保存为dump.rdb,可以使用

save <time_duration> <row_insertion>

指示Redis多少秒内插入多少条数据后持久化到数据库
也可以直接用savebgsave命令写入数据库

bgsave 异步持久化

bgsave使用写时复制COW。bgsave从主线程fork出来,当主线程修改数据时,bgsave线程会将写入数据拷贝一份,然后写入rdb

Append-Only File

快照不能做到完全持久,假如服务宕机,可能会丢失几条写入。
这时候我们直接做个命令日志AOF,将执行的修改指令写入appendonly.aof

appendonly yes
appendfilename "appendonly.aof"

aof有三种模式appendfsync

  • always:立刻写入磁盘
  • everysec:每秒写一次
  • no:交给OS调度
    但是,由于aof是记录命令,需要执行时间,对于持久化大量数据比较耗时间。
    对于连续操作(如自增)aof会优化为1条命令,可以用bgrewriteaof命令手动重写
# 最小重构大小
auto-aof-rewrite-min-size 64mb
# 增长了100%,即128mb就重构
auto-aof-rewrite-percentage 100

Redis4 混合持久化

由于Redis重启时优先使用aof恢复数据,rdb利用率不高。因此出现了混合持久化

# 必须同时开启aof
aof-use-rdb-preamle yes
# 可以直接把快照关掉,因为混合持久化都写在aof里面

开启后,当aof重写时,会直接写入rdb,将rdb快照和aof增量存储在一起。
于是Redis重启可以先读rdb,再执行增量aof恢复数据,提高效率。

Redis主从

# redis-<your_port>.conf
pidfile /var/run/redis_<your_port>.pid
logfile "<your_port>.log"
# 数据存放目录
dir /usr/local/redis/data/<your_port>

### 主从复制
replicaof <main_redis_ip> <port>
# 从节点,只读
replica-read-only yes


### 启动
# 启动从节点
redis-server redis-<your_port>.conf
# 连接到从节点
redis-cli -p <minor_redis_port>

主从原理

master: {
rdb data
repl buffer
}
slave

slave -> master: 1. psync全量复制同步数据(通过socket长连接)
master.rdb data -> master.rdb data: 2.1 收到psync命令,执行bgsave生成最新rdb快照
master.repl buffer -> master.repl buffer: 2.2 主节点将增量写语句更新到buffer
master.rdb data -> slave: 3. 发送rdb数据
slave -> slave: 4. 清空旧数据,加载主节点rdb
master.repl buffer -> slave: 5. 发送缓冲区写命令
slave -> slave: 6. 执行主节点buffer写命令
master -> slave: 7. 主节点通过socket长连接,持续发送写命令给从节点,保持数据一致

断点续传

master: {
repl backlog buffer
}
slave

slave -> master: 1. 连接断开
master.repl backlog buffer -> master.repl backlog buffer: 2. 主节点增量写命令写入buffer
slave -> master: 3. 恢复socket长连接
slave -> master: 4. psync(offset)带偏移量
master -> slave: 5. 若offset在buffer中,断点以后的数据发送给从节点;否则,全量发送
master -> slave: 6. 持续发送buffer写命令,保持数据一致

如果存在很多从节点,那么主节点传输压力会比较大。可以采用树型架构,让从节点再给它的子节点传输数据。

哨兵高可用

sentinel_cluster: {
sentinel1 <-> sentinel2 <-> sentinel3 <-> sentinel1
}

client -> master <-> sentinel_cluster
master -> slave1
master -> slave2
client -> sentinel_cluster
sentinel_cluster <-> slave1
sentinel_cluster <-> slave2

哨兵会动态监听redis主节点,如果主节点挂了,哨兵会选择一个新redis示例作为主节点(通知给client端)

开启哨兵

# sentinel.conf

port 26379
pidfile <your_file>
logfile <your_file>
dir "<your_dir>"

# quorm是指多少个sentinel同时认为主节点挂了,才让master失效,一般设置为一半以上
sentinel monitor mymaster <redis_ip> <redis_port> <quorm>

启动哨兵./redis-sentinel sentinel.conf

Redis Cluster

当哨兵集群选举新节点的时候,服务会宕机几秒钟。因此我们需要Cluster

client1 -> RedisCluster
client2 -> RedisCluster
RedisCluster: Hash slot: CRC16(key) % 16384
RedisCluster -> Redis集群
Redis集群: {
master1 -> slave1-1
master1 -> slave1-2

master2 -> slave2-1
master2 -> slave2-2

master3 -> slave3-1
master3 -> slave3-2
}

在Cluster中,每个master数据是不重叠的,数据会被分片储存。通过Hash算法来决定存储数据到哪一个master节点。
使用Cluster,可以避免Redis服务完全宕机。
2的幂次取模小技巧:

Xmod2n=X & (2n1)X \mod 2^n = X \text{ \& } (2^n - 1)

Redis集群搭建

redis-cluster/
|-- 8000
| `-- redis.conf
|-- 8010
`-- 8020
  1. Redis配置
# ...其他配置

daemonize yes
port 8000
dir /path/to/redis-cluster/8000/
# 启用集群
cluster-enabled yes
cluster-config-file nodes-8000.conf
cluster-node-timeout 5000
# 密码
requirepass <your_password>
masterauth <your_auth_password>
  1. 启动所有master和slave节点
redis-server /path/to/redis-cluster/80*/redis.conf
ps aux | grep redis
  1. 开启集群
# replicas表示节点的副本,配置为1,则1主1从
redis-cli -a <your_auth_password> --cluster create --cluster-replicas 1 \
localhost:8000 localhost:8001 localhost:8002 ...

注意,第二次启动集群后,就不需要这一步了。节点会自动读取nodes-8000.conf文件,恢复上次集群状态。

  1. 进入redis节点验证配置
cluster info
cluster nodes

Redission原理

Thread1: {
Redission
}
Thread2: {
Redission
}

Thread1.Redission -> Try Lock
Try Lock -> 守护线程: 加锁成功
守护线程 -> Redis(Master): lock,每隔10s检查线程是否仍持有锁。如果持有,则延长锁失效时间

Thread2.Redission -> Try Lock
Try Lock -> Thread2.Redission: 加锁失败,使用while自旋尝试加锁

Redission利用了Redis Lua脚本保证原子操作。

程序如何装载

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程序运行结束
阅读全文 »

基本配置

  1. sudo权限: 修改/etc/sudoers,或者rootadduser <username> sudo并重启
  2. apt软件包管理系统换源:/etc/apt/sources.list修改软件发布源
    deb http://站点/目录名/stretch版本名 main contrib non-free三类软件包
    Debian官方软件源:官网/mirror/list
  3. /usr/share/doc 有安装软件的信息

1.0 命令篇

基本命令

参考资料 https://missing-semester-cn.github.io/2020/course-shell/
  • 关机、重启
    shutdown
    -h now halt,挂起,相当于 halt
    -r now reboot,重启
    poweroff
    reboot

  • 手册
    man 命令

  • 导航
    pwd 显示当前所在目录
    cd 进入文件夹 '..' 上级目录 '.'当前目录 '/'开头的是绝对路径`

  • 查看文件
    ls 列出所有文件
    ls -l 查看文件权限信息

  • 创建文件夹
    mkdir 文件夹名
    rmdir 删除文件夹

  • 没有vim的时候如何创建、编辑、查看文件
    touch 文件 创建文件
    echo 文本 > 文件 echo+重定向输入文件(会把原来内容覆盖)
    echo 文本 >> 文件 追加输入(在原来内容的结尾另起一行输入)
    cat 文件 查看文件
    除了使用cat看文件,还有tac(从最后一行开始显示),more, less(可以翻页,好用)

  • 压缩
    压缩一整个目录,使用 tar
    压缩单个文件 bzip2 gzip(-d解压)
    tar -cvf 目标名 文件名 压缩 , tar -xvf解压

  • 查找
    grep 用法 grep "word" filename

grep "string" * # 在所有文件中搜索string
grep -r "string" # 递归搜索
`find`用法 `find filename`
阅读全文 »

ssh登录

ssh <user_name>@<remote_ip> -p <remote_port> -i <your_key>

ssh端口映射

可以用于不保留端口的情况下,远程连接数据库等。

ssh -N -L <local_port>:localhost:<remote_port> <user_name>@<remote_ip> -p <remote_port> -i <your_key> 

脚本批量映射

需要注意,Nacos有gRPC,除了8848端口外,9848端口也要一起开放。

# Port Mapping
PORTS=(
"ulocalport:localhost:uremoteport"
# MySQL
"53306:localhost:3306"
# Nacos
"58848:localhost:8848"
"59848:localhost:9848"
# Redis
"56379:localhost:6379"
# RocketMQ namesrv
"59876:localhost:9876"
# RocketMQ broker
"510911:localhost:10911"
)

ARGS=()
for port in "${PORTS[@]}"; do
ARGS+=(-L "$port")
done

ssh -o ServerAliveInterval=60 -N "${ARGS[@]}" <username>@<remote_ip>

密钥登录

  1. 首先在本地生成一份密钥,然后将公钥上传到remote的~/.ssh/authorized_keys
  2. 修改remote/etc/ssh/sshd_config
# 新端口
Port 22
# 启用密钥认证
PubkeyAuthentication yes
# 禁用密码登录
PasswordAuthentication no
# 允许Root登录但禁止密码验证
PermitRootLogin prohibit-password
  1. 重启ssh
# Ubuntu/Debian
sudo systemctl restart ssh

# CentOS/RHEL
sudo systemctl restart sshd

文本格式转换

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文件每行后都要空行

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

参考教程: https://www.ruanyifeng.com/blog/2019/10/tmux.html
  • tmux使会话与窗口解绑,一个窗口source .bashrc更新了,另一个窗口可能没有
  • ctrl+b 前缀键
    % 分成左右两栏
    " 分成上下两栏
    up 选择上边的窗口
    [ 查看历史记录

窗口

  • <C-d> 删除窗口
  • <C-b> z 最大化/最小化一个窗口
  • <C-b> c 创建一个新的窗口,使用 <C-d>关闭
  • <C-b> N 跳转到第 N 个窗口,注意每个窗口都是有编号的
  • <C-b> p 切换到前一个窗口
  • <C-b> n 切换到下一个窗口
  • <C-b> w 列出当前所有窗口

Tmux-Path 双向绑定

使用tmux会存在一个问题:在1个窗口设置了环境变量,在其他窗口并不会生效。
下面的脚本会帮助我们解决问题,它将tmux白名单内的全局变量值自动同步到Shell环境变量。

阅读全文 »
0%