开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

被人问到Redis有啥优缺点和它的持久化机制,别懵,看这儿~

发表于 2021-11-12

「这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战」

Redis有哪些优缺点?

  • 优点:
+ 性能极高
+ 支持数据的持久化,对数据的更新采用Copy-on-write技术(写拷贝),可以异步的保存在磁盘上
+ 丰富的数据类型
+ 原子性:多个操作通过MULTI和EXEC指令支持事务
+ 丰富的特性:key过期
+ 支持数据的备份,快速的主从复制
+ 节点集群
  • 缺点:
+ 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写
+ 适合的场景主要局限在较小数据量的高性能操作和运算上

Redis使用单线程模型为什么性能依然很好?

  • 避免了线程切换的资源消耗
  • 单线程不存在资源共享与竞争,不用考虑锁的问题
  • 基于内存的,内存的读写速度非常快
  • 使用非阻塞的IO多路复用机制
  • 数据存储进行了压缩优化
  • 使用了高性能数据结构,如hash、跳表等

Redis持久化机制有哪些?各有什么优缺点?

RDB(Redis DataBase)

定义:数据集快照的方式半持久化模式,记录数据库的所有键值对,在某个时间点将数据写入临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,可恢复数据。

RDB工作简单概述:

Redis会单独创建 ( fork )一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的。这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。我们默认的就是RDB,一般情况下不需要修改这个配置!

rdb保存的文件是dump.rdb

  • 优点:
    • 恢复操作简单,容灾性好
    • 性能高,fork子进程进行写操作,主进程继续处理命令
    • 大数据集比AOF的恢复率高
  • 缺点:
    • 数据安全性低,RDB是间隔一段时间进行持久化,若期间redis发生故障,可能发生数据丢失。
    • fork进程的时候,会占用一定的内存空间。

触发机制

  • save的规则满足的情况下,会自动触发rdb规则
  • 执行flushdb命令,也会触发我们的rdb规则
  • 退出redis,也会产生rdb文件!
+ 备份就自动生成一个dump.rdb

如何恢复rdb文件

  • 只需要将rdb文件放在我们redis启动目录同级下就可以,redis启动的时候会自动检查dump.rdb恢复其中的数据!

AOF(Append Only File)

定义:所有的命令记录以redis命令请求协议的格式完全持久化存储,保存为aof文件,恢复的时候就把这个文件全部在执行一遍!

  • 工作流程概述:

以日志的形式来记录每个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件, redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

aof保存的是appendonly.aof文件,默认是不开启的,我们需要手动进行配置!

  • 优点:
    • 数据安全
    • 三种同步策略:
      • 每次修改便会同步,保证文件完整性较好。
      • 每秒都进行一次同步,但是可能丢失这一秒的数据。
      • 从不进行同步,效率最高。
  • 缺点:
    • AOF的持久化文件比RDB大,恢复速度慢
    • aof文件运行也要比rdb慢

Redis过期键的删除策略有哪些?

  • 定时删除:在设置键的同时,创建一个定时器,让定时器在过期时间到达后来执行删除键的操作。
  • 惰性删除:放任过期键不管,每次从键空间中获取键值时,检查键是否过期,如果过期则删除,如果没过期则返回值。
  • 定期删除:每隔一段时间,程序对数据库进行检查,删除里面过期的键。

为什么Redis所有数据放到内存中?

  • 为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘,即具有高速读写和数据持久化的特征。
  • 如果程序直接与磁盘交互,磁盘IO速度会严重影响Redis的性能
  • 内存的硬件成本降低,使得Redis更受欢迎。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

ThreadLocal

发表于 2021-11-12
  1. Threadlocal 简介

ThreadLocal为每个线程访问的变量提供了一个独立的副本,线程在访问这个变量时,访问的都是自己的副本数据,从而线程安全,即ThreadLocal为变量提供了线程隔离。

  1. 线程是如何隔离的

每一个Thread维护了一个threadLocals变量,这是一个ThreadLocalMap类。

1
2
3
4
5
6
7
java复制代码public class Thread implements Runnable {
... ...
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
... ...
}

ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解,当前threadLocal对象作为key,变量值作为value),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。
ThreadLocal类通过操作每个线程独有的ThreadLocalMap副本,实现了变量访问的线程隔离。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class ThreadLocal<T> {
... ...
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}

Set 方法

1
2
3
4
5
6
7
8
9
java复制代码// 我们保存值的时候,是保存在当前Thread自己的ThreadLocalMap中,属于自己的独立副本,别人无法访问
public void set(T value) {
Thread t = Thread.currentThread(); // 获取当前线程
ThreadLocalMap map = getMap(t); // 获取当前线程的ThreadLocalMap(每个线程拥有自己独立的副本)
if (map != null)
map.set(this, value); // this,即threadLocal作为Key
else
createMap(t, value); // 没有则创建
}

Get 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public T get() {
Thread t = Thread.currentThread(); // 获取当前线程
ThreadLocalMap map = getMap(t); // 获取当前线程的ThreadLocalMap(每个线程拥有自己独立的副本)
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

总结来说就是,ThreadLocal是将线程需要访问的数据存储在线程对象自身中,从而避免多线程的竞争,实现了线程隔离。

  1. 内存泄漏问题

3.1 弱引用

我们可以注意到在ThreadLocalMap的Entry中,ThreadLocal作为key使用了弱引用。为什么使用弱引用呢?使用强引用会有什么问题?我们先回顾下Java的引用

  • 强引用:一个对象具有强引用,那么一定不会被gc回收;即使内存不足时,也不会回收
  • 软引用:可有可无,当内存不够时,不会回收;当内存不足时,就会回收
  • 弱引用:比软引用具有更短的生命周期。当垃圾回收器扫描到这部分内存,发现弱引用,无论内存是否足够,都会把它回收掉.
  • 虚引用:并不能决定对象的生命周期。任何时候都可能被回收。

3.1.1 为什么不使用强引用?

当我们使用完ThreadLocal准备释放它时,比如置为null,而此时ThreadLocalMap的ThreadLocal因为具有强引用,会导致不能被回收,从而可能会造成内存泄漏,且不符合用户预期的结果。

3.1.2 为什么使用弱引用?

为了解决使用强引用内存泄漏的问题。我们使用完把它置为null时,此时ThreadLocalMap中的ThreadLocal因为只具有弱引用,所以很容易被gc回收掉,从而释放掉,符合用户预期结果。

3.2 内存泄漏

使用弱引用就不会有问题了吗?仍然会有内存泄漏的问题。
当ThreadLocal由于弱引用被gc回收时,此时键值对中的value由于是强引用,所以此时并没有被回收,如果当前线程一直在持续工作(比如线程池中的线程持续复用),那么value会始终存在一条强引用链,而导致不能被回收,从而造成内存泄漏

CurrentThread—>ThreadLocalMap—>Entry—>value

3.3 如何解决内存泄漏

  1. 使用完ThreadLocal,调用其remove()方法,清除对应的Entry
1
2
3
4
5
6
java复制代码try {
threadLocal.set("张三");
……
} finally {
localName.remove();
}
  1. 当threadLocal被回收,key=null时,当前Thread的ThreadLocalMap每次get、set和remove方法时,都会对key=null的entry进行扫描,同时会把value置为null,从而回收,避免内存泄漏。
  2. 某些层面上,我们可以把ThreadLocal用static声明,从而保证ThreadLocal始终为强引用,不会被回收。
  1. 应用场景

  1. 想象你有一个场景,调用链路非常的长。当你在其中某个环节中查询到了一个数据后,最后的一个节点需要使用一下.这个时候你怎么办?你是在每个接口的入参中都加上这个参数,传递进去,然后只有最后一个节点用吗?
    可以实现,但是不太优雅
  2. 再想想一个场景,你有一个和业务没有一毛钱关系的参数,比如 traceId ,纯粹是为了做日志追踪用。你加一个和业务无关的参数一路透传干啥玩意?

此时就可以选择ThreadLocal.

  1. 链路跟踪中保存线程上下文环境,在一个请求流转中非常方便的获取一些关键信息
  2. PageHelper中,出现不加startPage也会给执行sql添加limit的小错误,可能就是因为ThreadLocal中的数据没有被清除导致的
  3. Spring框架的事务管理中使用ThreadLocal来管理连接,每个线程是单独的连接,当事务失败时不能影响到其他线程的事务过程或结果。Mybatis也是用ThreadLocal管理,SqlSession也是如此
  1. 其他问题

5.1 ThreadLocalMap解决Hash冲突

ThreadLocalMap中并没有使用链表的方式来解决Hash冲突,而是使用的线性探测法,即根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap这里位置是找下一个相邻的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}

/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}

5.2 扩展

Dubbo中的InternalThreadLocal 是 ThreadLocal 的一个变种,当配合 InternalThread 使用时,具有比普通 Thread 更高的访问性能。

InternalThread 的内部使用的是数组,通过下标定位,非常的快。如果遇得扩容,直接数组扩大一倍,完事。

而 ThreadLocal 的内部使用的是 hashCode 去获取值,多了一步计算的过程,而且用 hashCode 必然会遇到 hash 冲突的场景,ThreadLocal 还得去解决 hash 冲突,如果遇到扩容,扩容之后还得 rehash ,就会慢一些

数据结构都不一样了,这其实就是这两个类的本质区别,也是 InternalThread 的性能在 Dubbo 的这个场景中比 ThreadLocal 好的根本原因。

而 InternalThread 这个设计思想是从 Netty 的 FastThreadLocal 中学来的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

我发现!TransactionSynchronization

发表于 2021-11-12

TransactionSynchronizationManager事务同步管理器

TransactionSynchronizationManager理解

TransactionSynchronizationManager使用多个ThreadLocal维护每个线程事务独立的资源信息,配合AbstractPlatformTransactionManager以及其子类,
设置线程事务的配置属性和运行态,并可通过自定义实现TransactionSynchronization进行注册,监听事务的操作
举个业务场景:假设业务流程最后有个异步操作,业务流程依赖事务,且异步操作依赖业务流程在事务中新增或者更新的数据,
这会出现一个问题,异步操作在事务还没提交前执行了,查询的数据是未提交前的,导致异步操作无效
那么可以使用Spring的TransactionSynchronizationManager注册自定义的TransactionSynchronization,实现afterCommit方法(也有其他方法),
使得在事务的那个运行状态下,执行自定义的业务逻辑

TransactionSynchronizationManager源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public abstract class TransactionSynchronizationManager {
//线程上下文中保存着【线程池对象:ConnectionHolder】的Map对象。线程可以通过该属性获取到同一个Connection对象。
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
//事务同步器,事务运行时的扩展代码,每个线程可以注册N个事务同步器。
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal<>("Transaction synchronizations");
//事务名称
private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal<>("Current transaction name");
//事务是否只读
private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal<>("Current transaction read-only status");
//事务隔离级别
private static final ThreadLocal<Integer> currentTransactionIsolationLevel = new NamedThreadLocal<>("Current transaction isolation level");
//事务是否开启
private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal<>("Actual transaction active");

}

在org.springframework.transaction.interceptor.TransactionInterceptor#invoke中,对事务方法进行拦截处理。在createTransactionIfNecessary里执行getTransaction时,会调用AbstractPlatformTransactionManager#prepareSynchronization方法初始化事务同步器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码/**
* Initialize transaction synchronization as appropriate.
*/
protected void prepareSynchronization(DefaultTransactionStatus status, TransactionDefinition definition) {
if (status.isNewSynchronization()) {
TransactionSynchronizationManager.setActualTransactionActive(status.hasTransaction());
TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(
definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT ?
definition.getIsolationLevel() : null);
TransactionSynchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly());
TransactionSynchronizationManager.setCurrentTransactionName(definition.getName());
TransactionSynchronizationManager.initSynchronization();
}
}

TransactionSynchronization扩展

一般常用是TransactionSynchronization的afterCommit和afterCompletion方法

1
2
3
4
5
6
java复制代码TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
//自定义业务操作
}
});

哪里触发afterCommit和afterCompletion呢?

在AbstractPlatformTransactionManager的processCommit方法中进行回调所有TransactionSynchronization

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码private void processCommit(DefaultTransactionStatus status) throws TransactionException {
try {
//提交事务
doCommit(status);
...
try {
//回调所有事务同步器的afterCommit方法。
triggerAfterCommit(status);
}
finally {
//回调所有事务同步器的afterCompletion方法。
triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
}

}
finally {
//清除TransactionSynchronizationManager的ThreadLocal绑定的数据。
//解除Thread绑定的resources资源。
//将Commit设置为自动提交。
//清理ConnectionHolder资源。
cleanupAfterCompletion(status);
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

gin 源码分析之路由实现 导读

发表于 2021-11-12

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

pexels-filip-klinovský-3693548
导读
==

在go语言的框架中,由于net/http包已经实现了连接建立、拆包、封包等几乎70%的基础工作,留下了ServeHTTP这个接口给有各种不同需要的开发人员自行去扩展。这部分扩展中有很大一部分是对路由注册的封装,gin的路由实现并没有完全重新造轮子,而是一部分重要的代码使用的号称速度最快的httprouter,gin自己增加了易于路由管理的路由组的概念。

什么是路由?

路由直观理解就是根据不同的 URL 找到对应的处理函数,也可以成为一个映射的过程。

目前业界比较推荐的 API 接口的设计方式遵循 RESTful 风格。当然现实不总是这样美好,我也见过某些大公司的接口则不区分 GET/POST/DELETE 这些方法,而是完全靠接口的命名来表示不同的方法。

举个简单的例子,如:”创建一篇博客”

1
2
http复制代码RESTful:    POST  /blog/acb
非RESTful: GET /addBlog?name=acb

这种非RESTful 的方式,并不是错的,在内部使用可能不会有太多问题,只要大家都遵循相同的设计规范就好了。这种接口设计风格在人员较少时可能并没有明显的副作用,但是当团队成员超过几十人,甚至上百人时,在不同服务做对接时,因为缺少统一的规范地接口设计,沟通成本将会成倍增加。这里非常推荐大家去看看谷歌云对外提供的API,堪称设计的典范,非常值得参考和学习。同时Kubernetes的接口设计也是非常经典的,同样出自谷歌。

当URI相同,不同的请求 Method,最终其他代表的要处理的事情也完全不一样。

这里留一个小练习,让你来设计一个路由组件,需要满足不同URI和方法可以,你会如何设计呢?

gin 路由设计

如何设计不同的 Method ?

通过上面的介绍,已经知道 RESTful 是要区分方法的,不同的方法代表意义也完全不一样,gin 是如何实现这个的呢?

其实很简单,不同的方法就是一课路由树,所以当 gin 注册路由的时候,会根据不同的 Method 分别注册不同的路由树。

1
2
3
4
bash复制代码GET    /users/{uid} HTTP/1.1 查询
POST /users/{uid} HTTP/1.1 更新
PUT /users/{uid} HTTP/1.1 全量更新
DELETE /users/{uid} HTTP/1.1 删除

这四个请求最终会构造四棵不同的路由树来表达,具体添加方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
// 通过方法查询根
root := engine.trees.get(method)
// 未找到,需要构建新根
if root == nil {
root = new(node)
root.fullPath = "/"
// 添加树根
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
// 更新处理器
root.addRoute(path, handlers)
// ...
}

路由注册过程

1
2
3
4
5
6
7
8
9
go复制代码func main() {
r := gin.Default()
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "OK",
})
})
r.Run() // listen and serve on 0.0.0.0:8080
}

这段示例代码中,通过r.GET方式注册了一个健康检查路由到GET 路由树中。在实际工程项目中并不会这样直接注册路由,而是再进一步抽象封装将路由的注册放到一个单独的文件中进行管理,这样的好处是可以统一管理服务下的路由。

使用 RouteGroup

1
2
3
4
5
6
go复制代码v1 := router.Group("/openapi/v1")
{
v1.POST("/user/login", func(context *gin.Context) {
context.String(http.StatusOK, "openapi v1 user login")
})
}

RouteGroup 是非常重要和实用的功能,可以帮助开发者按照不同的目的对路由进行分组管理。例如,在一个实际的项目服务中,接口一般会分为鉴权接口和非鉴权接口,即需要登录和权限校验或者不需要,这可以通过 RouteGroup 来实现。另外不同版本的接口,也可以使用RouteGroup来区分。

gin 路由的实现细节

1
2
3
4
5
6
7
8
9
go复制代码func main() {
r := gin.Default()
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "OK",
})
})
r.Run() // listen and serve on 0.0.0.0:8080
}

从这个例子开始,我们带着下面三个问题出发:

  • URL->health 是怎么存储的?
  • handler-> 处理器又是怎么存储的?
  • health 和对应处理器实际是关联起来的?

1. 请求方法的底层实现

1
2
3
4
csharp复制代码func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
// 不同的方法底层都是通过handle函数来实现
return group.handle(http.MethodGet, relativePath, handlers)
}

可以看到handle函数是整个路由处理的核心所在,我们来看看对应的实现代码。

1
2
3
4
5
6
7
8
9
go复制代码func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
// 将相对路径转换绝对路径
absolutePath := group.calculateAbsolutePath(relativePath)
// 将处理器进行整合
handlers = group.combineHandlers(handlers)
// 调用addRoute函数将该路由信息加入到路由树中
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}

2. gin的路由树设计

在看gin的路由树设计之前,先来看看如果是我们该怎么设计路由树呢?

1
2
3
bash复制代码GET /blogs
GET /blocks
GET /boo

最简单直接的方式就是全部存储,即每个字符串占都存到树的叶子节点中。但是这种设计会有至少两个非常明显的问题:

  1. 存储空间浪费严重,不同字符串并不是完全不同,其中可能存在大量的相同的子串
  2. 查询效率并不太高,还需要其他一些辅助的措施来保证一定的顺序才能提高查询效率

还有没有更优的解决方案,通过观察 blogs, blocks, boo 是用相同的前缀的,这样就可以采用公共前缀树的方式来存储就会更好。实际上gin 路由树就是一棵前缀树。

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码// 方法树定义
type methodTree struct {
method string
root *node
}

// 树节点定义
type node struct {
path string
indices string
children []*node // 处理器节点 至少1个
handlers HandlersChain
priority uint32
nType nodeType
maxParams uint8
wildChild bool
fullPath string
}

节点操作核心函数如下:

1
2
3
4
go复制代码// addRoute adds a node with the given handle to the path.
// Not concurrency-safe!
func (n *node) addRoute(path string, handlers HandlersChain) {
}

3. URL如何与处理函数关联

从上小节可以看出node 是路由树的核心定义:

  • children成员记录一颗树的所有叶子结点。存储内容为去掉前缀后的路由信息。
  • path 则记录了该节点的最长前缀
  • handlers 存储了当前叶子节点对应的所有处理函数

前文说的路由注册一般发生在服务启动时,在接受请求前会完成所有的服务初始化工作,包括服务路由的注册。当 服务开始接受请求时,路由树已经在内存中构建完毕了,gin框架只需要实现路由的查询就可以了。gin框架开始处理请求的起点是ServeHTTP,因此我们从这里入手。

1
2
3
4
5
6
7
8
9
10
go复制代码func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
// 真正开始处理请求
engine.handleHTTPRequest(c)

engine.pool.Put(c)
}

handleHTTPRequest正是实现了请求URI到处理函数的映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
go复制代码func (engine *Engine) handleHTTPRequest(c *Context) {
// ...
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
// 根据请求方法进行判断
if t[i].method != httpMethod {
continue
}
root := t[i].root
// 在该方法树上查找路由
value := root.getValue(rPath, c.params, unescape)
if value.params != nil {
c.Params = *value.params
}
// 执行处理函数
if value.handlers != nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
c.Next() // 涉及到gin的中间件机制
// 到这里时,请求已经处理完毕,返回的结果也存储在对应的结构体中了
c.writermem.WriteHeaderNow()
return
}
// 省略
break
}
// 略
}

可以总结一下查找路由的整体思路:

  • 遍历所有路由树,找到对应的方法的路由树
  • 进行路由的匹配
  • 执行对应处理函数

总结

本次我们梳理总结了gin的路由整体流程,但是路由数的具体实现并没有特别仔细的讲解,这块可以留一个扣,后期有机会我们单独再讲。在go的web框架中路由的性能几乎决定了整个框架的性能,因此这是一个非常值得再深入挖掘的方向,大家感兴趣可以自行探索,也欢迎跟我讨论互动~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【图解】短视频推荐打散算法

发表于 2021-11-12

这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战

在推荐系统中,最终召回的候选池数据,有一个重要的处理,那就是候选列表中不同数据类型的散列化。例如抖音Feed信息流中推荐的短视频,如果不进行一定程度的打散,那么就有可能会看到连续的同类型堆积在一起,没有层次感。

打散是推荐系统比较重要的数据处理逻辑,是推荐系统避免数据扎堆最重要的实现手段,本次介绍推荐算法中最简单的轮询算法——单维度轮询。

单维度轮询主要是把数据按照某一个角度进行分类,比如短视频推荐业务中,相同作者的视频不能相邻出现。

下面是我自定义的一些打散规则,目的就是返回的视频列表不会有作者的作品相邻重复出现。

短视频打散规则

基本规则

  • 每个视频对应一个作者
  • 相同作者的视频不可连续
  • 可控制每次展示的视频数量

视频游标规则

  • 游标为视频ID,默认从头开始。视频游标可以每次获取视频时都能拿到还未看过的视频。可以解决分页查询视频时,会出现新增视频,旧的视频被挤到下一页,导致重复视频问题。

固定位视频插入规则

  • 标识控制是否显示固定视频
  • 固定视频的位置可以是列表中任意位置,不必是最前面

基本规则图解

  • 初始查出视频列表如下

1.png

  • 查出视频与作者对应关系

2.png

  • 初始结果列表,列表大小由参数size控制

3.png

  • 首次添加视频,视频id放到结果列表中,添加作者ID到作者临时表中

4.png

  • 第二次添加视频,作者临时表中有相同作者,不进行添加,将视频id保存到待添加队列中

5.1.png

  • 第三次添加视频,作者临时表中无相同作者,进行添加

6.png

  • 循环过一次之后,清空作者临时表,取待添加队列中的列表作为新的视频列表,继续走上面逻辑,将视频放入到结果列表中,直到待添加队列无数据,退出递归,返回最终结果列表。

7.png

固定视频规则图解

  • 固定视频需要再初始结果列表中对应索引位置放入视频ID

8.png

  • 作者临时表中放入作者信息

9.png

  • 添加视频逻辑增加个判断当前索引中是否有视频,有视频则不添加。结合上面的相同作者视频不可连续规则,16后面的视频应该是3,并记录作者临时表中放入作者B,如下图

10.png

  • 第一次遍历,只有3和5放到结果列表中了,其余的放到待添加队列中,结果如下

11.png

  • 继续按照基本规则循环添加,直到结果列表满了不再添加,最终结果如图

12.png

小结

本文通过画图,说明了一个简单的自定义打散规则,保证了返回视频列表更有层次感,具体的代码实现会再后面给出。

参考

  • 推荐系统打散算法–轮询(分类桶)
  • 推荐系统中信息流打散算法模型初探

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

小码农教环链 环链

发表于 2021-11-12

「这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战」
[toc]

环链

环形链表

题目

image-20211025221103012

分析

image-20211025223634637

image-20211025225530551

我们剖析一下代码

image-20211025233902707

1
2
3
4
5
6
7
8
9
10
11
c复制代码bool hasCycle(struct ListNode *head) {
struct ListNode* fast = head,*slow = head;
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
if(fast == slow)
return true;
}
return false;
}

延伸问题:

1.为什么fast和slow会在环中相遇,会不会有这么一种情况呢。就是在环中一直交错永远遇不上?请证明一下。

结论:就上面fast和slow而言是一定相遇的

证明:

第一步:slow和fast,fast肯定是先进环,这时slow是fast入环前距离的一半。

第二步:随着slow进环,fast已经在环里走了一段了,走了 多少和环的大小有关系

第三步:我们这里假设slow进环的时候距离和fast是N

slow每次往前走1步,fast往前走两步,每追一次,判断一下相遇,结果为N-1,也就是说每追一次他们间的距离是一步一步的在减少,直到他们相遇

image-20211026071344761

这里就又衍生出了一个问题就是slow与fast只要是步差为一就可以相遇

2.为什么slow走一步,fast走两步呢?fast可不可以走大于两步呢?

结论:fast走n步,n>2,不一定会相遇

这里分析一个slow走一步,fast走三步的交错与不交错的情况

他们之间的距离变化是每次是两两的减少,这也就意味着可能会交错

image-20211026080439189

上面的奇偶问题也就又衍生出了N是他们的步差倍数就能相遇

环形链表 II

题目

image-20211026222807713

如何求环的入口点

分析

我们先直接放结论一个指针从相遇点开始走,一个指针从链表头开始走,他们会在环的入口点相遇

image-20211026233854511

image-20211026234236282

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
c复制代码struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode * fast = head,* slow = head;
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
if(slow == fast)//相遇
{
//相遇节点
struct ListNode * meetNode = fast;
while(meetNode != head)
{
meetNode = meetNode->next;
head = head->next;
}
return meetNode;
}
}
return NULL;
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Linux K8S(Kubernetes)常用命令 一、K8

发表于 2021-11-12

「这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战」

一、K8S简介

Kubernetes是一个完备的分布式系统支撑平台,具有完备的集群管理能力,多扩多层次的安全防护和准入机制、多租户应用支撑能力、透明的服务注册和发现机制、內建智能负载均衡器、强大的故障发现和自我修复能力、服务滚动升级和在线扩容能力、可扩展的资源自动调度机制以及多粒度的资源配额管理能力。同时Kubernetes提供完善的管理工具,涵盖了包括开发、部署测试、运维监控在内的各个环节。

K8S作用:

  1. 服务发现与调度
  2. 负载均衡
  3. 服务自愈
  4. 服务弹性扩容
  5. 横向扩容
  6. 存储卷挂载

二、常用命令

2.1 想查看kubectl命令的方法

1
2
bash复制代码kubectl help 
kubectl <command> --help

2.2 常用的命令类

编辑类:编辑服务

1
bash复制代码kubectl edit svc  test0927-1-service -n ns-2

描述类

1
bash复制代码kubectl describe 类型/具体名 -n namespace

获得详情信息类

1
bash复制代码kubectl get 类型/具体名 -n namespace -o wide(或者yaml)

获得详情信息类

1
2
3
4
bash复制代码kubectl get 类型/具体名 -n namespace -o wide(或者yaml)

类型有:
pods、configmap、replicasets(rs)、service(srv)、deployment、endpoints ;

回滚类

1
bash复制代码kubectl rullout

2.3 K8S常用命令

查看参数的解释

1
2
bash复制代码kubectl explain deployment.spec.selector 
kubectl explain Ingress.spec

查看labels

1
2
3
4
bash复制代码get pods --show-labels 
kubectl get pod -n kube-xx -l env
kubectl get pod -n kube-xx -l env=dev
kubectl get all -n kube-xx -l env

查看当前可用的API版本

1
bash复制代码 kubectl api-versions

查看pod部署情况

1
bash复制代码kubectl get pod -n kube-xx-dev -o wide

删除pod

1
bash复制代码kubectl delete pod jenkins-64b866d55f-vw6wf -n kube-xx-dev

查看描述 比如pod启动失败、pod重启,都可以用这个查看是为什么

1
bash复制代码kubectl describe pod nacos-2 -n kube-xx-dev

查看日志

1
bash复制代码kubectl logs -f jenkins-6bbf69d97c-x9gk8 -c jenkins -n kube-xx-dev

进入pod的容器内部

1
bash复制代码kubectl exec -ti jnlp-slave-b1g8x -c nlp-slave /bin/bash -n kube-xx-dev

查看pod,不晓得部署到那个空间的

1
2
3
4
bash复制代码kubectl get pod -A -o wide |grep ingress
kubectl get ingress -A -o wide |grep ingress
kubectl get svc -A -o wide |grep ingress
kubectl get all -A -o wide |grep ingress

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java 里各个容器的最全遍历方法汇总

发表于 2021-11-12

这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战。

遍历可以说是对数据结构最常见的操作。之前说基本的操作是“增删改查”,但是遍历,就是要让我们找到,要对谁进行“增删改查”。一般,会结合一些 if 判断,来进行操作。

所以在准备这期文章时,我在删除那里停顿了很久。想不明白为什么边遍历边删除,要用 Iterator + remove,后来自己写了一个例子,放在文末。自觉非不用也没有关系,就是写出来很奇怪。欢迎批评指正。

套路:

  • for/while 循环,最普通的那种挨个取,要求容器有序;
  • for 增强版:增强 for 循环;
  • Iterator for、while版,新建 Iterator 对象,然后具体再写成 for 或者 while 的循环,用 hasNext 来做是否进行下一次的判断,再新建临时对象,打印。

遍历 List

先初始化一个 ArrayList:

1
2
3
4
Java复制代码List<String> ids = new ArrayList<>();
ids.add("007");
ids.add("008");
ids.add("009");
  • 遍历第一种,因为 list 有序,可以用索引来遍历:
1
2
3
4
5
Java复制代码// List 遍历1
System.out.println("ids 遍历 for1");
for (int i=0; i<ids.size(); i++) {
System.out.println(ids.get(i));
}
  • 第二种,用增强 for 循环,写法不一样,和索引就没有关系了:
1
2
3
4
5
Java复制代码// List 遍历2
System.out.println("ids 遍历 for2");
for (String temp: ids) {
System.out.println(temp);
}
  • 第三种,定义迭代器,然后写在 for 或者 while 里面:
1
2
3
4
5
6
7
8
9
10
11
12
13
Java复制代码// List 遍历 for版 Iterator
System.out.println("ids 遍历 Iterator1");
for (Iterator iterator = ids.listIterator(); iterator.hasNext();) {
String temp = (String) iterator.next();
System.out.println(temp);
}
// List 遍历 while版 Iterator
System.out.println("ids 遍历 Iterator2");
Iterator<String> iterator = ids.iterator();
while (iterator.hasNext()) {
Object temp = iterator.next();
System.out.println(temp);
}

以上内容的运行结果如下:

image.png

遍历 Set

Set 就无序了,所以,没有索引来用。这里顺带复习一下 HashSet 和 TreeSet 的区别,分别对应两种遍历方法:

  • 第一种用增强 for 循环,用了 HashSet,更无序一点:
1
2
3
4
5
6
7
8
9
Java复制代码// 初始化一个 HashSet
Set<Integer> ages = new HashSet<>();
ages.add(8);
ages.add(88);
ages.add(36);
// Set 遍历 1
for (Integer i: ages) {
System.out.println(i);
}

运行结果:

image.png

  • 第二种,同样用 for,但是用 Iterator 来做:
1
2
3
4
5
6
7
8
9
Java复制代码Set<Integer> ages2 = new TreeSet<>();
ages2.add(8);
ages2.add(88);
ages2.add(36);
// Set 遍历 Iterator
for (Iterator iterator2 = ages2.iterator(); iterator2.hasNext();) {
Integer temp = (Integer) iterator2.next();
System.out.println(temp);
}

运行结果就变成了:8 36 88(分行显示)。就已经按大小排好的顺序了。

遍历 Map

遍历 Map 的本质是在遍历 Set。

先初始化一个 课程表,用 HashMap 来存:

1
2
3
4
5
6
Java复制代码// 初始化一个 map
Map<String, String> courses = new HashMap<>();
courses.put("1001", "English");
courses.put("1003", "Novel Reading");
courses.put("1002", "History");
System.out.println("初始化的 Map:" + courses.toString());
  • 第一种方法,取 KeySet,然后用这个 Key,挨个再 get 对应的value:
1
2
3
4
5
vbnet复制代码// 遍历 Map 法1:根据 key ,取 value
Set<String> keySet = courses.keySet();
for (String courseId: keySet) {
System.out.println(courseId + ": " + courses.get(courseId));
}

这里也可以不在外面定义 keySet,直接在 for 里面改成:

1
2
Java复制代码for (String courseID: courses.keySet()) {
}
  • 第二种,去取这个 EntrySet,然后直接用每一个 Entry 来 getKey 和 getValue:
1
2
3
4
5
6
7
Java复制代码// 遍历 Map 法2:就用 entrySet
System.out.println("用 EntrySet 来遍历:");
Set<Map.Entry<String,String>> kvs = courses.entrySet();
for (Iterator iterator2 = kvs.iterator(); iterator2.hasNext();) {
Map.Entry e = (Map.Entry) iterator2.next();
System.out.println(e.getKey() + ": " + e.getValue());
}

对 Map 遍历的代码,运行结果如下:

image.png

迭代时候删除要用 Iterator?

遍历时就删除:

1
2
3
4
5
6
7
Java复制代码System.out.println("遍历完就删除, 这个就显得有点诡异");
int n = ids.size();
for (int i=0; i<n; i++) {
System.out.println(ids.get(0));
System.out.println(ids.toString());
ids.remove(0);
}

就直接用上面初始化后的例子。只是一开始把 size 存起来。不然 size 会随着 list 删除一个元素而改变。
每次先打印一下要删的元素,再打一下当前的 list 状态。
运行结果:
image.png

推荐版写法:

也就是用 Iterator 来实现,在遍历有条件的删除:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Java复制代码// 推荐的写法:
System.out.println(ids.toString());
ids.add("wait remove 1");
ids.add("wait remove 2");
ids.add("wait remove 3");
System.out.println(ids.toString());
System.out.println("推荐写法:");
Iterator iterator1 = ids.iterator();
while(iterator1.hasNext()) {
String obj = (String) iterator1.next();
System.out.println(obj);
if (obj.contains("1")) {
iterator1.remove();
}
}
System.out.println("end"+ids.toString());

运行结果:
image.png

写一下还是更能加深自己的学习理解。继续,开启下一个知识点!冲冲冲 ~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

详解SpringBoot中的profile

发表于 2021-11-12

这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战

写在前面

SpringBoot开发是目前Java后端的主流框架了,但是当我们去真正在生产环境上使用时,难免会存在和开发、测试环境不同的一些配置文件,或者是配置变量值等等的情况。

当遇到这种情况,我们又要如何更好的解决这个问题呢。所以我们要开始补充新的知识了。

profile

SpringBoot中的配置文件,一般都是使用application.yml或者是application.properties文件来实现配置文件的设置。

那么如何实现不同环境下的配置文件不同的情况呢,这个springboot给出了一些方案,我们今天就着重看单文件的形式,大家可以自行参考。

单文件方式

单文件操作,是可以通过---分隔符来进行区分的,比如我们有一些统一的配置,那么就放置在yml文件的头部,比如下面的示例:

1
2
yml复制代码server:
port: 8080

随后我们创建开发环境中的,那么就首先来使用---分隔符进行隔离,然后我们来进行开发环境的特殊配置。如下图示例:

1
2
3
4
5
6
7
8
yml复制代码server:
port: 8080
---
spring:
profiles: dev
server:
servlet:
context-path: /dev

再然后,我们开始创建测试环境中的特殊配置,接着看下图示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
yml复制代码server:
port: 8080
---
spring:
profiles: dev
server:
servlet:
context-path: /dev
---
spring:
profiles: test
server:
servlet:
context-path: /test

最后创建一个生产配置,如下图示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
yml复制代码server:
port: 8080
---
spring:
profiles: dev
server:
servlet:
context-path: /dev
---
spring:
profiles: test
server:
servlet:
context-path: /test
---
spring:
profiles: pro
server:
servlet:
context-path: /pro

如何进行切换各环境的配置呢?那就要使用spring.profiles.active配置了,如下图示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
yaml复制代码server:
port: 8080
spring:
profiles:
active: test
---
spring:
profiles: dev
server:
servlet:
context-path: /dev
---
spring:
profiles: test
server:
servlet:
context-path: /test
---
spring:
profiles: pro
server:
servlet:
context-path: /pro

总结

我们经常在生产环境进行一些JVM的调优,数据库的不同,所以我们才会针对不同环境下进行一些区分。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

springcloud与springcloudalibaba

发表于 2021-11-12

参考连接:

总的文档:github.com/alibaba/spr…

版本对应文档:github.com/alibaba/spr…

组件版本关系

Spring Cloud Alibaba Version Sentinel Version Nacos Version RocketMQ Version Dubbo Version Seata Version
2.2.6.RELEASE 1.8.1 1.4.2 4.4.0 2.7.8 1.3.0
2021.1 or 2.2.5.RELEASE or 2.1.4.RELEASE or 2.0.4.RELEASE 1.8.0 1.4.1 4.4.0 2.7.8 1.3.0
2.2.3.RELEASE or 2.1.3.RELEASE or 2.0.3.RELEASE 1.8.0 1.3.3 4.4.0 2.7.8 1.3.0
2.2.1.RELEASE or 2.1.2.RELEASE or 2.0.2.RELEASE 1.7.1 1.2.1 4.4.0 2.7.6 1.2.0
2.2.0.RELEASE 1.7.1 1.1.4 4.4.0 2.7.4.1 1.0.0
2.1.1.RELEASE or 2.0.1.RELEASE or 1.5.1.RELEASE 1.7.0 1.1.4 4.4.0 2.7.3 0.9.0
2.1.0.RELEASE or 2.0.0.RELEASE or 1.5.0.RELEASE 1.6.3 1.1.1 4.4.0 2.7.3 0.7.1

毕业版本依赖关系(推荐使用)

Spring Cloud Version Spring Cloud Alibaba Version Spring Boot Version
Spring Cloud 2020.0.1 2021.1 2.4.2
Spring Cloud Hoxton.SR9 2.2.6.RELEASE 2.3.2.RELEASE
Spring Cloud Greenwich.SR6 2.1.4.RELEASE 2.1.13.RELEASE
Spring Cloud Hoxton.SR3 2.2.1.RELEASE 2.2.5.RELEASE
Spring Cloud Hoxton.RELEASE 2.2.0.RELEASE 2.2.X.RELEASE
Spring Cloud Greenwich 2.1.2.RELEASE 2.1.X.RELEASE
Spring Cloud Finchley 2.0.4.RELEASE(停止维护,建议升级) 2.0.X.RELEASE
Spring Cloud Edgware 1.5.1.RELEASE(停止维护,建议升级) 1.5.X.RELEASE

依赖管理

Spring Cloud Alibaba BOM 包含了它所使用的所有依赖的版本。

RELEASE 版本

Spring Cloud 2020

如果需要使用 Spring Cloud 2020 版本,请在 dependencyManagement 中添加如下内容

1
2
3
4
5
6
7
xml复制代码<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>

Spring Cloud Hoxton

如果需要使用 Spring Cloud Hoxton 版本,请在 dependencyManagement 中添加如下内容

1
2
3
4
5
6
7
xml复制代码<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

Spring Cloud Greenwich

如果需要使用 Spring Cloud Greenwich 版本,请在 dependencyManagement 中添加如下内容

1
2
3
4
5
6
7
xml复制代码<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.4.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

Spring Cloud Finchley

如果需要使用 Spring Cloud Finchley 版本,请在 dependencyManagement 中添加如下内容

1
2
3
4
5
6
7
xml复制代码<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.0.4.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

Spring Cloud Edgware

如果需要使用 Spring Cloud Edgware 版本,请在 dependencyManagement 中添加如下内容

1
2
3
4
5
6
7
xml复制代码<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>1.5.1.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

孵化器版本依赖关系(不推荐使用)

Spring Cloud Version Spring Cloud Alibaba Version Spring Boot Version
Spring Cloud Greenwich 0.9.0.RELEASE 2.1.X.RELEASE
Spring Cloud Finchley 0.2.X.RELEASE 2.0.X.RELEASE
Spring Cloud Edgware 0.1.X.RELEASE 1.5.X.RELEASE
Note 请注意, Spring Cloud Edgware 最低支持 Edgware.SR5 版本

依赖管理

Spring Cloud Alibaba BOM 包含了它所使用的所有依赖的版本。

RELEASE 版本

Spring Cloud Greenwich

如果需要使用 Spring Cloud Greenwich 版本,请在 dependencyManagement 中添加如下内容

1
2
3
4
5
6
7
xml复制代码<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>0.9.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

Spring Cloud Finchley

如果需要使用 Spring Cloud Finchley 版本,请在 dependencyManagement 中添加如下内容

1
2
3
4
5
6
7
xml复制代码<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>0.2.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

Spring Cloud Edgware

如果需要使用 Spring Cloud Edgware 版本,请在 dependencyManagement 中添加如下内容

1
2
3
4
5
6
7
xml复制代码<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>0.1.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…362363364…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%