目录

  • 一、服务雪崩
  • 二、熔断-限流-降级概述
  • 三、熔断限流技术选型
  • 四、sentinel限流
    • 1 - 基于QPS限流
    • 2 - Throttling策略
    • 3 - sentinel预热/冷启动
  • 五、sentinel熔断
    • 1 - 熔断器模型
    • 2 - 基于错误数熔断
    • 3 - 基于错误率熔断
    • 4 - 基于慢请求熔断
  • 六、gin集成sentinel

在讨论熔断-限流-降级,我们再回顾下服务雪崩

一、服务雪崩

  • 服务雪崩原因
  • 服务雪崩应对策略


二、熔断-限流-降级概述

限流和熔断最终都会导致用户的体验降级

  • 限流:流量2k,但是我的服务能力只有1k,所以这个时候多出来的流量怎么办?

    • a.拒绝;
    • b.排队等待;
  • 用户体验
    • 用户体验不好:当前访问用户过多,请稍后重试
    • 用户体验降级:原本是访问流畅,下单流畅 -> 当前访问用户过多,请稍后重试
  • 熔断
    • 比如A服务访问B服务,这时候B服务很慢(B服务压力过大,导致了出现不少请求错误),调用方很容易出现一个问题:每次调用都超时
    • 结果这个时候数据库出现了问题,超时重试,导致网络2k的流量突然变成了3k,这让原本满负荷的B服务雪上加霜,B服务宕机
    • 如果这时候有一种熔断机制(比较恰当的比喻如保险丝)
      • a.发现了大部分请求很慢,50%请求都很慢
      • b.发现请求有50%都错误了
      • c.错误数量很多,比如1s出现了20个错误

三、熔断限流技术选型

  • Hystrix官网:https://github.com/Netflix/Hystrix;Netflix开源的,已经不维护了
  • Sentinel:https://github.com/alibaba/Sentinel;阿里开源的,一直在维护
  • sentinel-golang:https://github.com/alibaba/sentinel-golang
Sentinel Hystrix
隔离策略 信号量隔离 线程池隔离/信号量隔离
熔断降级策略 基于响应时间或失败比率 基于失败比率
实时指标实现 滑动窗口 滑动窗口(基于RxJava)
规则配置 支持多种数据源 支持多种数据源
扩展性 多个扩展点 插件的形式
基于注解的支持 支持 支持
限流 基于QPS,支持基于调用关系的限流 有限的支持
流量整形 支持慢启动、匀速器模式 不支持
系统负载保护 支持 不支持
控制台 开箱即用,可配置规则,查看秒级监控、机器发现等 不完善
常见框架的适配 Servlet、Spring Cloud、Dubbo、gRpc等 Servlet、Spring Cloud Netflix

四、sentinel限流

1 - 基于QPS限流

  • 基于QPS限流

    • Entry表示入口,base.Inbound表示入流量
    • StatIntervalInMs: 规则对应的流量控制器的独立统计结构的统计周期。如果StatIntervalInMs是1000,也就是统计QPS
    • 测试结果是之前10次都是检查通过,第11次和第12次限流
  • ControlBehavior:表示表示流量控制器的控制行为,目前 Sentinel 支持两种控制行为

    • Reject:表示如果当前统计周期内,统计结构统计的请求数超过了阈值,就直接拒绝
    • Throttling:表示匀速排队的统计策略。它的中心思想是,以固定的间隔时间让请求通过
  • Throttling匀速排队

  • Reject策略

package mainimport ("fmt"sentinel "github.com/alibaba/sentinel-golang/api""github.com/alibaba/sentinel-golang/core/base""github.com/alibaba/sentinel-golang/core/flow""log"
)func main() {//先初始化sentinelerr := sentinel.InitDefault()if err != nil {log.Fatalf("初始化sentinel 异常: %v", err)}//配置限流规则_, err = flow.LoadRules([]*flow.Rule{{Resource:               "some-test",TokenCalculateStrategy: flow.Direct,ControlBehavior:        flow.Reject, //匀速通过Threshold:              10,StatIntervalInMs:       1000,},{Resource:               "some-test2",TokenCalculateStrategy: flow.Direct,ControlBehavior:        flow.Reject, //直接拒绝Threshold:              10,StatIntervalInMs:       1000,},})if err != nil {log.Fatalf("加载规则失败: %v", err)}for i := 0; i < 12; i++ {e, b := sentinel.Entry("some-test", sentinel.WithTrafficType(base.Inbound))if b != nil {fmt.Println("限流了")} else {fmt.Println("检查通过")e.Exit()}}
}

2 - Throttling策略

  • Throttling策略:修改 ControlBehavior: flow.Throttling, //匀速通过
package mainimport ("fmt"sentinel "github.com/alibaba/sentinel-golang/api""github.com/alibaba/sentinel-golang/core/base""github.com/alibaba/sentinel-golang/core/flow""log"
)func main() {//先初始化sentinelerr := sentinel.InitDefault()if err != nil {log.Fatalf("初始化sentinel 异常: %v", err)}//配置限流规则_, err = flow.LoadRules([]*flow.Rule{{Resource:               "some-test",TokenCalculateStrategy: flow.Direct,ControlBehavior:        flow.Throttling, //匀速通过Threshold:              10,StatIntervalInMs:       1000,},})if err != nil {log.Fatalf("加载规则失败: %v", err)}for i := 0; i < 12; i++ {e, b := sentinel.Entry("some-test", sentinel.WithTrafficType(base.Inbound))if b != nil {fmt.Println("限流了")} else {fmt.Println("检查通过")e.Exit()}}
}

  • 验证Throttling策略:我们在每个Entry后添加sleep 101ms,结果就是全部通过
package mainimport ("fmt""log""time"sentinel "github.com/alibaba/sentinel-golang/api""github.com/alibaba/sentinel-golang/core/base""github.com/alibaba/sentinel-golang/core/flow"
)func main() {//先初始化sentinelerr := sentinel.InitDefault()if err != nil {log.Fatalf("初始化sentinel 异常: %v", err)}//配置限流规则_, err = flow.LoadRules([]*flow.Rule{{Resource:               "some-test",TokenCalculateStrategy: flow.Direct,ControlBehavior:        flow.Throttling, //匀速通过Threshold:              10,StatIntervalInMs:       1000,},})if err != nil {log.Fatalf("加载规则失败: %v", err)}for i := 0; i < 12; i++ {e, b := sentinel.Entry("some-test", sentinel.WithTrafficType(base.Inbound))if b != nil {fmt.Println("限流了")} else {fmt.Println("检查通过")e.Exit()}time.Sleep(101 * time.Millisecond)}
}

3 - sentinel预热/冷启动

  • WarmUp:即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮

  • 测试预热
package mainimport ("fmt""log""math/rand""time"sentinel "github.com/alibaba/sentinel-golang/api""github.com/alibaba/sentinel-golang/core/base""github.com/alibaba/sentinel-golang/core/flow"
)func main() {//先初始化sentinelerr := sentinel.InitDefault()if err != nil {log.Fatalf("初始化sentinel 异常: %v", err)}var globalTotal intvar passTotal intvar blockTotal intch := make(chan struct{})//配置限流规则_, err = flow.LoadRules([]*flow.Rule{{Resource:               "some-test",TokenCalculateStrategy: flow.WarmUp, //冷启动策略ControlBehavior:        flow.Reject, //直接拒绝Threshold:              1000,WarmUpPeriodSec:        30,},})if err != nil {log.Fatalf("加载规则失败: %v", err)}//我会在每一秒统计一次,这一秒只能 你通过了多少,总共有多少, block了多少, 每一秒会产生很多的blockfor i := 0; i < 100; i++ {go func() {for {globalTotal++e, b := sentinel.Entry("some-test", sentinel.WithTrafficType(base.Inbound))if b != nil {//fmt.Println("限流了")blockTotal++time.Sleep(time.Duration(rand.Uint64()%10) * time.Millisecond)} else {passTotal++time.Sleep(time.Duration(rand.Uint64()%10) * time.Millisecond)e.Exit()}}}()}go func() {var oldTotal int //过去1s总共有多少个var oldPass int  //过去1s总共pass多少个var oldBlock int //过去1s总共block多少个for {oneSecondTotal := globalTotal - oldTotaloldTotal = globalTotaloneSecondPass := passTotal - oldPassoldPass = passTotaloneSecondBlock := blockTotal - oldBlockoldBlock = blockTotaltime.Sleep(time.Second)fmt.Printf("total:%d, pass:%d, block:%d\n", oneSecondTotal, oneSecondPass, oneSecondBlock)}}()<-ch
}


五、sentinel熔断

1 - 熔断器模型

  • sentinel熔断器模型:Sentinel 熔断降级基于熔断器模式 (circuit breaker pattern) 实现。熔断器内部维护了一个熔断器的状态机

  • 熔断器有三种状态

    • Closed 状态:也是初始状态,该状态下,熔断器会保持闭合,对资源的访问直接通过熔断器的检查
    • Open 状态:断开状态,熔断器处于开启状态,对资源的访问会被切断
    • Half-Open 状态:半开状态,该状态下除了探测流量,其余对资源的访问也会被切断。探测流量指熔断器处于半开状态时,会周期性的允许一定数目的探测请求通过,如果探测请求能够正常的返回,代表探测成功,此时熔断器会重置状态到 Closed 状态,结束熔断;如果探测失败,则回滚到 Open 状态
  • 静默期
    • Sentinel 熔断器的三种熔断策略都支持静默期 (规则中通过MinRequestAmount字段表示)
    • 静默期是指一个最小的静默请求数,在一个统计周期内,如果对资源的请求数小于设置的静默数,那么熔断器将不会基于其统计值去更改熔断器的状态
    • 静默期的设计理由也很简单,举个例子,假设在一个统计周期刚刚开始时候,第 1 个请求碰巧是个慢请求,这个时候这个时候的慢调用比例就会是 100%,很明显是不合理,所以存在一定的巧合性
    • 所以静默期提高了熔断器的精准性以及降低误判可能性

2 - 基于错误数熔断

package mainimport ("errors""fmt""log""math/rand""time"sentinel "github.com/alibaba/sentinel-golang/api""github.com/alibaba/sentinel-golang/core/circuitbreaker""github.com/alibaba/sentinel-golang/core/config""github.com/alibaba/sentinel-golang/logging""github.com/alibaba/sentinel-golang/util"
)type stateChangeTestListener struct {}func (s *stateChangeTestListener) OnTransformToClosed(prev circuitbreaker.State, rule circuitbreaker.Rule) {fmt.Printf("rule.steategy: %+v, From %s to Closed, time: %d\n", rule.Strategy, prev.String(), util.CurrentTimeMillis())
}func (s *stateChangeTestListener) OnTransformToOpen(prev circuitbreaker.State, rule circuitbreaker.Rule, snapshot interface{}) {fmt.Printf("rule.steategy: %+v, From %s to Open, snapshot: %d, time: %d\n", rule.Strategy, prev.String(), snapshot, util.CurrentTimeMillis())
}func (s *stateChangeTestListener) OnTransformToHalfOpen(prev circuitbreaker.State, rule circuitbreaker.Rule) {fmt.Printf("rule.steategy: %+v, From %s to Half-Open, time: %d\n", rule.Strategy, prev.String(), util.CurrentTimeMillis())
}func main() {total := 0totalPass := 0totalBlock := 0totalErr := 0conf := config.NewDefaultConfig()// for testing, logging output to consoleconf.Sentinel.Log.Logger = logging.NewConsoleLogger()err := sentinel.InitWithConfig(conf)if err != nil {log.Fatal(err)}ch := make(chan struct{})// Register a state change listener so that we could observer the state change of the internal circuit breaker.circuitbreaker.RegisterStateChangeListeners(&stateChangeTestListener{})_, err = circuitbreaker.LoadRules([]*circuitbreaker.Rule{// Statistic time span=10s, recoveryTimeout=3s, maxErrorCount=50{Resource:         "abc",Strategy:         circuitbreaker.ErrorCount,RetryTimeoutMs:   3000, //3s之后尝试恢复MinRequestAmount: 10,   //静默数StatIntervalMs:   5000,Threshold:        50,},})if err != nil {log.Fatal(err)}logging.Info("[CircuitBreaker ErrorCount] Sentinel Go circuit breaking demo is running. You may see the pass/block metric in the metric log.")go func() {for {total++e, b := sentinel.Entry("abc")if b != nil {// g1 blockedtotalBlock++fmt.Println("协程熔断了")time.Sleep(time.Duration(rand.Uint64()%20) * time.Millisecond)} else {totalPass++if rand.Uint64()%20 > 9 {totalErr++// Record current invocation as error.sentinel.TraceError(e, errors.New("biz error"))}// g1 passedtime.Sleep(time.Duration(rand.Uint64()%20+10) * time.Millisecond)e.Exit()}}}()go func() {for {total++e, b := sentinel.Entry("abc")if b != nil {// g2 blockedtotalBlock++time.Sleep(time.Duration(rand.Uint64()%20) * time.Millisecond)} else {// g2 passedtotalPass++time.Sleep(time.Duration(rand.Uint64()%80) * time.Millisecond)e.Exit()}}}()go func() {for {time.Sleep(time.Second)fmt.Println(totalErr)}}()<-ch
}


3 - 基于错误率熔断

package mainimport ("errors""fmt""log""math/rand""time"sentinel "github.com/alibaba/sentinel-golang/api""github.com/alibaba/sentinel-golang/core/circuitbreaker""github.com/alibaba/sentinel-golang/core/config""github.com/alibaba/sentinel-golang/logging""github.com/alibaba/sentinel-golang/util"
)type stateChangeTestListener struct {}func (s *stateChangeTestListener) OnTransformToClosed(prev circuitbreaker.State, rule circuitbreaker.Rule) {fmt.Printf("rule.steategy: %+v, From %s to Closed, time: %d\n", rule.Strategy, prev.String(), util.CurrentTimeMillis())
}func (s *stateChangeTestListener) OnTransformToOpen(prev circuitbreaker.State, rule circuitbreaker.Rule, snapshot interface{}) {fmt.Printf("rule.steategy: %+v, From %s to Open, snapshot: %.2f, time: %d\n", rule.Strategy, prev.String(), snapshot, util.CurrentTimeMillis())
}func (s *stateChangeTestListener) OnTransformToHalfOpen(prev circuitbreaker.State, rule circuitbreaker.Rule) {fmt.Printf("rule.steategy: %+v, From %s to Half-Open, time: %d\n", rule.Strategy, prev.String(), util.CurrentTimeMillis())
}func main() {total := 0totalPass := 0totalBlock := 0totalErr := 0conf := config.NewDefaultConfig()// for testing, logging output to consoleconf.Sentinel.Log.Logger = logging.NewConsoleLogger()err := sentinel.InitWithConfig(conf)if err != nil {log.Fatal(err)}ch := make(chan struct{})// Register a state change listener so that we could observer the state change of the internal circuit breaker.circuitbreaker.RegisterStateChangeListeners(&stateChangeTestListener{})_, err = circuitbreaker.LoadRules([]*circuitbreaker.Rule{// Statistic time span=10s, recoveryTimeout=3s, maxErrorCount=50{Resource:         "abc",Strategy:         circuitbreaker.ErrorRatio,RetryTimeoutMs:   3000, //3s之后尝试恢复MinRequestAmount: 10,   //静默数StatIntervalMs:   5000,Threshold:        0.4,},})if err != nil {log.Fatal(err)}logging.Info("[CircuitBreaker ErrorCount] Sentinel Go circuit breaking demo is running. You may see the pass/block metric in the metric log.")go func() {for {total++e, b := sentinel.Entry("abc")if b != nil {// g1 blockedtotalBlock++fmt.Println("协程熔断了")time.Sleep(time.Duration(rand.Uint64()%20) * time.Millisecond)} else {totalPass++if rand.Uint64()%20 > 9 {totalErr++// Record current invocation as error.sentinel.TraceError(e, errors.New("biz error"))}// g1 passedtime.Sleep(time.Duration(rand.Uint64()%40+10) * time.Millisecond)e.Exit()}}}()go func() {for {total++e, b := sentinel.Entry("abc")if b != nil {// g2 blockedtotalBlock++time.Sleep(time.Duration(rand.Uint64()%20) * time.Millisecond)} else {// g2 passedtotalPass++time.Sleep(time.Duration(rand.Uint64()%80) * time.Millisecond)e.Exit()}}}()go func() {for {time.Sleep(time.Second)fmt.Println(float64(totalErr) / float64(total))}}()<-ch
}


4 - 基于慢请求熔断

package mainimport ("errors""fmt""log""math/rand""time"sentinel "github.com/alibaba/sentinel-golang/api""github.com/alibaba/sentinel-golang/core/circuitbreaker""github.com/alibaba/sentinel-golang/core/config""github.com/alibaba/sentinel-golang/logging""github.com/alibaba/sentinel-golang/util"
)type stateChangeTestListener struct {}func (s *stateChangeTestListener) OnTransformToClosed(prev circuitbreaker.State, rule circuitbreaker.Rule) {fmt.Printf("rule.steategy: %+v, From %s to Closed, time: %d\n", rule.Strategy, prev.String(), util.CurrentTimeMillis())
}func (s *stateChangeTestListener) OnTransformToOpen(prev circuitbreaker.State, rule circuitbreaker.Rule, snapshot interface{}) {fmt.Printf("rule.steategy: %+v, From %s to Open, snapshot: %.2f, time: %d\n", rule.Strategy, prev.String(), snapshot, util.CurrentTimeMillis())
}func (s *stateChangeTestListener) OnTransformToHalfOpen(prev circuitbreaker.State, rule circuitbreaker.Rule) {fmt.Printf("rule.steategy: %+v, From %s to Half-Open, time: %d\n", rule.Strategy, prev.String(), util.CurrentTimeMillis())
}func main() {conf := config.NewDefaultConfig()// for testing, logging output to consoleconf.Sentinel.Log.Logger = logging.NewConsoleLogger()err := sentinel.InitWithConfig(conf)if err != nil {log.Fatal(err)}ch := make(chan struct{})// Register a state change listener so that we could observer the state change of the internal circuit breaker.circuitbreaker.RegisterStateChangeListeners(&stateChangeTestListener{})_, err = circuitbreaker.LoadRules([]*circuitbreaker.Rule{// Statistic time span=10s, recoveryTimeout=3s, slowRtUpperBound=50ms, maxSlowRequestRatio=50%{Resource:         "abc",Strategy:         circuitbreaker.SlowRequestRatio,RetryTimeoutMs:   3000,MinRequestAmount: 10,StatIntervalMs:   5000,MaxAllowedRtMs:   50,Threshold:        0.5,},})if err != nil {log.Fatal(err)}logging.Info("[CircuitBreaker SlowRtRatio] Sentinel Go circuit breaking demo is running. You may see the pass/block metric in the metric log.")go func() {for {e, b := sentinel.Entry("abc")if b != nil {// g1 blockedtime.Sleep(time.Duration(rand.Uint64()%20) * time.Millisecond)} else {if rand.Uint64()%20 > 9 {// Record current invocation as error.sentinel.TraceError(e, errors.New("biz error"))}// g1 passedtime.Sleep(time.Duration(rand.Uint64()%80+10) * time.Millisecond)e.Exit()}}}()go func() {for {e, b := sentinel.Entry("abc")if b != nil {// g2 blockedtime.Sleep(time.Duration(rand.Uint64()%20) * time.Millisecond)} else {// g2 passedtime.Sleep(time.Duration(rand.Uint64()%80) * time.Millisecond)e.Exit()}}}()<-ch
}


六、gin集成sentinel

  • 我们在goods_web中对grpc的GoodsList做限流:熔断跟限流差不多,只是配置不同而已,根据需求进行修改即可
  • goods_web/initialize/init_sentinel.go:初始化sentinel
package initializeimport (sentinel "github.com/alibaba/sentinel-golang/api""github.com/alibaba/sentinel-golang/core/flow""go.uber.org/zap"
)func InitSentinel() {err := sentinel.InitDefault()if err != nil {zap.S().Fatalf("初始化sentinel 异常: %v", err)}//配置限流规则//这种配置应该从nacos中读取_, err = flow.LoadRules([]*flow.Rule{{Resource:               "goods-list",TokenCalculateStrategy: flow.Direct,ControlBehavior:        flow.Reject,//Threshold:              20,Threshold:        3, //为了测试,6秒钟只允许3个请求StatIntervalInMs: 6000,},})if err != nil {zap.S().Fatalf("加载规则失败: %v", err)}
}
  • goods_web/main.go:main中添加初始化逻辑
func main() {//1. 初始化loggerinitialize.InitLogger()//2. 初始化配置文件initialize.InitConfig()//3. 初始化routersRouter := initialize.Routers()//4. 初始化翻译if err := initialize.InitTrans("zh"); err != nil {panic(err)}//5. 初始化srv的连接initialize.InitSrvConn()//6.初始化sentinelinitialize.InitSentinel()//省略。。。
  • goods_web/api/goods/api_goods.go
func List(ctx *gin.Context) {//省略。。。e, b := sentinel.Entry("goods-list", sentinel.WithTrafficType(base.Inbound))if b != nil {ctx.JSON(http.StatusTooManyRequests, gin.H{"msg": "请求过于频繁,请稍后重试",})return}请求商品的service服务r, err := global.GoodsSrvClient.GoodsList(context.WithValue(context.Background(), "ginContext", ctx), request)if err != nil {zap.S().Errorw("[List] 查询 【商品列表】失败")api.HandleGrpcErrorToHttp(err, ctx)return}e.Exit()reMap := map[string]interface{}{"total": r.Total,}

36、熔断-限流-降级相关推荐

  1. 「微服务系列 13」熔断限流隔离降级

    我们知道微服务分布式依赖关系错综复杂,比方说前端的请求转化为后端调用的服务请求,一个前端请求会转为成很多个后端调用的服务请求,那么这个时候后台的服务出现不稳定或者延迟,如果没有好的限流熔断措施,可能会 ...

  2. 系统降级熔断限流和排队

    这类问题的主要原因在于系统压力太大.负载太高,导致无法快速处理业务请求,由此引发更多的后续问题.最常见的情况就是,数据库慢查询将数据库的服务器资源耗尽,导致读写超时,业务读写数据库时要么无法连接数据库 ...

  3. 限流降级神器-哨兵(sentinel)原理分析

    Sentinel 是阿里中间件团队开源的,面向分布式服务架构的轻量级高可用流量控制组件,主要以流量为切入点,从流量控制.熔断降级.系统负载保护等多个维度来帮助用户保护服务的稳定性. 大家可能会问:Se ...

  4. sentinel 阿里 原理_限流降级神器:哨兵(sentinel)原理分析

    文章较长,但是干货满满,建议收藏或关注后细读 Sentinel 是阿里中间件团队开源的,面向分布式服务架构的轻量级高可用流量控制组件,主要以流量为切入点,从流量控制.熔断降级.系统负载保护等多个维度来 ...

  5. Spring Cloud Alibaba配置实例nacos+sentinel+dubbo实行服务注册、配置中心、熔断限流

    通过Spring Cloud Alibaba相关组件nacos+sentinel+dubbo实行服务注册.配置中心.熔断限流等功能 1.本机安装nacos和sentinel-dashboard服务端 ...

  6. 高可用架构之限流降级

    一.服务等级协议 我们常说的N个9,就是对SLA的一个描述. SLA全称是ServiceLevel Agreement,翻译为服务水平协议,也称服务等级协议,它表明了公有云提供服务的等级以及质量. 例 ...

  7. 带哨兵节点的链_限流降级神器-哨兵(sentinel)的资源调用链原理分析

    点击上方 Yoon丶徒手摘星 ,选择 置顶或者星标技术干货每日送达! 我们已经知道了sentinel实现限流降级的原理,其核心就是一堆Slot组成的调用链. 这里大概的介绍下每种Slot的功能职责:N ...

  8. android熔断,限流熔断技术

    Hystrix 作为Spring Cloud官方默认的熔断组件,停止开发,Hystrix官方推荐使用Resilience4j: Netflix的 Hystrix 是一个帮助解决分布式系统交互时超时处理 ...

  9. php熔断,限流、熔断、降级

    mservice 可以方便的对资源进行限流.包括Http.Thrift. ## 一.安装 ``` composer require clevephp/la-limiting ``` ## 二.配置 在 ...

最新文章

  1. Redis报错解决The TCP backlog setting of 511 cannot be enforced和This will create latency and memory usage
  2. google的阴阳历转换查询
  3. Linux 精通Linux的“kill”命令
  4. SQL 2005 全文索引
  5. 敏捷制造:并不是你想像的矛盾体
  6. 二维码研究综述--传统图像处理方法
  7. Docker 数据持久化的三种方案
  8. Ubuntu 下VNC(Real VNC) 的安装和配置
  9. 博文视点新书快讯第78期
  10. 【考研数学】张宇1000题,汤家凤1800,李永乐660,应该怎么选择?
  11. 8款超好用的SVG编辑工具用起来
  12. 100套PPT模板用于论文答辩、工作方案等
  13. Pygame详解(十):mouse 模块
  14. 计算机硬件的五大功能模块,什么是操作系统的五大功能模块
  15. bzoj 4484 [Jsoi2015]最小表示——bitset
  16. 《算法笔记》2.3小节——C/C++快速入门-选择结构
  17. Funcode-Q版泡泡堂
  18. html属性 id去重,JS相关知识点总结
  19. 激光SLAM-地图边界噪点的处理(地图的美化)--图像处理的方法
  20. 在python里面使用you-get批量下载哔哩哔哩视频

热门文章

  1. Python应用篇——词频统计
  2. ObjectARX单点JIG正交简单例子
  3. 我,一个正在成长的西门少年
  4. echarts下载图片,getDataURL获取base64地址
  5. 读《把时间当做朋友》
  6. OkHttp原理解析(一)
  7. 第三代移动通信技术(3G)
  8. 他们所未见过而又等待他们去发现的外界
  9. charles之map功能
  10. 10款推荐系统仿真器(模拟平台)汇总和点评