Golang 定时任务 github/robfig/cron/v3 使用与源码解析

Cron 源码阅读

robfig/cron/v3 是一个 Golang 的定时任务库,支持 cron 表达式。Cron 的源码真实教科书级别的存在(可能是我菜 …),真的把低耦合高内聚体现地淋漓尽致,另外其中涉及的装饰器模式,并发处理等都很值得学习。

使用 cron 可以很方便的实现一个定时任务,如下:

go get github.com/robfig/cron/v3@v3.0.0
package mainimport "github.com/robfig/cron/v3"c := cron.New()
// 添加一个任务,每 30 分钟 执行一次
c.AddFunc("30 * * * *", func() { fmt.Println("Every hour on the half hour") })
// 开始执行(每个任务会在自己的 goroutine 中执行)
c.Start()// 允许往正在执行的 cron 中添加任务
c.AddFunc("@daily", func() { fmt.Println("Every day") })// 检查上一个和下一个任务执行的时间
inspect(c.Entries())
..
c.Stop()  // 停止调度,但正在运行的作业不会被停止

通过上面的示例,可以发现, cron 最常用的几个函数:

  • New(): 实例化一个 cron 对象
  • Cron.AddFunc(): 向 Cron 对象中添加一个作业,接受两个参数,第一个是 cron 表达式,第二个是一个无参无返回值的函数(作业)
  • Cron.Stop(): 停止调度,Stop 之后不会再有未执行的作业被唤醒,但已经开始执行的作业不会受影响。

关于 cron 表达式可以先看看 cron表达式的介绍与使用 这篇文章,一个 cron 表达式是一个由 5 个空格分隔的字符串,每一部分从左到右分别表示 秒,分, 时, 天,月, 星期,每个部分由数字和一些特殊字符表示一个约定的时间项,在 robfig/cron 中,每一部分允许的特殊字符如下:

Field name是否强制 ?允许的值允许的特殊字符
SecondsYes0-59* / , -
MinutesYes0-59* / , -
HoursYes0-23* / , -
Day of monthYes1-31* / , - ?
MonthYes1-12 or JAN-DEC* / , -
Day of weekYes0-6 or SUN-SAT* / , - ?

这些特殊字符的含义如下:

  • *: 匹配该字段所有值,如 0 0 * 1 1 *, 第三个字段为 * 表示(1 月 1 日)每小时。
  • /: 表示范围增量,如 */12 * * * * * 表示每 12 秒执行一次
  • ,: 用来分隔同一组中的项目,如 * * 5,10,15 3,4 * * 表示每个三月或四月的 5, 10, 15 号(3.05, 3.10, 3.15, 4.05, 4.10,4.15)
  • -: 表示范围,如 */5 * 10-12 * * * 表示每天十点到十二点每五秒执行一次
  • ?: 同 *

cron 表达式虽然简单,但他却能满足定时任务复杂的使用场景,比如每周一到周五早上十点就可以表示为 0 0 10 * * 1-5,除此之外,cron 还有几个预定义的时间表:

EntryDescriptionEquivalent To
@yearly (or @annually)Run once a year, midnight, Jan. 1st0 0 1 1 *
@monthlyRun once a month, midnight, first of month0 0 1 * *
@weeklyRun once a week, midnight between Sat/Sun0 0 * * 0
@daily (or @midnight)Run once a day, midnight0 0 * * *
@hourlyRun once an hour, beginning of hour0 * * * *

表示每隔多长时间时,你还可以使用预定义的 @every <duration> 如每隔十分钟就可以表示为 @every 10m

源码概览

cron 并不是一个很大的库,核心文件与作用如下:

  • chain.go: 装饰器模式,使用 Chain 可以给一个作业添加多个装饰器,以实现日志记录等功能
  • constantdelay.go:顾名思义,提供了一个简单的常量延迟,如 每5分钟,最小粒度支持到秒
  • cron.go:提供核心功能
  • logger.go: 定义了一个 Logger 接口,使之能插入到结构化日志系统中
  • option.go:对默认行为的修改相关
  • parser.go:解析 cron 表达式
  • spec.go

核心数据结构和接口

type Entry truct

Entry 是对添加到 Cron 中的作业的封装,每个 Entry 有一个 ID,除此之外,Entry 里保存了这个作业上次运行的时间和下次运行的时间。

type EntryID inttype Entry struct {ID EntryIDSchedule ScheduleNext time.TimePrev time.TimeWrappedJob JobJob Job
}

type Cron struct

type Cron struct {entries   []*Entry          // 保存了所有加入到 Cron 的作业chain     Chainstop      chan struct{}     // 接收 Stop() 信号的 chanadd       chan *Entry       // Cron 运行过程中接收 AddJob() 信号的 chan remove    chan EntryID      // 接收移除 Job 信号的 chansnapshot  chan chan []Entry // 快照信号running   bool              // 标志 Cron 是否在运行中logger    LoggerrunningMu sync.Mutex        // Cron 运行前需要抢占该锁,保证并发安全location  *time.Locationparser    ScheduleParser    // cron 表达式的解析器nextID    EntryID           // 即将加入的 Job 对应的 Entry 的 IDjobWaiter sync.WaitGroup
}

interface

// Cron 表达式解析器接口,Parse 方法接收一个 Cron 表达式 spec,
// 返回一个解析出的 Schedule 类型对象
type ScheduleParser interface {Parse(spec string) (Schedule, error)
}// Schedule 类型的对象用来表输 Job 的工作周期,它包含一个 Next() 方法,
// 用来返回 Job 下一次执行的时间
type Schedule interface {Next(time.Time) time.Time
}// Job is an interface for submitted cron jobs.
type Job interface {Run()
}

对接口的实现

ScheduleParser 的实现

parser.go 中,我们可以找到对 ScheduleParser 接口的实现 Parser

type Parser struct {options ParseOption
}func (p Parser) Parse(spec string) (Schedule, error) {...}

Parser 通过 NewParser() 方法创建:

func NewParser(options ParseOption) Parser {optionals := 0if options&DowOptional > 0 {optionals++}if options&SecondOptional > 0 {optionals++}if optionals > 1 {panic("multiple optionals may not be configured")}return Parser{options}
}

除此之外,parser.go 中,创建了一个私有的全局变量 standardParser

var standardParser = NewParser(Minute | Hour | Dom | Month | Dow | Descriptor,
)

后续 Cron 所使用的就是这个解析器。

Schedule 的实现

Schedule 的实现位于 spec.go 中,定义了一个 SpecSchedule 结构体,实现了 Schedule 接口:

type SpecSchedule struct {Second, Minute, Hour, Dom, Month, Dow uint64Location *time.Location
}func (s *SpecSchedule) Next(t time.Time) time.Time {...}

Job 的实现

Job 其实就是用户传入的一个函数,对其的实现位于 cron.go 中:

type FuncJob func()func (f FuncJob) Run() { f() }

总结

Cron 中核心数据结构的类图如下:

New()

cron.go 中的 New() 方法用来创建并返回一个 Corn 对象指针,其实现如下:

func New(opts ...Option) *Cron {c := &Cron{entries:   nil,chain:     NewChain(),add:       make(chan *Entry),stop:      make(chan struct{}),snapshot:  make(chan chan []Entry),remove:    make(chan EntryID),running:   false,runningMu: sync.Mutex{},logger:    DefaultLogger,location:  time.Local,parser:    standardParser,}for _, opt := range opts {opt(c)}return c
}

这个函数接收一组可变的 Option 类型的参数,该类型实际上是一类函数:

type Option func(*Cron)

Corn 内置了一些 Option 类型的函数,都在 option.go 中,以 With 开头,用来改变 Cron 的默认行为,在 New() 中创建完 Cron 之后,会依次执行这些函数。

另外,注意 c.parser 的值是 standardParser, 这个变量在上一节介绍过,位于 parser.go 中,是一个 Parse 类型的变量, Parse 是对 SchedleParse 的一个默认实现。

AddFunc()

AddFunc() 用于向 Corn 中添加一个作业:

func (c *Cron) AddFunc(spec string, cmd func()) (EntryID, error) {// 包装return c.AddJob(spec, FuncJob(cmd))
}func (c *Cron) AddJob(spec string, cmd Job) (EntryID, error) {schedule, err := c.parser.Parse(spec)if err != nil {return 0, err}return c.Schedule(schedule, cmd), nil
}

AddFunc() 相较于 AddJob() 帮用户省去了包装成 Job 类型的一步,在 AddJob() 中,调用了 standardParser.Parse() 将 cron 表达式解释成了 schedule 类型,最终,他们调用了 Schedule() 方法:

func (c *Cron) Schedule(schedule Schedule, cmd Job) EntryID {c.runningMu.Lock()defer c.runningMu.Unlock()c.nextID++entry := &Entry{ID:         c.nextID,Schedule:   schedule,WrappedJob: c.chain.Then(cmd),Job:        cmd,}if !c.running {c.entries = append(c.entries, entry)} else {c.add <- entry}return entry.ID
}

这个方法负责创建 Entry 结构体,并把它追加到 Cron 的 entries 列表中,如果 Cron 已经处于运行状态,会将这个创建好的 entry 发送到 Cron 的 add chan 中,在 run() 中会处理这种情况。

Entries() 和 Entry()

这两个方法被用来返回 Cron entries 的一组快照,Entries() 返回所有作业的快照,Entry(id EntryID) 根据 ID 返回特定作业的快照,其实就是遍历了一遍 Entries() 的返回值:

func (c *Cron) Entry(id EntryID) Entry {for _, entry := range c.Entries() {if id == entry.ID {return entry}}return Entry{}
}

关键在于 Entries() 的实现上:

func (c *Cron) Entries() []Entry {c.runningMu.Lock()defer c.runningMu.Unlock()if c.running {replyChan := make(chan []Entry, 1)c.snapshot <- replyChanreturn <-replyChan}return c.entrySnapshot()
}

获取快照时,根据 Cron 是否在运行有不同的处理逻辑,为了避免获取快照过程中 Cron 开始运行,需要竞争 runningMutex;

如果 Cron 没在运行,直接调用 entrySnapshot() 返回快照:

func (c *Cron) entrySnapshot() []Entry {var entries = make([]Entry, len(c.entries))for i, e := range c.entries {entries[i] = *e}return entries
}

这种情况很简单,如果 Cron 已经在运行中了,会向 c.snapshot 发送一个信号,在 cron.run() 中会处理这个信号:

case replyChan := <-c.snapshot:replyChan <- c.entrySnapshot()continue

这有点向一个钩子,Entries() 中创建了一个新的 chan replyChan, 并将其发送给了 c.snapshot, run() 中通过多路复用监听到这个信号后,调用了 c.entrySnapshot() ,并将结果发送到了 replyChan 中,Entries() 阻塞等待结果并返回。

既然最终调用的都是 c.entrySnapshot() 为什么要分两种情况呢?后面再说。

Remove()

Remove() 用于删除一个作业,实现逻辑和 Entries() 类似:

func (c *Cron) Remove(id EntryID) {c.runningMu.Lock()defer c.runningMu.Unlock()if c.running {c.remove <- id} else {c.removeEntry(id)}
}func (c *Cron) removeEntry(id EntryID) {var entries []*Entryfor _, e := range c.entries {if e.ID != id {entries = append(entries, e)}}c.entries = entries
}

run() 中处理 c.remove 信号:

case id := <-c.remove:timer.Stop()now = c.now()c.removeEntry(id)c.logger.Info("removed", "entry", id)

Stop()

Stop() 用来停止 Cron 的运行,但已经在执行中的作业是不会被打断的,也就是从执行 Stop() 之后,不会再有新的作业被调度:

func (c *Cron) Stop() context.Context {c.runningMu.Lock()defer c.runningMu.Unlock()if c.running {c.stop <- struct{}{}c.running = false}ctx, cancel := context.WithCancel(context.Background())go func() {// 等待所有已经在执行的作业执行完毕c.jobWaiter.Wait()// 会发出一个 cancelCtx.Done() 信号cancel()}()return ctx
}

大体逻辑和上面的一样,比较巧妙地是 Stop() 返回了一个 Context, 具体来说是一个 cancelCtx, 用户可以监听 cancelCtx.Done() 得知什么时候 Cron 真的停止了.

Start()

Start() 用于开始执行 Cron:

func (c *Cron) Start() {c.runningMu.Lock()defer c.runningMu.Unlock()if c.running {return}c.running = truego c.run()
}

这个函数干了三件事:

  1. 获取锁
  2. c.running 置为 true 表示 cron 已经在运行中了
  3. 开启一个 goroutine 执行 c.run(), run 中会一直轮循 c.entries 中的 entry, 如果一个 entry 允许执行了,就会开启单独的 goroutine 去执行这个作业

run是整个 cron 的一个核心,它负责处理 cron 开始执行后的大部分事情,包括添加作业,删除作业,执行作业等,这是一个近一百行的大函数,其结构如下:

func (c *Cron) run() {c.logger.Info("start")// 第一部分now := c.now()for _, entry := range c.entries {entry.Next = entry.Schedule.Next(now)c.logger.Info("schedule", "now", now, "entry", entry.ID, "next", entry.Next)}// 第二部分for {// 2.1sort.Sort(byTime(c.entries))// 2.2var timer *time.Timerif len(c.entries) == 0 || c.entries[0].Next.IsZero() {timer = time.NewTimer(100000 * time.Hour)} else {timer = time.NewTimer(c.entries[0].Next.Sub(now))}// 2.3for {select {}break}}
}

大概包含下面这几部分:

  • 第一部分:遍历了 c.entries 列表,通过 schedule.Next() 计算出这个作业下一次执行的时间,并赋值给了 entry.Next 字段。

  • 第二部分是一个死循环,这一部分又可以分为三个部分:

    • 2.1:调用了 sort 的快排,其实是对 entries 中的元素按 Next 字段的时间线后顺序排序。

    • 2.2:这一部分是对定时器的一个初始化操作:如果没有可以执行的作业,定时器被设置为十万小时后触发(其实就是休眠),否则定时器会在第一个作业允许被执行时触发,定时器触发后, 2.3 部分会去做剩下的事。

    • 2.3:这又是整个 run 的核心,其主体是一个死循环(其实它会退出,不算是死循环),这个循环里面的核心又是一个 select 多路复用,这个多路复用里监听了五种信号,这五种信号是怎样发出的我们在上面其实已经说过了,他们分别是定时器触发信号 timer.C, 运行过程中添加作业的信号 c.add, 快照信号 c.snapshot, cron 停止的信号 c.stop, 移除作业的信号 c.remove

      for {select {case now = <-timer.C:// ...case newEntry := <-c.add:// ...case replyChan := <-c.snapshot:// ...continuecase <-c.stop:// ...returncase id := <-c.remove:// ...}break
      }
      

      下面我们分开看对每一种信号的处理:

对 timer.C 的处理

case now = <-timer.C:now = now.In(c.location)c.logger.Info("wake", "now", now)// Run every entry whose next time was less than nowfor _, e := range c.entries {if e.Next.After(now) || e.Next.IsZero() {break}c.startJob(e.WrappedJob)e.Prev = e.Nexte.Next = e.Schedule.Next(now)c.logger.Info("run", "now", now, "entry", e.ID, "next", e.Next)}

这个信号被触发有两种情况:

  1. 排序后 entries 中第 0 位的作业可以被执行了。
  2. 休眠了十万小时后,定时器被触发…

在处理这类信号时,run 会遍历所有的 entries, 因为这些作业都是按下一次执行时间排过序的,所以如果因为第一种情况出发了信号,说明至少有一个作业是可以执行的,我们遍历整个 entries,直到遇到一个作业可执行时间大于当前时间,说明前面遍历到的都是可以执行的,后面的都是不可以执行的;如果因为第二种情况发出来这个信号,则在第一次判断时就会 break

执行作业调用了 cron.startJob() 方法,这个方法会为每个作业开启一个 goroutine 去执行用户函数:

func (c *Cron) startJob(j Job) {c.jobWaiter.Add(1)go func() {defer c.jobWaiter.Done()j.Run()}()
}

这里的操作简单粗暴,直接开 goroutine 去执行,在使用时要注意定时任务一定要能结束,定时任务执行时间过长且执行速率很高时,可能造成 goroutine 泄露,进而可能导致内存溢出。

还有关于 jobWaiter,他是为了通知用户程序 Cron 什么时候真的结束了,结合 Stop() 可以理解。

对 c.add 的处理

case newEntry := <-c.add:timer.Stop()now = c.now()newEntry.Next = newEntry.Schedule.Next(now)c.entries = append(c.entries, newEntry)c.logger.Info("added", "now", now, "entry", newEntry.ID, "next", newEntry.Next)

如果 cron 在运行的过程中有作业被加入,会停止定时器(新加入的作业需要重新进行排序),然后计算新作业的下一次执行时间(cron 未运行时添加作业没有这一步,是因为在 Start 的第一步会集中计算,集中计算结束后,进入第二步的死循环,就不会再次集中计算了),最后把新作业加入到 entries 列表中。

对 c.snapshot 的处理

case replyChan := <-c.snapshot:replyChan <- c.entrySnapshot()continue

上面已经说过这个信号,如果 Cron 在运行过程中,用户请求获取作业快照会触发这个信号,之所以不在 Entries() 中直接返回,是因为一旦 Cron 被启动,entries 列表中的元素就会被不断排序,而这个操作是在另一个 goroutine 中进行的,这就可能导致直接返回的数据是脏数据。

另外,请注意这个 continue, 如果没有 continue, 这个 case 执行完后,select 会退出,接着执行 break, 这可能导致与 c.snapshot 同时满足的其他事件不被执行;可以说,select 外层的那个 for 就是未这种情况存在的。

那为什么只有 c.snapshot 需要 continue 呢?其实这个 select 最终的目的是让 run 重新阻塞等待下一个事件信号,其他几个不重新阻塞,原因在于他们执行完后需要对 entries 重新排序,而快照不需要,仔细对比 c.addc.snapshot, 就会恍然大悟。

对 c.stop 的处理

case <-c.stop:timer.Stop()c.logger.Info("stop")return

这就很简单了,停止定时器,结束 run goroutine, 因为作业的执行在自己单独的 goroutine 中,所以 run() goroutine 的返回不会影响他们。

对 c.remove 的处理

case id := <-c.remove:timer.Stop()now = c.now()c.removeEntry(id)c.logger.Info("removed", "entry", id)

逻辑和 c,add 是一样的。

Option

开头说过,New() 时可以接收一组 option 参数,用以改变 Cron 的默认行为,这些参数其实是一些函数,他们会在 Cron 初始化后被依次执行,Cron 内置了一些函数, 他们会返回 Option 类型的函数,下面简单了解一些这些函数的作用:

WithLocation

用于改变时区,默认情况下通过 time.Local 获取

func WithLocation(loc *time.Location) Option {return func(c *Cron) {c.location = loc}
}

可以这样使用:

c := cron.New(cron.WithLocation(nyc))

WithSeconds

用于覆盖默认的 Cron 解析格式,默认的格式是 分钟 小时 日 月 星期,也就是 Minute | Hour | Dom | Month | Dow

func WithSeconds() Option {return WithParser(NewParser(Second | Minute | Hour | Dom | Month | Dow | Descriptor,))
}

允许的字段如下:

const (Second         ParseOption = 1 << iota // Seconds field, default 0SecondOptional                         // Optional seconds field, default 0Minute                                 // Minutes field, default 0Hour                                   // Hours field, default 0Dom                                    // Day of month field, default *Month                                  // Month field, default *Dow                                    // Day of week field, default *DowOptional                            // Optional day of week field, default *Descriptor                             // Allow descriptors such as @monthly, @weekly, etc.
)

WithParser

如果你觉得 Cron 表达式是在难以理解,也记不住,可以写一个自己的解析器,用这个函数替代原来的解析器。

func WithParser(p ScheduleParser) Option {return func(c *Cron) {c.parser = p}
}

WithChain

修改默认修饰器

func WithChain(wrappers ...JobWrapper) Option {return func(c *Cron) {c.chain = NewChain(wrappers...)}
}

WihLogger

使用自定义的 logger

func WithLogger(logger Logger) Option {return func(c *Cron) {c.logger = logger}
}

Chain

这是一个很值得学习的装饰器模式,我们先看一下默认情况下,装饰器是怎么工作的:

Cron 结构体只有一个 Chain 类型的 chain 字段,该字段在执行 New() 时会通过 NewChain() 初始化:

c := &Cron{entries:   nil,chain:     NewChain(),// ...
}

这个 NewChain() 接收一组装饰器函数,并且会用这些函数初始化一个 Chain 对象返回:

type Chain struct {wrappers []JobWrapper
}func NewChain(c ...JobWrapper) Chain {return Chain{c}
}

每个 Entry 结构体持有一个 WrappedJob Job 属性,在 Schedule() 中初始化时,会调用 chainThan() 方法初始化:

entry := &Entry{ID:         c.nextID,Schedule:   schedule,WrappedJob: c.chain.Then(cmd),// ...
}

Then() 中,这些装饰器会被执行:

func (c Chain) Then(j Job) Job {for i := range c.wrappers {j = c.wrappers[len(c.wrappers)-i-1](j)}return j
}

Then() 返回的是执行完装饰器之后的 Job(被装饰后的 Job), 这也解释了为什么在 run() 中,传递给 startJob() 的是 e.WrappedJob 而不是 e.job.

了解了装饰器是如何工作的,我们再来看 chain.go 中提供的三个内置装饰器

Recover

类似于内置的 recover(),它会捕捉运行过程中的 panic,并使用提供的 logger 记录下来,其实做的事情就是往用户的 Job 里插入了一个 defer func(){}()

func Recover(logger Logger) JobWrapper {return func(j Job) Job {return FuncJob(func() {defer func() {if r := recover(); r != nil {const size = 64 << 10buf := make([]byte, size)buf = buf[:runtime.Stack(buf, false)]err, ok := r.(error)if !ok {err = fmt.Errorf("%v", r)}logger.Error(err, "panic", "stack", "...\n"+string(buf))}}()j.Run()})}
}

DelayIfStillRunning

这个装饰器的作用是保证一个 Job 的前一次执行完,后一次才执行,比如有一个 Job 需要执行 10s, 但执行频率是一秒一次,如果我们想要保证同时只有一个相同的 Job 被执行,就可以使用这个装饰器,在实现上,他是为每个 Job 添加了一个排它锁实现的,Job 执行前获取该锁,退出时释放锁,当一个 Job 等待该锁的时间大于一分钟,会记录在日志中,设计很巧妙。

func DelayIfStillRunning(logger Logger) JobWrapper {return func(j Job) Job {var mu sync.Mutexreturn FuncJob(func() {start := time.Now()mu.Lock()defer mu.Unlock()if dur := time.Since(start); dur > time.Minute {logger.Info("delay", "duration", dur)}j.Run()})}
}

SkipIfStillRunning

上面那个是等待执行完,这个是如果上一个还在执行,就直接跳过,在实现上,这个装饰器使用了一个容量为 1 的 chan, 在执行 Job 前,会消费 chan 里的数据,执行完后,再往 chan 里填一个数据,通过 select 监听 chan, 如果里面有数据,则执行,否则说明上一个还在执行,只打印一个日志就好了。

func SkipIfStillRunning(logger Logger) JobWrapper {return func(j Job) Job {var ch = make(chan struct{}, 1)ch <- struct{}{}return FuncJob(func() {select {case v := <-ch:defer func() { ch <- v }()j.Run()default:logger.Info("skip")}})}
}

总结

Cron 的几个特点:

  1. 允许在允许中添加或删除 Job:通过 chan 发送信号,select 监听,重新排序。
  2. 装饰器机制:允许给 Job 添加装饰器,装饰器会在 Entry 初始化时执行。
  3. 低耦合:New() 时可以传递 Option, 以此可以改变一些默认行为,如可以实现自己的 cron 解释器。
  4. 每个 Job 使用单独的 goroutine 执行。
  5. Stop Cron 不会停止已经开始执行但为执行完的 Job, 可以通过 Context 得知什么时候执行完了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/457402.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

修改 cmd 字体为 Consolas

windows 下的 cmd 窗口默认的字体有点难看&#xff0c;长时间使用操作 node.js 有点小疲劳&#xff0c;可以修改注册表替换字体为 Consolas&#xff0c;并且可以全屏 cmd 窗口&#xff0c;代码如下&#xff1a; Windows Registry Editor Version 5.00 [HKEY_CURRENT_USER\Conso…

关于 HTTP 的一切(HTTP/1.1,HTTP/2,HTTP/3,HTTPS, CORS, 缓存 ,无状态)

HTTP 为什么会出现 HTTP 协议&#xff0c;从 HTTP1.0 到 HTTP3 经历了什么&#xff1f;HTTPS 又是怎么回事&#xff1f; HTTP 是一种用于获取类似于 HTML 这样的资源的 应用层通信协议&#xff0c; 他是万维网的基础&#xff0c;是一种 CS 架构的协议&#xff0c;通常来说&…

AS 2.0新功能 Instant Run

Instant Run上手作为一个Android开发者&#xff0c;很多的时候我们需要花大量的时间在bulid&#xff0c;运行到真机&#xff08;虚拟机&#xff09;上&#xff0c;对于ios上的Playground羡慕不已&#xff0c;这种情况将在Android Studio 2.0有了很大改善&#xff0c;使用instan…

MySQL InnoDB 是如何存储数据的

InnoDB 是怎么存储数据的 本文是《MySQL 是怎样运行的 —— 从根儿上理解 MySQL》读书总结&#xff0c;强烈推荐这本书&#xff1b; CSDN 不能显示 SVG&#xff0c;可能有图片加载不出来&#xff0c;可以到 我的博客 上看。 数据目录 众所周之&#xff0c;MySQL 的数据是存储在…

WebSocket实战之————GatewayWorker使用笔记例子

参考文档&#xff1a;http://www.workerman.net/gatewaydoc/ 目录结构 ├── Applications // 这里是所有开发者应用项目 │ └── YourApp // 其中一个项目目录&#xff0c;目录名可以自定义 │ ├── Events.php // 开发者只需要关注这个文件 │ ├── st…

[转]关于凸优化的一些简单概念

没有系统学过数学优化&#xff0c;但是机器学习中又常用到这些工具和技巧&#xff0c;机器学习中最常见的优化当属凸优化了&#xff0c;这些可以参考Ng的教学资料&#xff1a;http://cs229.stanford.edu/section/cs229-cvxopt.pdf&#xff0c;从中我们可以大致了解到一些凸优化…

centos7部署两个mysql_一文掌握mysql实用工具--pt-online-schema-change、innotop部署

概述因为OSC和innotop这两个需要的依赖包比较接近&#xff0c;所以这次就写一起了&#xff0c;下面介绍下完整的部署教程&#xff0c;以下基于centos7操作系统。官网文档&#xff1a;http://dev.mysql.com/doc/refman/5.7/en/innodb-create-index-overview.htmlOSC&#xff1a;…

python面试题目

问题一&#xff1a;以下的代码的输出将是什么? 说出你的答案并解释。 1234567891011121314class Parent(object):x 1class Child1(Parent):passclass Child2(Parent):passprint Parent.x, Child1.x, Child2.xChild1.x 2print Parent.x, Child1.x, Child2.xParent.x 3print …

修改页面后获得flag_互动征集丨是时候为2021立flag了

2020马上就要过去了今年的flag各位小伙伴实现了多少&#xff1f;翻出了生灰的flag擦擦说不定2021还能接着用哦2020年就要过去了还记得你在年初立下的那些Flag吗&#xff1f;减肥“明天我就开始减肥&#xff01;”是大部分人在大部分时候都挂在嘴边的一句话疫情宅家不仅没减成还…

为ESXI 添加ISCSI存储设备 Linux服务器系统

为ESXI 添加ISCSI存储设备 Linux系统本文使用的LINUX 6系统上一块硬盘制作的ISCSI存储设备其IP地址为&#xff1a;192.168.26.218:在系统上直接输入&#xff1a;yum -y install scsi-target-utils 命令 安装 iscsi分区设置我们将SDD这块硬盘的SDD1作为iscsi存储设备编辑ISCSI配…

出栈顺序 与 卡特兰数(Catalan)的关系

一&#xff0c;问题描述 给定一个以字符串形式表示的入栈序列&#xff0c;请求出一共有多少种可能的出栈顺序&#xff1f;如何输出所有可能的出栈序列&#xff1f; 比如入栈序列为&#xff1a;1 2 3 &#xff0c;则出栈序列一共有五种&#xff0c;分别如下&#xff1a;1 2 3、…

cad多段线画圆弧方向_CAD箭头怎么画

CAD箭头怎么画问&#xff1a;CAD箭头怎么画&#xff1f;答&#xff1a;想要回答CAD箭头怎么画这个问题&#xff0c;得先从CAD多段线命令说起&#xff0c;画箭只是多段线的一种应用。执行CAD多段线命令的三种方式1.单击菜单栏上的"绘图">>"多段线"。2…

php 获取delete蚕丝_php结合Redis实现100万用户投票项目,并实时查看到投票情况的案例...

场景&#xff1a;某网站需要对其项目做一个投票系统&#xff0c;投票项目上线后一小时之内预计有100万用户进行投票&#xff0c;希望用户投票完就能看到实时的投票情况这个场景可以使用redismysql冷热数据交换来解决。何为冷热数据交换&#xff1f;冷数据&#xff1a;之前使用的…

硬件内存模型 Hardware Memory Models

硬件内存模型 Hardware Memory Models (Memory Models, Part 1) Posted on Tuesday, June 29, 2021. 简介&#xff1a;童话的终结 很久以前&#xff0c;当人们还在写单线程程序的时候&#xff0c;让程序跑的更快的一个最有效的办法就是什么也不做&#xff0c;因为下一代硬件…

【Go】Map 的空间利用率统计

Go 中 map 利用率 今天刷 B 站看见有 Up 主在讲布隆过滤器&#xff0c;提到了利用率的问题&#xff0c;假设有一组数据&#xff0c;范围分布非常广&#xff0c;使用布隆过滤器时如何尽量少的减少内存使用&#xff0c;感觉除了针对特定数据的定向优化外没什么特别好的办法&…

ap模式和sta模式共存_AP+AC组网下的本地转发及集中转发

现在越来越多的企业都有自己的无线网络&#xff0c;而无线网络的组网方式一般都是使用ACAP模式进行组网&#xff0c;使用无线网络能够提供经济、高效的网络接入方式。相比有线网络&#xff0c;无线网络下只要能接入无线网的地方都可以使用网络&#xff0c;用户可以自由移动。而…

【干货分享】流程DEMO-事务呈批表

流程名&#xff1a; 事务呈批表 业务描述&#xff1a; 办公采购、会议费用等事务的申请。流程发起时&#xff0c;会检查预算&#xff0c;如果预算不够&#xff0c;将不允许发起费用申请&#xff0c;如果预算够用&#xff0c;将发起流程&#xff0c;同时占用相应金额的预算&…

【译】TcMalloc: Thread-Caching Malloc

TcMalloc 的核心是分层缓存&#xff0c;前端没有锁竞争&#xff0c;可以快速分配和释放较小的内存对象&#xff08;一般是 256 KB&#xff09;前端有两种实现&#xff0c;分别是 pre-CPU 和 pre-Thread 模式&#xff0c;前者申请一块大的连续内存&#xff0c;每一个逻辑 CPU 将…

kotlin编译失败_Kotlin使用GraalVM开发原生命令行应用

背景之前用kotlin开发过一款根据建表DDL语句生成plantuml ER图的应用。被问如何使用&#xff0c;答曰"给你一个jar包&#xff0c;然后执行java -jar ddl2plantuml.jar ./ddl.sql ./er.puml 就可以了。是不是so easy?"结果被吐槽了一番&#xff0c;为什么不能像命令行…

Swift - 添加纯净的Alamofire

Swift - 添加纯净的Alamofire 如果你有代码洁癖,不能容忍任何多余的东西,请继续往下看. 1. 下载Alamofire (https://github.com/Alamofire/Alamofire) 2. 解压缩并打开 Alamofire.xcworkspace 3. 删除不必要的内容 (根据你的需求自己定) 4. 顺便把文件夹里面的无关内容也删除掉…