Coder Social home page Coder Social logo

blog's People

Contributors

dongjun111111 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

blog's Issues

Golang中的race使用

Golang中的race使用

在本质上说,goroutine的使用增加了函数的危险系数论go语言中goroutine的使用。比如一个全局变量,如果没有加上锁,我们写一个比较庞大的项目下来,就根本不知道这个变量是不是会引起多个goroutine竞争。下面的是一个案例:

package main
import(
    "time"
    "fmt"
    "math/rand"
)

func main() {
    start := time.Now()
    var t *time.Timer
    t = time.AfterFunc(randomDuration(), func() {
        fmt.Println(time.Now().Sub(start))
        t.Reset(randomDuration())
    })
    time.Sleep(5 * time.Second)
}

func randomDuration() time.Duration {
    return time.Duration(rand.Int63n(1e9))
}
output==>
948.0543ms
1.0330591s
1.7000973s
1.9351107s
2.2231272s
2.7731587s
3.4061949s
3.7382139s
3.9222244s
4.405252s

再比如下面的例子:

package main
import (
    "time"
    "fmt"
)
func main(){
    a := 1
    go func(){
        a  = 2
    }()
    a = 3
    fmt.Println("a is ",a)
    time.Sleep(2 * time.Second)
}

可喜的是,golang在1.1之后引入了竞争检测的概念。我们可以使用go run -race 或者 go build -race 来进行竞争检测。
golang语言内部大概的实现就是同时开启多个goroutine执行同一个命令,并且纪录每个变量的状态。

runtime  go run -race race1.go
a is  3
==================
WARNING: DATA RACE
Write by goroutine 5:
  main.func·001()
      /Users/yejianfeng/Documents/workspace/go/src/runtime/race1.go:11 +0x3a

Previous write by main goroutine:
  main.main()
      /Users/yejianfeng/Documents/workspace/go/src/runtime/race1.go:13 +0xe7

Goroutine 5 (running) created at:
  main.main()
      /Users/yejianfeng/Documents/workspace/go/src/runtime/race1.go:12 +0xd7
==================
Found 1 data race(s)
exit status 66

这个命令输出了Warning,告诉我们,goroutine5运行到第11行和main goroutine运行到13行的时候触发竞争了。而且goroutine5是在第12行的时候产生的。我们据此可以分析程序哪里出现了问题。

Golang布隆过滤器实现

Golang布隆过滤器实现

布隆过滤器一般用来过滤黑名单。
布隆过滤器的原理不算复杂。对数据进行查找,简单点的可以直接遍历;对于排好顺序的数据,可以使用二分查找等。但这些方法的时间复杂度都较高,分别是O(n)和O(logn)。无法对大量乱序的数据进行快速的查找。
哈希,将给定的数据通过哈希函数得到一个唯一的值,此值可以作为数据的唯一标识,只要通过该标识,再通过哈希函数逆向计算,就能还原出来原始的数据。在前面的网址压缩的调研分析里面介绍的压缩网址的方法,其实也就是一种哈希,只不过借助了数据库的支持。
布隆过滤器就是借助了哈希实现的过滤算法。通过将黑名单的数据哈希之后,可以得到一个数据。申请一个数组空间,长度是黑名单的长度。将前面的数据转换成数组中唯一的一个单元,并且标记为1,标识此位置对应的数据是黑名单。如此一来,过滤的时候,将数据哈希之后,去前面的数组当中查找,如果对应的位置值为1,表明需要被过滤,如果是0,则不需要。如果黑名单有一亿个黑名单数据,每个数据需要1bit来记录,最后也就会占用1G空间,放在内存里面妥妥的。而哈希函数计算时间可以认为是O(1),所以过滤算法效率也很高。
然而,哈希算法不可能保证不发生碰撞。尤其是黑名单这种字符串,可能会包含各种字符,也不能单纯的当作数字来处理。布隆过滤器的做法就是通过调用多个哈希函数,降低碰撞的概率。比如有3个哈希函数,碰撞概率都是10%,并且哈希方式不同,那么一个数据通过三次哈希得到的碰撞概率是单独哈希一次的 10%×10%×10%/10%=1%,也就是说,原本哈希一次碰撞概率为10%,现在三次是0.1%。黑名单误过滤的概率大大降低,而存在于黑名单当中的肯定会被过滤掉。而三次哈希的结果也直接放进之前的数组里即可。判断是否在黑名单当中,三次结果计算结果都匹配才需要过滤。

package dablooms
/*
#cgo LDFLAGS: -ldablooms
#include 
#include 
*/
import "C"

import (
    "unsafe"
)

func Version() string {
    return "0.9.0"
}

type ScalingBloom struct {
    cfilter *C.scaling_bloom_t
}

func NewScalingBloom(capacity C.uint, errorRate C.double, filename string) *ScalingBloom {
    cFilename := C.CString(filename)
    defer C.free(unsafe.Pointer(cFilename))
    sb := &ScalingBloom{
        cfilter: C.new_scaling_bloom(capacity, errorRate, cFilename),
    }
    return sb
}

func NewScalingBloomFromFile(capacity C.uint, errorRate C.double, filename string) *ScalingBloom {
    cFilename := C.CString(filename)
    defer C.free(unsafe.Pointer(cFilename))
    sb := &ScalingBloom{
        cfilter: C.new_scaling_bloom_from_file(capacity, errorRate, cFilename),
    }
    return sb
}

// apparently this is an unsupported feature of cgo
// we should probably use runtime.SetFinalizer
// see: https://groups.google.com/forum/?fromgroups#!topic/golang-dev/5cD0EmU2voI
func (sb *ScalingBloom) Destroy() {
    C.free_scaling_bloom(sb.cfilter)
}

func (sb *ScalingBloom) Check(key []byte) bool {
    cKey := (*C.char)(unsafe.Pointer(&key[0]))
    return C.scaling_bloom_check(sb.cfilter, cKey, C.size_t(len(key))) == 1
}

func (sb *ScalingBloom) Add(key []byte, id C.uint64_t) bool {
    cKey := (*C.char)(unsafe.Pointer(&key[0]))
    return C.scaling_bloom_add(sb.cfilter, cKey, C.size_t(len(key)), id) == 1
}

func (sb *ScalingBloom) Remove(key []byte, id C.uint64_t) bool {
    cKey := (*C.char)(unsafe.Pointer(&key[0]))
    return C.scaling_bloom_remove(sb.cfilter, cKey, C.size_t(len(key)), id) == 1
}

func (sb *ScalingBloom) Flush() bool {
    return C.scaling_bloom_flush(sb.cfilter) == 1
}

func (sb *ScalingBloom) MemSeqNum() C.uint64_t {
    return C.scaling_bloom_mem_seqnum(sb.cfilter)
}

func (sb *ScalingBloom) DiskSeqNum() C.uint64_t {
    return C.scaling_bloom_disk_seqnum(sb.cfilter)
}

参考

http://blog.cyeam.com/hash/2014/07/30/bloomfilter/

Java线程池的原理及实现

Java线程池的原理及实现

1、线程池简介

多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。    
假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。

如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。
            一个线程池包括以下四个基本组成部分:
            1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
            2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
            3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
            4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。

线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。它把T1,T3分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1,T3的开销了。
线程池不仅调整T1,T3产生的时间段,而且它还显著减少了创建线程的数目,看一个例子:
假设一个服务器一天要处理50000个请求,并且每个请求需要一个单独的线程完成。在线程池中,线程数一般是固定的,所以产生线程总数不会超过线程池中线程的数目,而如果服务器不利用线程池来处理这些请求则线程总数为50000。一般线程池大小是远小于50000。所以利用线程池的服务器程序不会为了创建50000而在处理请求时浪费时间,从而提高效率。

代码实现中并没有实现任务接口,而是把Runnable对象加入到线程池管理器(ThreadPool),然后剩下的事情就由线程池管理器(ThreadPool)来完成了.
package mine.util.thread;  
  
import java.util.LinkedList;  
import java.util.List;  
  
/** 
 * 线程池类,线程管理器:创建线程,执行任务,销毁线程,获取线程基本信息 
 */  
public final class ThreadPool {  
    // 线程池中默认线程的个数为5  
    private static int worker_num = 5;  
    // 工作线程  
    private WorkThread[] workThrads;  
    // 未处理的任务  
    private static volatile int finished_task = 0;  
    // 任务队列,作为一个缓冲,List线程不安全  
    private List taskQueue = new LinkedList();  
    private static ThreadPool threadPool;  
  
    // 创建具有默认线程个数的线程池  
    private ThreadPool() {  
        this(5);  
    }  
  
    // 创建线程池,worker_num为线程池中工作线程的个数  
    private ThreadPool(int worker_num) {  
        ThreadPool.worker_num = worker_num;  
        workThrads = new WorkThread[worker_num];  
        for (int i = 0; i < worker_num; i++) {  
            workThrads[i] = new WorkThread();  
            workThrads[i].start();// 开启线程池中的线程  
        }  
    }  
  
    // 单态模式,获得一个默认线程个数的线程池  
    public static ThreadPool getThreadPool() {  
        return getThreadPool(ThreadPool.worker_num);  
    }  
  
    // 单态模式,获得一个指定线程个数的线程池,worker_num(>0)为线程池中工作线程的个数  
    // worker_num<=0创建默认的工作线程个数  
    public static ThreadPool getThreadPool(int worker_num1) {  
        if (worker_num1 <= 0)  
            worker_num1 = ThreadPool.worker_num;  
        if (threadPool == null)  
            threadPool = new ThreadPool(worker_num1);  
        return threadPool;  
    }  
  
    // 执行任务,其实只是把任务加入任务队列,什么时候执行有线程池管理器觉定  
    public void execute(Runnable task) {  
        synchronized (taskQueue) {  
            taskQueue.add(task);  
            taskQueue.notify();  
        }  
    }  
  
    // 批量执行任务,其实只是把任务加入任务队列,什么时候执行有线程池管理器觉定  
    public void execute(Runnable[] task) {  
        synchronized (taskQueue) {  
            for (Runnable t : task)  
                taskQueue.add(t);  
            taskQueue.notify();  
        }  
    }  
  
    // 批量执行任务,其实只是把任务加入任务队列,什么时候执行有线程池管理器觉定  
    public void execute(List task) {  
        synchronized (taskQueue) {  
            for (Runnable t : task)  
                taskQueue.add(t);  
            taskQueue.notify();  
        }  
    }  
  
    // 销毁线程池,该方法保证在所有任务都完成的情况下才销毁所有线程,否则等待任务完成才销毁  
    public void destroy() {  
        while (!taskQueue.isEmpty()) {// 如果还有任务没执行完成,就先睡会吧  
            try {  
                Thread.sleep(10);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
        // 工作线程停止工作,且置为null  
        for (int i = 0; i < worker_num; i++) {  
            workThrads[i].stopWorker();  
            workThrads[i] = null;  
        }  
        threadPool=null;  
        taskQueue.clear();// 清空任务队列  
    }  
  
    // 返回工作线程的个数  
    public int getWorkThreadNumber() {  
        return worker_num;  
    }  
  
    // 返回已完成任务的个数,这里的已完成是只出了任务队列的任务个数,可能该任务并没有实际执行完成  
    public int getFinishedTasknumber() {  
        return finished_task;  
    }  
  
    // 返回任务队列的长度,即还没处理的任务个数  
    public int getWaitTasknumber() {  
        return taskQueue.size();  
    }  
  
    // 覆盖toString方法,返回线程池信息:工作线程个数和已完成任务个数  
    @Override  
    public String toString() {  
        return "WorkThread number:" + worker_num + "  finished task number:"  
                + finished_task + "  wait task number:" + getWaitTasknumber();  
    }  
  
    /** 
     * 内部类,工作线程 
     */  
    private class WorkThread extends Thread {  
        // 该工作线程是否有效,用于结束该工作线程  
        private boolean isRunning = true;  
  
        /* 
         * 关键所在啊,如果任务队列不空,则取出任务执行,若任务队列空,则等待 
         */  
        @Override  
        public void run() {  
            Runnable r = null;  
            while (isRunning) {// 注意,若线程无效则自然结束run方法,该线程就没用了  
                synchronized (taskQueue) {  
                    while (isRunning && taskQueue.isEmpty()) {// 队列为空  
                        try {  
                            taskQueue.wait(20);  
                        } catch (InterruptedException e) {  
                            e.printStackTrace();  
                        }  
                    }  
                    if (!taskQueue.isEmpty())  
                        r = taskQueue.remove(0);// 取出任务  
                }  
                if (r != null) {  
                    r.run();// 执行任务  
                }  
                finished_task++;  
                r = null;  
            }  
        }  
  
        // 停止工作,让该线程自然执行完run方法,自然结束  
        public void stopWorker() {  
            isRunning = false;  
        }  
    }  
} 

测试代码

package mine.util.thread;  
  
//测试线程池  
public class TestThreadPool {  
    public static void main(String[] args) {  
        // 创建3个线程的线程池  
        ThreadPool t = ThreadPool.getThreadPool(3);  
        t.execute(new Runnable[] { new Task(), new Task(), new Task() });  
        t.execute(new Runnable[] { new Task(), new Task(), new Task() });  
        System.out.println(t);  
        t.destroy();// 所有线程都执行完成才destory  
        System.out.println(t);  
    }  
  
    // 任务类  
    static class Task implements Runnable {  
        private static volatile int i = 1;  
  
        @Override  
        public void run() {// 执行任务  
            System.out.println("任务 " + (i++) + " 完成");  
        }  
    }  
} 

运行结果

WorkThread number:3 finished task number:0 wait task number:6
任务 1 完成
任务 2 完成
任务 3 完成
任务 4 完成
任务 5 完成
任务 6 完成
WorkThread number:3 finished task number:6 wait task number:0

简单分析

由于并没有任务接口,传入的可以是自定义的任何任务,所以线程池并不能准确的判断该任务是否真正的已经完成(真正完成该任务是这个任务的run方法执行完毕),只能知道该任务已经出了任务队列,正在执行或者已经完成。

参考

http://blog.csdn.net/hsuxu/article/details/8985931

Golang热更新

Golang热更新
首先强类型的golang自己没有从语言层面支持热更新,也就是说大家可以理解为golang自身不支持热更新。不过有第三方的库让golang支持热更新,比如:https://github.com/rcrowley/goagainhttps://github.com/facebookgo/grace, 这两个都是star在1k以上的,可用性稳定性应该不错(自己还没有尝试使用过-.-)。当然还有人提出使用C的方式来支持热更新。具体是通过编译成so共享库文件(so 为共享库,是shared object,用于Linux下动态链接文件,和window下动态链接库文件dll差不多。特点:ELF格式文件,共享库(动态库),类似于DLL。节约资源,加快速度,代码升级简化),例如:

主程序:

package main
/*
#include 
#cgo LDFLAGS: -ldl

void (*foo)(int);

void set_foo(void* fn) {
    foo = fn;
}

void call_foo(int i) {
    foo(i);
}
*/
import "C"
import "fmt"

func main() {
    n := 0
    var bar string
    for {
        hd := C.dlopen(C.CString(fmt.Sprintf("./foo-%d.so", n)), C.RTLD_LAZY)
        if hd == nil {
            panic("dlopen")
        }
        foo := C.dlsym(hd, C.CString("foo"))
        if foo == nil {
            panic("dlsym")
        }
        fmt.Printf("%v\n", foo)
        C.set_foo(foo)
        C.call_foo(42)
        fmt.Scanf("%s", &bar)
        n++
    }
}

so源码:

package main

import "fmt"
import "C"

func main() {
}

//export foo
func foo(i C.int) {
    fmt.Printf("%d-2\n", i)
}

用 go build -buildmode=c-shared -o foo-1.so mod.go 编译。需要golang编译器版本>=1.5。这是借助C的机制来实现的,go的execution modes文档提到会有go原生的plugin模式。不过这种的可行性有待考究。

还有一种做法是可以将服务端微服务化;这样每个服务的重启成本很低,reload数据库到内存的时间成本就会更低。另外为服务化后,可以针对不同的服务是否有必要热更新,结合脚本或其它方法实现(如:游戏运维的活动服务需要频繁变更)。而像一些基本的如用户,游戏逻辑等接口设计灵活一点的情况下;是完全没必要热更新的;每次版本变更停服重启就ok。

nginx是支持热升级的,可以用老进程服务先前链接的链接,使用新进程服务新的链接,即在不停止服务的情况下完成系统的升级与运行参数修改。那么热升级和热编译是不同的概念,热编译是通过监控文件的变化重新编译,然后重启进程。那么也可以用golang模仿nginx的方式来实现热更新。

根据以上的思路,谢大总结出了一套他自己实现beego热更新的方法,思路如下:

 热升级的原理基本上就是:主进程fork一个进程,然后子进程exec相应的程序。那么这个过程中发生了什么呢?我们知道进程fork之后会把主进程的所有句柄、数据和堆栈、但是里面所有的句柄存在一个叫做CloseOnExec,也就是执行exec的时候,copy的所有的句柄都被关闭了,除非特别申明,而我们期望的是子进程能够复用主进程的net.Listener的句柄。一个进程一旦调用exec类函数,它本身就"死亡"了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。

那么我们要做的:
第一步就是让子进程继承主进程的这个句柄,我们可以通过os.StartProcess的参数来附加Files,把需要继承的句柄写在里面。

第二步就是我们希望子进程能够从这个句柄启动监听,还好Go里面支持net.FileListener,直接从句柄来监听,但是我们需要子进程知道这个FD,所以在启动子进程的时候我们设置了一个环境变量设置这个FD。

第三步就是我们期望老的链接继续服务完,而新的链接采用新的进程,这里面有两个细节,第一就是老的链接继续服务,那么我们怎么有老链接存在?所以我们必须每次接收一个链接记录一下,这样我们就知道还存在没有服务完的链接,第二就是怎么让老进程停止接收数据,让新进程接收数据呢?大家都监听在同一个端口,理论上是随机来接收的,所以这里我们只要关闭老的链接的接收就行,这样就会使得在l.Accept的时候报错。

演示地址:http://my.oschina.net/astaxie/blog/136364 

到底golang是不是一定要热更新功能,最后用达达来观点来总结一下。

没有热更新的确没那么方便,但是也没那么可怕。

原因:

  1. 需要临时重启更新就运营公告,如果实际较长就适当发放补偿。
  2. Go加载数据到内存的速度也比之前快很多,重启压力也没想象的那么大。
  3. 强类型语法在编译器提前排除了很多之前要到线上运行时才能发现的问题,所以BUG率低了。

所以没有热更新也顺利跑下来了。

不过以上只能做为参考,不同项目需求不一样,还是得结合实际情况来判断。

热更新肯定是可以做的,方案挺多,数据驱动、内嵌脚本或者无状态进程都可行,只是花多大代价换多少回报的问题。

如果评估下来觉得热更新必做不可,那么用再大代价也得做,这是项目存亡问题。

如果不是必须的,那就需要评估性价比了。

做热更新、换编程语言或者换服务端架构所花的代价,换来的产品在运营、运维或开发各方面的效率提升,是否划算。

参考

https://www.zhihu.com/question/31912663/answer/53872820

树和图

树和图

解题策略

对于树和图的性质,一般全局解依赖于局部解。通常可以用DFS来判断子问题的解,然后综合得到当前的全局结论。

值得注意的是,当我们在传递节点指针的时候,其实其代表的不只是这个节点本身,而是指对整个子树、子图进行操作。只要每次递归的操作对象的结构一致,我们就可以选择Divide and Conquer(事实上对于树和图总是如此,因为subgraph和subtree仍然是graph和tree结构)。实现函数递归的步骤是:首先设置函数出口,就此类问题而言,递归出口往往是node == NULL。其次,在构造递归的时候,不妨将递归调用自身的部分视为黑盒,并想象它能够完整解决子问题。以二叉树的中序遍历为例,函数的实现为:

void InOrderTraversal(TreeNode *root) {
    if (root == NULL) {
        return;
    }
    InOrderTraversal(root->left);
    root->print();
    InOrderTraversal(root->right);
}

想象递归调用的部分 InOrderTraversal(root->left)/InOrderTraversal(root->right)能够完整地中序遍历一棵子树,那么根据中序遍历“按中序遍历左子树;访问根结点;按中序遍历右子树”的定义,写出上述实现就显得很自然了。

DFS 处理树的问题

有一类关于树的问题是, 要求找出一条满足特定条件的路径 。对于这类问题,通常都是传入一个 vector 记录当前走过的路径(为尽可能模版化,统一记为path),传入 path 的时候可以是引用,可以是值。还需要传入另一个 vector 引用记录所有符合条件的 path (为尽可能模版化,统一记为result)。注意, result 可以用引用或指针形式,相当于一个全局变量,或者就开辟一个独立于函数的成员变量。由于 path 通常是vector ,那么result就是 。当然,那个特定条件,也是函数的一个输入变量。

在解答此类问题的时候,通常都采用DFS来访问,利用回溯**,直到无法继续访问再返回。值得注意的是,如果path本身是以引用(reference)的形式传入,那么需要在返回之前消除之前所做的影响(回溯)。因为传引用(Pass by reference)相当于把path也看作全局变量,对path的任何操作都会影响其他递归状态,而传值(pass by value)则不会。传引用的好处是可以减小空间开销。

树和其他数据结构的相互转换

这类题目要求将树的结构转化成其他数据结构,例如链表、数组等,或者反之,从数组等结构构成一棵树。前者通常是通过树的遍历,合并局部解来得到全局解,而后者则可以利用D&C的策略,递归将数据结构的两部分分别转换成子树,再合并。

寻找特定节点

此类题目通常会传入一个当前节点,要求找到与此节点具有一定关系的特定节点:例如前驱、后继、左/右兄弟等。

对于这类题目,首先可以了解一下常见特定节点的定义及性质。在存在指向父节点指针的情况下,通常可以由当前节点出发,向上倒推解决。如果节点没有父节点指针,一般需要从根节点出发向下搜索,搜索的过程就是DFS。

图的访问

关于图的问题一般有两类。一类是前面提到的关于图的基本问题,例如图的遍历、最短路径、可达性等;另一类是将问题转化成图,再通过图的遍历解决问题。第二类问题有一定的难度,但也有一些规律可循:如果题目有一个起始点和一个终止点,可以考虑看成图的最短路径问题。

树的概念

树(tree)是一种能够分层存储数据的重要数据结构,树中的每个元素被称为树的节点,每个节点有若干个指针指向子节点。从节点的角度来看,树是由唯一的起始节点引出的节点集合。这个起始结点称为根(root)。树中节点的子树数目称为节点的度(degree)。在面试中,关于树的面试问题非常常见,尤其是关于二叉树(binary tree),二叉搜索树(Binary Search Tree, BST)的问题。

所谓的二叉树,是指对于树中的每个节点而言,至多有左右两个子节点,即任意节点的度小于等于2。而广义的树则没有如上限制。二叉树是最常见的树形结构。二分查找树是二叉树的一种特例,对于二分查找树的任意节点,该节点存储的数值一定比左子树的所有节点的值大比右子树的所有节点的值小“(与之完全对称的情况也是有效的:即该节点存储的数值一定比左子树的所有节点的值小比右子树的所有节点的值大)。

基于这个特性,二分查找树通常被用于维护有序数据。二分查找树查找、删除、插入的效率都会于一般的线性数据结构。事实上,对于二分查找树的操作相当于执行二分搜索,其执行效率与树的高度(depth)有关,检索任意数据的比较次数不会多于树的高度。这里需要引入高度的概念:对一棵树而言,从根节点到某个节点的路径长度称为该节点的层数(level),根节点为第0层,非根节点的层数是其父节点的层数加1。树的高度定义为该树中层数最大的叶节点的层数加1,即相当于于从根节点到叶节点的最长路径加1。由此,对于n个数据,二分查找树应该以“尽可能小的高度存储所有数据。由于二叉树第L层至多可以存储 2^L 个节点,故树的高度应在logn量级,因此,二分查找树的搜索效率为O(logn)。

直观上看,尽可能地把二分查找树的每一层“塞满”数据可以使得搜索效率最高,但考虑到每次插入删除都需要维护二分查找树的性质,要实现这点并不容易。特别地,当二分查找树退化为一个由小到大排列的单链表(每个节点只有右孩子),其搜索效率变为O(n)。为了解决这样的问题,人们引入平衡二叉树的概念。所谓平衡二叉树,是指一棵树的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。通过恰当的构造与调整,平衡二叉树能够保证每次插入删除之后都保持平衡性。平衡二叉树的具体实现算法包括AVL算法和红黑算法等。由于平衡二叉树的实现比较复杂,故一般面试官只会问些概念性的问题。

树型的概念

满二叉树(full binary tree):如果一棵二叉树的任何结点,或者是叶节点,或者左右子树都存在,则这棵二叉树称作满二叉树。

完全二叉树(complete binary tree):如果一棵二叉树最多只有最下面的两层节点度数可以小于2,并且最下面一层的节点都集中在该层最左边的连续位置上,则此二叉树称作完全二叉树。

二叉树的遍历

二叉树的常见操作包括树的遍历,即以一种特定的规律访问树中的所有节点。常见的遍历方式包括:

  • 前序遍历(Pre-order traversal):访问根结点;按前序遍历左子树;按前序遍历右子树。
  • 中序遍历(In-order traversal):按中序遍历左子树;访问根结点;按中序遍历右子树。特别地,对于二分- 查找树而言,中序遍历可以获得一个由小到大或者由大到小的有序序列。
  • 后续遍历(Post-order traversal):按后序遍历左子树;按后序遍历右子树;访问根结点。

以上三种遍历方式都是深度优先搜索算法(depth-first search)。深度优先算法最自然的实现方式是通过递归实现,事实上,大部分树相关的面试问题都可以优先考虑递归。此外,另一个值得注意的要点是:深度优先的算法往往都可以通过使用栈数据结构将递归化为非递归实现。这里利用了栈先进后出的特性,其数据的进出顺序与递归顺序一致(请见 Stack and Queue) 。

层次遍历(Level traversal):首先访问第0层,也就是根结点所在的层;当第i层的所有结点访问完之后,再从左至右依次访问第i+1层的各个结点。层次遍历属于广度优先搜索算法(breadth-first search)。广度优先算法往往通过队列数据结构实现。

Trie

字典树(trie or prefix tree)是一个26叉树,用于在一个集合中检索一个字符串,或者字符串前缀。字典树的每个节点有一个指针数组代表其所有子树,其本质上是一个哈希表,因为子树所在的位置(index)本身,就代表了节点对应的字母。节点与每个兄弟具有相同的前缀,这就是trie也被称为prefix tree的原因。

假设我们要存储如下名字,年龄:

Amy 12
Ann 18
Bob 30

则构成的字典树如下:

.                 root: level 0
a---------b       level 1
|         |  
m---n     o       level 2
|   |     |
y   n     b       level 3
|   |     |
12  18   30       level 4

由于Amy和Ann共享前缀a,故第二个字母m和n构成兄弟关系。

字典树以及字典树节点的原型:

class TrieNode {
    private:
        T mContent;
        vector mChildren;
    public:
        Node();
        ~Node();
        friend class Trie;
};
class Trie {
public:
    Trie();
    ~Trie();
    void addWord(string s);
    bool searchWord(string s);
    void deleteWord(string s);
private:
    TrieNode* root;
};

字典树的基本功能如下:

  1. void addWord(string key, int value);

添加一个键:值对。添加时从根节点出发,如果在第i层找到了字符串的第i个字母,则沿该节点方向下降一层(注意,如果下一层存储的是数据,则视为没有找到)。否则,将第i个字母作为新的兄弟插入到第i层。将键插入完成后插入值节点。

  1. bool searchWord(string key, int &value);

查找某个键是否存在,并返回值。从根节点出发,在第i层寻找字符串中第i个字母是否存在。如果是,沿着该节点方向下降一层;否则,返回false。

  1. void deleteWord(string key)

删除一个键:值对。删除时从底层向上删除节点,“直到遇到第一个有兄弟的节点(说明该节点向上都是与其他节点共享的前缀),删除该节点。

堆与优先队列

通常所说的堆(Heap)是指二叉堆,从结构上说是完全二叉树,从实现上说一般用数组。以数组的下标建立父子节点关系:对于下标为i的节点,其父节点为(int)i/2,其左子节点为2i,右子节点为2i+1。堆最重要的性质是,它满足部分有序(partial order):最大(小)堆的父节点一定大于等于(小于等于)当前节点,且堆顶元素一定是当前所有元素的最大(小)值。

堆算法的核心在于插入,删除算法如何保持堆的性质(以下讨论均以最大堆为例):

下移(shift-down)操作:下移是堆算法的核心。对于最大值堆而言,对于某个节点的下移操作相当于比较当前节点与其左右子节点的相对大小。如果当前节点小于其子节点,则将当前节点与其左右子节点中较大的子节点对换,直至操作无法进行(即当前节点大于其左右子节点)。

建堆:假设堆数组长度为n,建堆过程如下,注意这里数组的下标是从 1 开始的:

for i, n/2 downto 1
    do shift-down(A,i)

插入:将新元素插入堆的末尾,并且与父节点进行比较,如果新节点的值大于父节点,则与之交换,即上移(shift-up),直至操作无法进行。

弹出堆顶元素:弹出堆顶元素(假设记为A[1],堆尾元素记为A[n])并维护堆性质的过程如下:

output = A[1]
exchange A[1] <-> A[n]
heap size -= 1
shift-down(A,1)
return output

值得注意的是,堆的插入操作逐层上移,耗时O(log(n)),与二叉搜索树的插入相同。但建堆通过下移所有非叶子节点(下标n/2至1)实现,耗时O(n),小于BST的O(nlog(n))。

通过上述描述,不难发现堆其实就是一个优先队列。对于C++,标准模版库中的priority_queue是堆的一种具体实现。

图(Graph)是节点集合的一个拓扑结构,节点之间通过边相连。图分为有向图和无向图。有向图的边具有指向性,即AB仅表示由A到B的路径,但并不意味着B可以连到A。与之对应地,无向图的每条边都表示一条双向路径。

图的数据表示方式也分为两种,即邻接表(adjacency list)和邻接矩阵(adjacency matrix)。对于节点A,A的邻接表将与A之间相连的所有节点以链表的形势存储起来,节点A为链表的头节点。这样,对于有V个节点的图而言,邻接表表示法包含V个链表。因此,链接表需要的空间复杂度为O(V+E)。邻接表适用于边数不多的稀疏图。但是,如果要确定图中边(u, v)是否存在,则只能在节点u对应的邻接表中以O(E)复杂度线性搜索。

对于有V个节点的图而言,邻接矩阵用V*V的二维矩阵形式表示一个图。矩阵中的元素Aij表示节点i到节点j之间是否直接有边相连。若有,则Aij数值为该边的权值,否则Aij数值为0。特别地,对于无向图,由于边的双向性,其邻接矩阵的转置矩阵为其本身。邻接矩阵的空间复杂度为O(V^2 ),适用于边较为密集的图。邻接矩阵在检索两个节点之间是否有边相连这样一个需求上,具有优势。

图的遍历

对于图的遍历(Graph Transversal)类似于树的遍历(事实上,树可以看成是图的一个特例),也分为广度优先搜索和深度优先搜索。算法描述如下:

广度优先

对于某个节点,广度优先会先访问其所有邻近节点,再访问其他节点。即,对于任意节点,算法首先发现距离为d的节点,当所有距离为d的节点都被访问后,算法才会访问距离为d+1的节点。广度优先算法将每个节点着色为白,灰或黑,白色表示未被发现,灰色表示被发现,黑色表示已访问。算法利用先进先出队列来管理所有灰色节点。
一句话总结,广度优先算法先访问当前节点,一旦发现未被访问的邻近节点,推入队列,以待访问。

《算法导论》第22章图的基本算法给出了广度优先的伪代码实现,引用如下:

BFS(G, s)
For each vertex u exept s
    Do Color[u] = WHITE
        Distance[u] = MAX
        Parent[u] = NIL
Color[s] = GRAY
Distance[s] = 0
Parent[s] = NIL
Enqueue(Q, s)
While Q not empty
    Do u = Dequeue(Q)
        For each v is the neighbor of u
            Do if Color[v] == WHITE
                Color[v] = GRAY
                Distance[v] = Distance[u] + 1
                Parent[v] = u
                Enqueue(Q, v)
            Color[u] = BLACK”

深度优先

深度优先算法尽可能“深”地搜索一个图。对于某个节点v,如果它有未搜索的边,则沿着这条边继续搜索下去,直到该路径无法发现新的节点,回溯回节点v,继续搜索它的下一条边。深度优先算法也通过着色标记节点,白色表示未被发现,灰色表示被发现,黑色表示已访问。算法通过递归实现先进后出。一句话总结,深度优先算法一旦发现没被访问过的邻近节点,则立刻递归访问它,直到所有邻近节点都被访问过了,最后访问自己。

《算法导论》第22章图的基本算法给出了深度优先的伪代码实现,引用如下:

DFS(G)
For each vertex v in G
    Do Color[v] = WHITE
    Parent[v] = NIL
For each vertex v in G
    DFS_Visit(v)

DFS_Visit(u)
Color[u] = GRAY
For each v is the neighbor of u
    If Color[v] == WHITE
        Parent[v] = u
        DFS_Visit(v)
Color[u] = BLACK

单源最短路径问题

对于每条边都有一个权值的图来说,单源最短路径问题是指从某个节点出发,到其他节点的最短距离。该问题的常见算法有Bellman-Ford和Dijkstra算法。前者适用于一般情况(包括存在负权值的情况,但不存在从源点可达的负权值回路),后者仅适用于均为非负权值边的情况。Dijkstra的运行时间可以小于Bellman-Ford。本小节重点介绍Dijkstra算法。

特别地,如果每条边权值相同(无权图),由于从源开始访问图遇到节点的最小深度 就等于到该节点的最短路径,因此 Priority Queue就退化成Queue,Dijkstra算法就退化成BFS。

Dijkstra的核心在于,构造一个节点集合S,对于S中的每一个节点,源点到该节点的最短距离已经确定。进一步地,对于不在S中的节点,“我们总是选择其中到源点最近的节点,将它加入S,并且更新其邻近节点到源点的距离。算法实现时需要依赖优先队列。一句话总结,Dijkstra算法利用贪心的**,在剩下的节点中选取离源点最近的那个加入集合,并且更新其邻近节点到源点的距离,直至所有节点都被加入集合。关于Dijkstra算法的正确性分析,可以使用数学归纳法证明,详见《算法导论》第24章,单源最短路径。 给出伪代码如下:

DIJKSTRA(G, s)
S = EMPTY
Insert all vertexes into Q
While Q is not empty
    u = Q.top
    S.insert(u)
    For each v is the neighbor of u
        If d[v] > d[u] + weight(u, v)
            d[v] = d[u] + weight(u, v)
            parent[v] = u

任意两点之间的最短距离

另一个关于图常见的算法是,如何获得任意两点之间的最短距离(All-pairs shortest paths)。直观的想法是,可以对于每个节点运行Dijkstra算法,该方法可行,但更适合的算法是Floyd-Warshall算法。

Floyd算法的核心是动态编程,利用二维矩阵存储i,j之间的最短距离,矩阵的初始值为i,j之间的权值,如果i,j不直接相连,则值为正无穷。动态编程的递归式为:d(k)ij = min(d(k-1)ij, d(k-1)ik+ d(k-1)kj) (1<= k <= n)。直观上理解,对于第k次更新,我们比较从i到j只经过节点编号小于k的中间节点(d(k-1)ij),和从i到k,从k到j的距离之和(d(k-1)ik+ d(k-1)kj)。Floyd算法的复杂度是O(n^3)。关于Floyd算法的理论分析,请见《算法导论》第25章,每对顶点间的最短路径。 给出伪代码如下:

FLOYD(G)
Distance(0) = Weight(G)
For k = 1 to n
    For i = 1 to n
        For j = 1 to n
Distance(k)ij = min(Distance (k-1)ij, Distance (k-1)ik+ Distance(k-1)kj)  
Return Distance(n)

参考

http://wdxtub.com/2016/01/23/programmer-startline-7/

往期博客引导

往期博客引导

决定重新开一个仓库作为我博客的地址。其实很久之前就已经养成了将重要信息记录下来的习惯,她们就在
这里
,接下来,我会有选择的将一些东西迁移到这里,原来的也会继续更新。

Golang简单Web服务器

Golang简单Web服务器

Golang中创建一个web服务器十分之简单,下面就有一个简单的实现。
main.go

package main

import (
    "html/template"
    "fmt"
    "net/http"
)
func hi(w http.ResponseWriter,r *http.Request){
    fmt.Fprintf(w,"hello wolrd")
}
func login(w http.ResponseWriter,r *http.Request){
    if r.Method == "GET"{
        t,_:=template.ParseFiles("login.gtpl")
        t.Execute(w,nil)
    }else{
        r.ParseForm()
        username := r.Form["username"]
        password := r.Form["password"]
        user := ""
        pass := ""
        for _,s :=range username{
            user +=s
        }
        for _,s :=range password{
            pass +=s
        }
        fmt.Fprintf(w,"username:",user)
        fmt.Fprintf(w,"password:",pass)
        //fmt.Println("username:",r.Form["username"])
        //fmt.Println("password:",r.Form["password"])
    }
}
func main(){
    http.HandleFunc("/",hi)
    http.HandleFunc("/login",login)
    http.ListenAndServe(":9099",nil)
}

login.gtpl

  
  
<title> </title>  
  
  
  
  
        user:   
        pass:   
          
  
  
 

在浏览器地址栏输入:localhost:127.0.0.1:9099/login,可以看到效果。

Golang之垃圾回收

垃圾回收

目前Go中垃圾回收的核心函数是scanblock,源代码在文件runtime/mgc0.c中。这个函数非常难读,单个函数写了足足500多行。上面有两个大的循环,外层循环作用是扫描整个内存块区域,将类型信息提取出来,得到其中的gc域。内层的大循环是实现一个状态机,解析执行类型信息中gc域的指令码。

先说说上一节留的疑问吧。MType中的数据其实是类型信息,但它是用uintptr表示,而不是Type结构体的指针,这是一个优化的小技巧。由于内存分配是机器字节对齐的,所以地址就只用到了高位,低位是用不到的。于是低位可以利用起来存储一些额外的信息。这里的uintptr中高位存放的是Type结构体的指针,低位用来存放类型。通过

    t = (Type*)(type & ~(uintptr)(PtrSize-1));

就可以从uintptr得到Type结构体指针,而通过

    type & (PtrSize-1)

就可以得到类型。这里的类型有TypeInfo_SingleObject,TypeInfo_Array,TypeInfo_Map,TypeInfo_Chan几种。

基本的标记过程

从最简单的开始看,基本的标记过程,有一个不带任何优化的标记的实现,对应于函数debug_scanblock。

debug_scanblock函数是递归实现的,单线程的,更简单更慢的scanblock版本。该函数接收的参数分别是一个指针表示要扫描的地址,以及字节数。

首先要将传入的地址,按机器字节大小对齐。
然后对待扫描区域的每个地址:
找到它所属的MSpan,将地址转换为MSpan里的对象地址。
根据对象的地址,找到对应的标记位图里的标记位。
判断标记位,如果是未分配则跳过。否则加上特殊位标记(debug_scanblock中用特殊位代码的mark位)完成标记。
判断标记位中标记了无指针标记位,如果没有,则要递归地调用debug_scanblock。

这个递归版本的标记算法还是很容易理解的。其中涉及的细节在上节中已经说过了,比如任意给定一个地址,找到它的标记位信息。很明显这里仅仅使用了一个无指针位,并没有精确的垃圾回收。

并行的垃圾回收

Go在这个版本中不仅实现了精确的垃圾回收,而且实现了并行的垃圾回收。标记算法本质上就是一个树的遍历过程,上面实现的是一个递归版本。

并行的垃圾回收需要做的第一步,就是先将算法做成非递归的。非递归版本的树的遍历需要用到一个队列。树的非递归遍历的伪代码大致是:

根结点进队
while(队列不空) {
    出队
    访问
    将子结点进队
}

第二步是使上面的代码能够并行地工作,显然这时是需要一个线程安全的队列的。假设有这样一个队列,那么上面代码就能够工作了。但是,如果不加任何优化,这里的队列的并行访问非常地频繁,对这个队列加锁代价会非常高,即使是使用CAS操作也会大大降低效率。

所以,第三步要做的就是优化上面队列的数据结构。事实上,Go中并没有使用这样一个队列,为了优化,它通过三个数据结构共同来完成这个队列的功能,这三个数据结构分别是PtrTarget数组,Workbuf,lfstack。

先说Workbuf吧。听名字就知道,这个结构体的意思是工作缓冲区,里面存放的是一个数组,数组中的每个元素都是一个待处理的结点,也就是一个Obj指针。这个对象本身是已经标记了的,这个对象直接或间接引用到的对象,都是应该被标记的,它们不会被当作垃圾回收掉。Workbuf是比较大的,一般是N个内存页的大小(目前是2页,也就是8K)。

PtrTarget数组也是一个缓冲区,相当于一个intermediate buffer,跟Workbuf有一点点的区别。第一,它比Workbuf小很多,大概只有32或64个元素的数组。第二,Workbuf中的对象全部是已经标记过的,而PtrTarget中的元素可能是标记的,也可能是没标记的。第三,PtrTarget里面的元素是指针而不是对象,指针是指向任意地址的,而对象是对齐到正确地址的。从一个指针变为一个对象要经过一次变换,上一节中有讲过具体细节。

垃圾回收过程中,会有一个从PtrTarget数组冲刷到Workbuf缓冲区的过程。对应于源代码中的flushptrbuf函数,这个函数作用就是对PtrTaget数组中的所有元素,如果该地址是mark了的,则将它移到Workbuf中。标记过程形成了一个环,在环的一边,对Workbuf中的对象,会将它们可能引用的区域全部放到PtrTarget中记录下来。在环的另一边,又会将PtrTarget中确定需要标记的地址刷到Workbuf中。这个过程一轮一轮地进行,推动非递归版本的树的遍历过程,也就是前面伪代码中的出队,访问,子结点进队的过程。

另一个数据结构是lfstack,这个名字的意思是lock free栈。其实它是被用作了一个无锁的链表,链表结点是以Workbuf为单位的。并行垃圾回收中,多条线程会从这个链表中取数据,每次以一个Workbuf为工作单位。同时,标记的过程中也会产生Workbuf结点放到链中。lfstack保证了对这个链的并发访问的安全性。由于现在链表结点是以Workbuf为单位的,所以保证整体的性能,lfstack的底层代码是用CAS操作实现的。

经过第三步中数据结构上的拆解,整个并行垃圾回收的架构已经呼之欲出了,这就是标记扫描的核心函数scanblock。这个函数是在多线程下并行安全的。

那么,最后一步,多线程并行。整个的gc是以runtime.gc函数为入口的,它实际调用的是gc。进入gc函数后会先stoptheworld,接着添加标记的root区域。然后会设置markroot和sweepspan的并行任务。运行mark的任务,扫描块,运行sweep的任务,最后starttheworld并切换出去。

有一个ParFor的数据结构。在gc函数中调用了

 runtime·parforsetup(work.markfor, work.nproc, work.nroot, nil, false, markroot);
    runtime·parforsetup(work.sweepfor, work.nproc, runtime·mheap->nspan, nil, true, sweepspan);

是设置好回调函数让线程去执行markroot和sweepspan函数。垃圾回收时会stoptheworld,其它goroutine会对发起stoptheworld做出响应,调用runtime.gchelper,这个函数会调用scanblock帮助标记过程。也会并行地做markroot和sweepspan的过程。

  void
    runtime·gchelper(void)
    {
        gchelperstart();

        // parallel mark for over gc roots
        runtime·parfordo(work.markfor);

        // help other threads scan secondary blocks
        scanblock(nil, nil, 0, true);

        if(DebugMark) {
            // wait while the main thread executes mark(debug_scanblock)
            while(runtime·atomicload(&work.debugmarkdone) == 0)
                runtime·usleep(10);
        }

        runtime·parfordo(work.sweepfor);
        bufferList[m->helpgc].busy = 0;
        if(runtime·xadd(&work.ndone, +1) == work.nproc-1)
            runtime·notewakeup(&work.alldone);
    }

其中并行时也有实现工作流窃取的概念,多个worker同时去工作缓存中取数据出来处理,如果自己的任务做完了,就会从其它的任务中“偷”一些过来执行。

垃圾回收的时机

垃圾回收的触发是由一个gcpercent的变量控制的,当新分配的内存占已在使用中的内存的比例超过gcprecent时就会触发。比如,gcpercent=100,当前使用了4M的内存,那么当内存分配到达8M时就会再次gc。如果回收完毕后,内存的使用量为5M,那么下次回收的时机则是内存分配达到10M的时候。也就是说,并不是内存分配越多,垃圾回收频率越高,这个算法使得垃圾回收的频率比较稳定,适合应用的场景。

gcpercent的值是通过环境变量GOGC获取的,如果不设置这个环境变量,默认值是100。如果将它设置成off,则是关闭垃圾回收。

参考

https://tiancaiamao.gitbooks.io/go-internals/content/zh/06.2.html

Golang之defer关键字

Golang之defer关键字

defer和go一样都是Go语言提供的关键字。defer用于资源的释放,会在函数返回之前进行调用。一般采用如下模式:

f,err := os.Open(filename)
if err != nil {
    panic(err)
}
defer f.Close()

如果有多个defer表达式,调用顺序类似于栈,越后面的defer表达式越先被调用。

不过如果对defer的了解不够深入,使用起来可能会踩到一些坑,尤其是跟带命名的返回参数一起使用时。在讲解defer的实现之前先看一看使用defer容易遇到的问题。

使用注意点

先来看看几个例子。例1:

func f() (result int) {
    defer func() {
        result++
    }()
    return 0
}

例2:

func f() (r int) {
     t := 5
     defer func() {
       t = t + 5
     }()
     return t
}

例3:

func f() (r int) {
    defer func(r int) {
          r = r + 5
    }(r)
    return 1
}

例1的正确答案不是0,例2的正确答案不是10,如果例3的正确答案不是6......

defer是在return之前执行的。这个在 官方文档中是明确说明了的。要使用defer时不踩坑,最重要的一点就是要明白,return xxx这一条语句并不是一条原子指令!

函数返回的过程是这样的:先给返回值赋值,然后调用defer表达式,最后才是返回到调用函数中。

defer表达式可能会在设置函数返回值之后,在返回到调用函数之前,修改返回值,使最终的函数返回值与你想象的不一致。

其实使用defer时,用一个简单的转换规则改写一下,就不会迷糊了。改写规则是将return语句拆成两句写,return xxx会被改写成:

返回值 = xxx
调用defer函数
空的return
,

先看例1,它可以改写成这样:

func f() (result int) {
     result = 0  //return语句不是一条原子调用,return xxx其实是赋值+ret指令
     func() { //defer被插入到return之前执行,也就是赋返回值和ret指令之间
         result++
     }()
     return
}

所以这个返回值是1。
再看例2,它可以改写成这样:

func f() (r int) {
     t := 5
     r = t //赋值指令
     func() {        //defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过
         t = t + 5
     }
     return        //空的return指令
}

所以这个的结果是5。

最后看例3,它改写后变成:

func f() (r int) {
     r = 1  //给返回值赋值
     func(r int) {        //这里改的r是传值传进去的r,不会改变要返回的那个r值
          r = r + 5
     }(r)
     return        //空的return
}

所以这个例子的结果是1。

defer确实是在return之前调用的。但表现形式上却可能不像。本质原因是return xxx语句并不是一条原子指令,defer被插入到了赋值 与 ret之间,因此可能有机会改变最终的返回值。

参考

https://tiancaiamao.gitbooks.io/go-internals/content/zh/03.4.html

数据结构之二叉树

数据结构之二叉树

树是一种比较重要的数据结构,尤其是二叉树。二叉树是一种特殊的树,在二叉树中每个节点最多有两个子节点,一般称为左子节点和右子节点(或左孩子和右孩子),并且二叉树的子树有左右之分,其次序不能任意颠倒。二叉树是递归定义的,因此,与二叉树有关的题目基本都可以用递归**解决,当然有些题目非递归解法也应该掌握,如非递归遍历节点等等。
一、求二叉树中的节点个数
递归解法:
(1)如果二叉树为空,节点个数为0
(2)如果二叉树不为空,二叉树节点个数 = 左子树节点个数 + 右子树节点个数 + 1
参考代码如下:

int GetNodeNum(BinaryTreeNode * pRoot)  
{  
    if(pRoot == NULL) // 递归出口  
        return 0;  
    return GetNodeNum(pRoot->m_pLeft) + GetNodeNum(pRoot->m_pRight) + 1;  
}  

二、求二叉树的深度
递归解法:
(1)如果二叉树为空,二叉树的深度为0
(2)如果二叉树不为空,二叉树的深度 = max(左子树深度, 右子树深度) + 1
参考代码如下:

int GetDepth(BinaryTreeNode * pRoot)  
{  
    if(pRoot == NULL) // 递归出口  
        return 0;  
    int depthLeft = GetDepth(pRoot->m_pLeft);  
    int depthRight = GetDepth(pRoot->m_pRight);  
    return depthLeft > depthRight ? (depthLeft + 1) : (depthRight + 1);   
}  

三、前序遍历,中序遍历,后序遍历
前序遍历递归解法:
(1)如果二叉树为空,空操作
(2)如果二叉树不为空,访问根节点,前序遍历左子树,前序遍历右子树
参考代码如下:

void PreOrderTraverse(BinaryTreeNode * pRoot)  
{  
    if(pRoot == NULL)  
        return;  
    Visit(pRoot); // 访问根节点  
    PreOrderTraverse(pRoot->m_pLeft); // 前序遍历左子树  
    PreOrderTraverse(pRoot->m_pRight); // 前序遍历右子树  
}  

中序遍历递归解法
(1)如果二叉树为空,空操作。
(2)如果二叉树不为空,中序遍历左子树,访问根节点,中序遍历右子树
参考代码如下:

void InOrderTraverse(BinaryTreeNode * pRoot)  
{  
    if(pRoot == NULL)  
        return;  
    InOrderTraverse(pRoot->m_pLeft); // 中序遍历左子树  
    Visit(pRoot); // 访问根节点  
    InOrderTraverse(pRoot->m_pRight); // 中序遍历右子树  
}  

后序遍历递归解法
(1)如果二叉树为空,空操作
(2)如果二叉树不为空,后序遍历左子树,后序遍历右子树,访问根节点
参考代码如下:

void PostOrderTraverse(BinaryTreeNode * pRoot)  
{  
    if(pRoot == NULL)  
        return;  
    PostOrderTraverse(pRoot->m_pLeft); // 后序遍历左子树  
    PostOrderTraverse(pRoot->m_pRight); // 后序遍历右子树  
    Visit(pRoot); // 访问根节点  
}  

四、分层遍历二叉树(按层次从上往下,从左往右)

相当于广度优先搜索,使用队列实现。队列初始化,将根节点压入队列。当队列不为空,进行如下操作:弹出一个节点,访问,若左子节点或右子节点不为空,将其压入队列。

void LevelTraverse(BinaryTreeNode * pRoot)  
{  
    if(pRoot == NULL)  
        return;  
    queue q;  
    q.push(pRoot);  
    while(!q.empty())  
    {  
        BinaryTreeNode * pNode = q.front();  
        q.pop();  
        Visit(pNode); // 访问节点  
        if(pNode->m_pLeft != NULL)  
            q.push(pNode->m_pLeft);  
        if(pNode->m_pRight != NULL)  
            q.push(pNode->m_pRight);  
    }  
    return;  
}  

五、 将二叉查找树变为有序的双向链表

要求不能创建新节点,只调整指针。
递归解法:
(1)如果二叉树查找树为空,不需要转换,对应双向链表的第一个节点是NULL,最后一个节点是NULL
(2)如果二叉查找树不为空:
如果左子树为空,对应双向有序链表的第一个节点是根节点,左边不需要其他操作;如果左子树不为空,转换左子树,二叉查找树对应双向有序链表的第一个节点就是左子树转换后双向有序链表的第一个节点,同时将根节点和左子树转换后的双向有序链 表的最后一个节点连接;如果右子树为空,对应双向有序链表的最后一个节点是根节点,右边不需要其他操作;如果右子树不为空,对应双向有序链表的最后一个节点就是右子树转换后双向有序链表的最后一个节点,同时将根节点和右子树转换后的双向有序链表的第一个节点连 接。参考代码如下:

1./****************************************************************************** 
2.参数: 
3.pRoot: 二叉查找树根节点指针 
4.pFirstNode: 转换后双向有序链表的第一个节点指针 
5.pLastNode: 转换后双向有序链表的最后一个节点指针 
6.******************************************************************************/  
7.void Convert(BinaryTreeNode * pRoot,   
8.             BinaryTreeNode * & pFirstNode, BinaryTreeNode * & pLastNode)  
9.{  
10.    BinaryTreeNode *pFirstLeft, *pLastLeft, * pFirstRight, *pLastRight;  
11.    if(pRoot == NULL)   
12.    {  
13.        pFirstNode = NULL;  
14.        pLastNode = NULL;  
15.        return;  
16.    }  
17.  
18.    if(pRoot->m_pLeft == NULL)  
19.    {  
20.        // 如果左子树为空,对应双向有序链表的第一个节点是根节点  
21.        pFirstNode = pRoot;  
22.    }  
23.    else  
24.    {  
25.        Convert(pRoot->m_pLeft, pFirstLeft, pLastLeft);  
26.        // 二叉查找树对应双向有序链表的第一个节点就是左子树转换后双向有序链表的第一个节点  
27.        pFirstNode = pFirstLeft;  
28.        // 将根节点和左子树转换后的双向有序链表的最后一个节点连接  
29.        pRoot->m_pLeft = pLastLeft;  
30.        pLastLeft->m_pRight = pRoot;  
31.    }  
32.  
33.    if(pRoot->m_pRight == NULL)  
34.    {  
35.        // 对应双向有序链表的最后一个节点是根节点  
36.        pLastNode = pRoot;  
37.    }  
38.    else  
39.    {  
40.        Convert(pRoot->m_pRight, pFirstRight, pLastRight);  
41.        // 对应双向有序链表的最后一个节点就是右子树转换后双向有序链表的最后一个节点  
42.        pLastNode = pLastRight;  
43.        // 将根节点和右子树转换后的双向有序链表的第一个节点连接  
44.        pRoot->m_pRight = pFirstRight;  
45.        pFirstRight->m_pRight = pRoot;  
46.    }  
47.  
48.    return;  
49.}  

六、 求二叉树第K层的节点个数
递归解法:
(1)如果二叉树为空或者k<1返回0
(2)如果二叉树不为空并且k==1,返回1
(3)如果二叉树不为空且k>1,返回左子树中k-1层的节点个数与右子树k-1层节点个数之和
参考代码如下:

int GetNodeNumKthLevel(BinaryTreeNode * pRoot, int k)  
{  
    if(pRoot == NULL || k < 1)  
        return 0;  
    if(k == 1)  
        return 1;  
    int numLeft = GetNodeNumKthLevel(pRoot->m_pLeft, k-1); // 左子树中k-1层的节点个数  
    int numRight = GetNodeNumKthLevel(pRoot->m_pRight, k-1); // 右子树中k-1层的节点个数  
    return (numLeft + numRight);  
}  

七、 求二叉树中叶子节点的个数
递归解法:
(1)如果二叉树为空,返回0
(2)如果二叉树不为空且左右子树为空,返回1
(3)如果二叉树不为空,且左右子树不同时为空,返回左子树中叶子节点个数加上右子树中叶子节点个数
参考代码如下:

int GetLeafNodeNum(BinaryTreeNode * pRoot)  
{  
    if(pRoot == NULL)  
        return 0;  
    if(pRoot->m_pLeft == NULL && pRoot->m_pRight == NULL)  
        return 1;  
    int numLeft = GetLeafNodeNum(pRoot->m_pLeft); // 左子树中叶节点的个数  
    int numRight = GetLeafNodeNum(pRoot->m_pRight); // 右子树中叶节点的个数  
    return (numLeft + numRight);  
}  

八、 判断两棵二叉树是否结构相同
不考虑数据内容。结构相同意味着对应的左子树和对应的右子树都结构相同。
递归解法:
(1)如果两棵二叉树都为空,返回真
(2)如果两棵二叉树一棵为空,另一棵不为空,返回假
(3)如果两棵二叉树都不为空,如果对应的左子树和右子树都同构返回真,其他返回假
参考代码如下:

bool StructureCmp(BinaryTreeNode * pRoot1, BinaryTreeNode * pRoot2)  
{  
    if(pRoot1 == NULL && pRoot2 == NULL) // 都为空,返回真  
        return true;  
    else if(pRoot1 == NULL || pRoot2 == NULL) // 有一个为空,一个不为空,返回假  
        return false;  
    bool resultLeft = StructureCmp(pRoot1->m_pLeft, pRoot2->m_pLeft); // 比较对应左子树
       bool resultRight = StructureCmp(pRoot1->m_pRight, pRoot2->m_pRight); // 比较对应右子树  
    return (resultLeft && resultRight);  
}  

九、 判断二叉树是不是平衡二叉树
递归解法:
(1)如果二叉树为空,返回真
(2)如果二叉树不为空,如果左子树和右子树都是AVL树并且左子树和右子树高度相差不大于1,返回真,其他返回假
参考代码:

bool IsAVL(BinaryTreeNode * pRoot, int & height)  
{  
    if(pRoot == NULL) // 空树,返回真  
    {  
        height = 0;  
        return true;  
    }  
    int heightLeft;  
    bool resultLeft = IsAVL(pRoot->m_pLeft, heightLeft);  
    int heightRight;  
    bool resultRight = IsAVL(pRoot->m_pRight, heightRight);  
    if(resultLeft && resultRight && abs(heightLeft - heightRight) <= 1) // 左子树和右子树都是AVL,并且高度相差不大于1,返回真  
   {  
        height = max(heightLeft, heightRight) + 1;  
        return true;  
    }  
    else  
    {  
        height = max(heightLeft, heightRight) + 1;  
        return false;  
    }  
}  

十、 求二叉树的镜像
递归解法:
(1)如果二叉树为空,返回空
(2)如果二叉树不为空,求左子树和右子树的镜像,然后交换左子树和右子树
参考代码如下:

BinaryTreeNode * Mirror(BinaryTreeNode * pRoot)  
{  
    if(pRoot == NULL) // 返回NULL  
        return NULL;  
    BinaryTreeNode * pLeft = Mirror(pRoot->m_pLeft); // 求左子树镜像  
    BinaryTreeNode * pRight = Mirror(pRoot->m_pRight); // 求右子树镜像  
        // 交换左子树和右子树   
   pRoot->m_pLeft = pRight;  
    pRoot->m_pRight = pLeft;  
    return pRoot;  
} 

十一、 求二叉树中两个节点的最低公共祖先节点
递归解法:
(1)如果两个节点分别在根节点的左子树和右子树,则返回根节点
(2)如果两个节点都在左子树,则递归处理左子树;如果两个节点都在右子树,则递归处理右子树
参考代码如下:

bool FindNode(BinaryTreeNode * pRoot, BinaryTreeNode * pNode)  
{  
    if(pRoot == NULL || pNode == NULL)  
        return false;  
  
    if(pRoot == pNode)  
        return true;    
    bool found = FindNode(pRoot->m_pLeft, pNode);  
    if(!found)  
        found = FindNode(pRoot->m_pRight, pNode);  
  
    return found;  
}  
BinaryTreeNode * GetLastCommonParent(BinaryTreeNode * pRoot,   
       BinaryTreeNode * pNode1,   
       BinaryTreeNode * pNode2)  
{  
    if(FindNode(pRoot->m_pLeft, pNode1))  
    {  
        if(FindNode(pRoot->m_pRight, pNode2))  
            return pRoot;  
        else  
            return GetLastCommonParent(pRoot->m_pLeft, pNode1, pNode2);  
    }  
    else  
    {  
        if(FindNode(pRoot->m_pLeft, pNode2))  
            return pRoot;  
        else  
            return GetLastCommonParent(pRoot->m_pRight, pNode1, pNode2);  
    }  
}  

递归解法效率很低,有很多重复的遍历,下面看一下非递归解法。
非递归解法:
先求从根节点到两个节点的路径,然后再比较对应路径的节点就行,最后一个相同的节点也就是他们在二叉树中的最低公共祖先节点
参考代码如下:

bool GetNodePath(BinaryTreeNode * pRoot, BinaryTreeNode * pNode,   
                 list & path)  
{  
    if(pRoot == pNode)  
        return true;  
    if(pRoot == NULL)  
        return false;  
    path.push_back(pRoot);  
    bool found = false;  
    found = GetNodePath(pRoot->m_pLeft, pNode, path);  
    if(!found)  
        found = GetNodePath(pRoot->m_pRight, pNode, path);  
    if(!found)  
        path.pop_back();  
    return found;  
}  
BinaryTreeNode * GetLastCommonParent(BinaryTreeNode * pRoot,   
                                     BinaryTreeNode * pNode1,   
                                     BinaryTreeNode * pNode2)  
{  
   if(pRoot == NULL || pNode1 == NULL || pNode2 == NULL)  
       return NULL;  
 
   list path1;  
    GetNodePath(pRoot, pNode1, path1);  
    list path2;  
    GetNodePath(pRoot, pNode2, path2);  
 
    BinaryTreeNode * pLast = NULL;  
   list::const_iterator iter1 = path1.begin();  
    list::const_iterator iter2 = path2.begin();  
   while(iter1 != path1.end() && iter2 != path2.end())  
    {  
       if(*iter1 == *iter2)  
            pLast = *iter1;  
        else  
            break;  
        iter1++;  
        iter2++;  
    }  
  
   return pLast;  
}  

在上述算法的基础上稍加变化即可求二叉树中任意两个节点的距离了。

十二、 求二叉树中节点的最大距离

即二叉树中相距最远的两个节点之间的距离。
递归解法:
(1)如果二叉树为空,返回0,同时记录左子树和右子树的深度,都为0
(2)如果二叉树不为空,最大距离要么是左子树中的最大距离,要么是右子树中的最大距离,要么是左子树节点中到根节点的最大距离+右子树节点中到根节点的最大距离,同时记录左子树和右子树节点中到根节点的最大距离。
参考代码如下:

int GetMaxDistance(BinaryTreeNode * pRoot, int & maxLeft, int & maxRight)  
{  
    // maxLeft, 左子树中的节点距离根节点的最远距离  
    // maxRight, 右子树中的节点距离根节点的最远距离  
    if(pRoot == NULL)  
    {  
        maxLeft = 0;  
        maxRight = 0;  
        return 0;  
    }  
    int maxLL, maxLR, maxRL, maxRR;  
    int maxDistLeft, maxDistRight;  
    if(pRoot->m_pLeft != NULL)  
    {  
        maxDistLeft = GetMaxDistance(pRoot->m_pLeft, maxLL, maxLR);  
        maxLeft = max(maxLL, maxLR) + 1;  
    }  
    else  
    {  
        maxDistLeft = 0;  
        maxLeft = 0;  
    }  
    if(pRoot->m_pRight != NULL)  
    {  
        maxDistRight = GetMaxDistance(pRoot->m_pRight, maxRL, maxRR);  
        maxRight = max(maxRL, maxRR) + 1;  
    }  
    else  
    {  
        maxDistRight = 0;  
        maxRight = 0;  
    }  
    return max(max(maxDistLeft, maxDistRight), maxLeft+maxRight);  
}  

十三、 由前序遍历序列和中序遍历序列重建二叉树
二叉树前序遍历序列中,第一个元素总是树的根节点的值。中序遍历序列中,左子树的节点的值位于根节点的值的左边,右子树的节点的值位于根节点的值的右边。
递归解法:
(1)如果前序遍历为空或中序遍历为空或节点个数小于等于0,返回NULL。
(2)创建根节点。前序遍历的第一个数据就是根节点的数据,在中序遍历中找到根节点的位置,可分别得知左子树和右子树的前序和中序遍历序列,重建左右子树。

BinaryTreeNode * RebuildBinaryTree(int* pPreOrder, int* pInOrder, int nodeNum)  
{  
    if(pPreOrder == NULL || pInOrder == NULL || nodeNum <= 0)  
        return NULL;  
    BinaryTreeNode * pRoot = new BinaryTreeNode;  
    // 前序遍历的第一个数据就是根节点数据  
    pRoot->m_nValue = pPreOrder[0];  
    pRoot->m_pLeft = NULL;  
    pRoot->m_pRight = NULL;  
    // 查找根节点在中序遍历中的位置,中序遍历中,根节点左边为左子树,右边为右子树  
    int rootPositionInOrder = -1;  
    for(int i = 0; i < nodeNum; i++)  
        if(pInOrder[i] == pRoot->m_nValue)  
        {        
      rootPositionInOrder = i;        
      break;  
        }  
    if(rootPositionInOrder == -1)  
    {  
        throw std::exception("Invalid input.");  
    }  
    // 重建左子树  
    int nodeNumLeft = rootPositionInOrder;  
    int * pPreOrderLeft = pPreOrder + 1;  
    int * pInOrderLeft = pInOrder;  
    pRoot->m_pLeft = RebuildBinaryTree(pPreOrderLeft, pInOrderLeft, nodeNumLeft);  
    // 重建右子树  
    int nodeNumRight = nodeNum - nodeNumLeft - 1;  
    int * pPreOrderRight = pPreOrder + 1 + nodeNumLeft;  
    int * pInOrderRight = pInOrder + nodeNumLeft + 1;  
    pRoot->m_pRight = RebuildBinaryTree(pPreOrderRight, pInOrderRight, nodeNumRight);  
    return pRoot;  
}  

同样,有中序遍历序列和后序遍历序列,类似的方法可重建二叉树,但前序遍历序列和后序遍历序列不同恢复一棵二叉树,证明略。

十四、 判断二叉树是不是完全二叉树

若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
有如下算法,按层次(从上到下,从左到右)遍历二叉树,当遇到一个节点的左子树为空时,则该节点右子树必须为空,且后面遍历的节点左右子树都必须为空,否则不是完全二叉树。

1.bool IsCompleteBinaryTree(BinaryTreeNode * pRoot)  
2.{  
3.    if(pRoot == NULL)  
4.        return false;  
5.    queue q;  
6.    q.push(pRoot);  
7.    bool mustHaveNoChild = false;  
8.    bool result = true;  
9.    while(!q.empty())  
10.    {  
11.        BinaryTreeNode * pNode = q.front();  
12.        q.pop();  
13.        if(mustHaveNoChild) // 已经出现了有空子树的节点了,后面出现的必须为叶节点(左右子树都为空)  
14.        {  
15.            if(pNode->m_pLeft != NULL || pNode->m_pRight != NULL)  
16.            {  
17.                result = false;  
18.                break;  
19.            }  
20.        }  
21.        else  
22.        {  
23.            if(pNode->m_pLeft != NULL && pNode->m_pRight != NULL)  
24.            {  
25.                q.push(pNode->m_pLeft);  
26.                q.push(pNode->m_pRight);  
27.            }  
28.            else if(pNode->m_pLeft != NULL && pNode->m_pRight == NULL)  
29.            {  
30.                mustHaveNoChild = true;  
31.                q.push(pNode->m_pLeft);  
32.            }  
33.            else if(pNode->m_pLeft == NULL && pNode->m_pRight != NULL)  
34.            {  
35.                result = false;  
36.                break;  
37.            }  
38.        }  
39.    }  
40.    return result;  
41.}  

参考

http://www.cnblogs.com/10jschen/archive/2012/08/29/2662942.html

Golang之包系统

Golang之包系统

任何包系统设计的目的都是为了简化大型程序的设计和维护工作,通过将一组相关的特性放进一个独立的单元以便于理解和更新,在每个单元更新的同时保持和程序中其它单元的相对独立性。这种模块化的特性允许每个包可以被其它的不同项目共享和重用,在项目范围内、甚至全球范围统一的分发和复用。

每个包一般都定义了一个不同的名字空间用于它内部的每个标识符的访问。每个名字空间关联到一个特定的包,让我们给类型、函数等选择简短明了的名字,这样可以避免在我们使用它们的时候减少和其它部分名字的冲突。

每个包还通过控制包内名字的可见性和是否导出来实现封装特性。通过限制包成员的可见性并隐藏包API的具体实现,将允许包的维护者在不影响外部包用户的前提下调整包的内部实现。通过限制包内变量的可见性,还可以强制用户通过某些特定函数来访问和更新内部变量,这样可以保证内部变量的一致性和并发时的互斥约束。

当我们修改了一个源文件,我们必须重新编译该源文件对应的包和所有依赖该包的其他包。即使是从头构建,Go语言编译器的编译速度也明显快于其它编译语言。Go语言的闪电般的编译速度主要得益于三个语言特性。第一点,所有导入的包必须在每个文件的开头显式声明,这样的话编译器就没有必要读取和分析整个源文件来判断包的依赖关系。第二点,禁止包的环状依赖,因为没有循环依赖,包的依赖关系形成一个有向无环图,每个包可以被独立编译,而且很可能是被并发编译。第三点,编译后包的目标文件不仅仅记录包本身的导出信息,目标文件同时还记录了包的依赖关系。因此,在编译一个包的时候,编译器只需要读取每个直接导入包的目标文件,而不需要遍历所有依赖的的文件(译注:很多都是重复的间接依赖)。

导入路径

每个包是由一个全局唯一的字符串所标识的导入路径定位。出现在import语句中的导入路径也是字符串。

import (
    "fmt"
    "math/rand"
    "encoding/json"

    "golang.org/x/net/html"

    "github.com/go-sql-driver/mysql"
)

如果你计划分享或发布包,那么导入路径最好是全球唯一的。为了避免冲突,所有非标准库包的导入路径建议以所在组织的互联网域名为前缀;而且这样也有利于包的检索。例如,上面的import语句导入了Go团队维护的HTML解析器和一个流行的第三方维护的MySQL驱动。

如果我们想同时导入两个有着名字相同的包,例如math/rand包和crypto/rand包,那么导入声明必须至少为一个同名包指定一个新的包名以避免冲突。这叫做导入包的重命名。

import (
    "crypto/rand"
    mrand "math/rand" // alternative name mrand avoids conflict
)

导入包的重命名只影响当前的源文件。其它的源文件如果导入了相同的包,可以用导入包原本默认的名字或重命名为另一个完全不同的名字。

导入包重命名是一个有用的特性,它不仅仅只是为了解决名字冲突。如果导入的一个包名很笨重,特别是在一些自动生成的代码中,这时候用一个简短名称会更方便。选择用简短名称重命名导入包时候最好统一,以避免包名混乱。选择另一个包名称还可以帮助避免和本地普通变量名产生冲突。例如,如果文件中已经有了一个名为path的变量,那么我们可以将"path"标准包重命名为pathpkg。

每个导入声明语句都明确指定了当前包和被导入包之间的依赖关系。如果遇到包循环导入的情况,Go语言的构建工具将报告错误。

参考

https://docs.ruanjiadeng.com/gopl-zh/ch10/ch10-01.html

Golang查看Goroutine的运行状况

使用runtime/pprof中的pprof.Lookup查看Goroutine的运行状况

自从接触golang以来,我就对被神化已久的goroutine十分敬畏,一直都在小心翼翼的使用它来开发高并发程序,但是说实话自己对goroutine的了解也仅仅局限于使用层面。对于内部实现和运行状态的监控了解甚少,今天在阅读大牛博客的时候突然方法pprof这一神器,特记录下来。

package main
import (
    "runtime/pprof"
    "net/http"
)
 var quit chan struct{} = make(chan struct{})
func test(){
    <- quit
}
func handler(w http.ResponseWriter,r *http.Request){
    w.Header().Set("Content-type","text/plain")
    
    p:=pprof.Lookup("goroutine")
    p.WriteTo(w,1)
}
func main(){
    for i:=0;i<20000;i++{
        go test()
    }
    http.HandleFunc("/",handler)
    http.ListenAndServe(":8787",nil)
}

在浏览器中访问localhoost:8787,可以看到屏幕中打印了下面的信息,记录了goroutine的运行状态:

goroutine profile: total 20007
1 @ 0x4454dd 0x445274 0x4412fc 0x40112f 0x459f88 0x45b724 0x45c0b1 0x459abe 0x43fd01
#   0x4454dd    runtime/pprof.writeRuntimeProfile+0xdd  c:/go/src/runtime/pprof/pprof.go:540
#   0x445274    runtime/pprof.writeGoroutine+0xa4   c:/go/src/runtime/pprof/pprof.go:502
#   0x4412fc    runtime/pprof.(*Profile).WriteTo+0xdc   c:/go/src/runtime/pprof/pprof.go:229
#   0x40112f    main.handler+0xdf           C:/mygo/src/act/main.go:15
#   0x459f88    net/http.HandlerFunc.ServeHTTP+0x48 c:/go/src/net/http/server.go:1265
#   0x45b724    net/http.(*ServeMux).ServeHTTP+0x184    c:/go/src/net/http/server.go:1541
#   0x45c0b1    net/http.serverHandler.ServeHTTP+0x1a1  c:/go/src/net/http/server.go:1703
#   0x459abe    net/http.(*conn).serve+0xb5e        c:/go/src/net/http/server.go:1204

1 @ 0x4151ac 0x411b97 0x410fff 0x4cd66e 0x4ce935 0x4d1aa7 0x4d1eca 0x4e7085 0x45cf23 0x45c329 0x45c25b 0x45c861 0x4011ea 0x414eda 0x43fd01
#   0x4cd66e    net.(*pollDesc).Wait+0x4e           c:/go/src/net/fd_poll_runtime.go:84
#   0x4ce935    net.(*ioSrv).ExecIO+0x305           c:/go/src/net/fd_windows.go:188
#   0x4d1aa7    net.(*netFD).acceptOne+0x547            c:/go/src/net/fd_windows.go:558
#   0x4d1eca    net.(*netFD).accept+0x17a           c:/go/src/net/fd_windows.go:585
#   0x4e7085    net.(*TCPListener).AcceptTCP+0x55       c:/go/src/net/tcpsock_posix.go:234
#   0x45cf23    net/http.tcpKeepAliveListener.Accept+0x53   c:/go/src/net/http/server.go:1976
#   0x45c329    net/http.(*Server).Serve+0x99           c:/go/src/net/http/server.go:1728
#   0x45c25b    net/http.(*Server).ListenAndServe+0x15b     c:/go/src/net/http/server.go:1718
#   0x45c861    net/http.ListenAndServe+0xc1            c:/go/src/net/http/server.go:1808
#   0x4011ea    main.main+0xaa                  C:/mygo/src/act/main.go:22
#   0x414eda    runtime.main+0xfa               c:/go/src/runtime/proc.go:63

1 @ 0x4151ac 0x41521f 0x415045 0x43fd01
#   0x4151ac    runtime.gopark+0x10c        c:/go/src/runtime/proc.go:131
#   0x41521f    runtime.goparkunlock+0x4f   c:/go/src/runtime/proc.go:136
#   0x415045    runtime.forcegchelper+0xd5  c:/go/src/runtime/proc.go:99

1 @ 0x4151ac 0x41521f 0x40e883 0x43fd01
#   0x4151ac    runtime.gopark+0x10c        c:/go/src/runtime/proc.go:131
#   0x41521f    runtime.goparkunlock+0x4f   c:/go/src/runtime/proc.go:136
#   0x40e883    runtime.bgsweep+0xc3        c:/go/src/runtime/mgc0.go:98

1 @ 0x4151ac 0x41521f 0x40de71 0x43fd01
#   0x4151ac    runtime.gopark+0x10c        c:/go/src/runtime/proc.go:131
#   0x41521f    runtime.goparkunlock+0x4f   c:/go/src/runtime/proc.go:136
#   0x40de71    runtime.runfinq+0xc1        c:/go/src/runtime/malloc.go:727

20000 @ 0x4151ac 0x41521f 0x403939 0x40349b 0x401046 0x43fd01
#   0x401046    main.test+0x46  C:/mygo/src/act/main.go:9

2 @ 0x4151ac 0x411b97 0x410fff 0x4cd66e 0x4ce935 0x4d0d60 0x4e0c83 0x4536f2 0x496e25 0x477295 0x47809e 0x478119 0x50dd35 0x50da86 0x44e9e2 0x455e66 0x459605 0x43fd01
#   0x4cd66e    net.(*pollDesc).Wait+0x4e           c:/go/src/net/fd_poll_runtime.go:84
#   0x4ce935    net.(*ioSrv).ExecIO+0x305           c:/go/src/net/fd_windows.go:188
#   0x4d0d60    net.(*netFD).Read+0x180             c:/go/src/net/fd_windows.go:470
#   0x4e0c83    net.(*conn).Read+0xe3               c:/go/src/net/net.go:121
#   0x4536f2    net/http.(*liveSwitchReader).Read+0xb2      c:/go/src/net/http/server.go:214
#   0x496e25    io.(*LimitedReader).Read+0xd5           c:/go/src/io/io.go:408
#   0x477295    bufio.(*Reader).fill+0x1d5          c:/go/src/bufio/bufio.go:97
#   0x47809e    bufio.(*Reader).ReadSlice+0x25e         c:/go/src/bufio/bufio.go:295
#   0x478119    bufio.(*Reader).ReadLine+0x69           c:/go/src/bufio/bufio.go:324
#   0x50dd35    net/textproto.(*Reader).readLineSlice+0xa5  c:/go/src/net/textproto/reader.go:55
#   0x50da86    net/textproto.(*Reader).ReadLine+0x56       c:/go/src/net/textproto/reader.go:36
#   0x44e9e2    net/http.ReadRequest+0xd2           c:/go/src/net/http/request.go:598
#   0x455e66    net/http.(*conn).readRequest+0x276      c:/go/src/net/http/server.go:586
#   0x459605    net/http.(*conn).serve+0x6a5            c:/go/src/net/http/server.go:1162

第一行记录的是正在运行的goroutines的数目,这正是我想要知道的:)

Goroutine背后的系统知识

goroutine背后的系统知识

这篇文章初衷是希望能为比较缺少系统编程背景的Web开发人员介绍一下goroutine背后的系统知识。

  1. 操作系统与运行库
  2. 并发与并行 (Concurrency and Parallelism)
  3. 线程的调度
  4. 并发编程框架
  5. goroutine

一 、 操作系统与运行库

对于普通的电脑用户来说,能理解应用程序是运行在操作系统之上就足够了,可对于开发者,我们还需要了解我们写的程序是如何在操作系统之上运行起来的,操作系统如何为应用程序提供服务,这样我们才能分清楚哪些服务是操作系统提供的,而哪些服务是由我们所使用的语言的运行库提供的。

除了内存管理、文件管理、进程管理、外设管理等等内部模块以外,操作系统还提供了许多外部接口供应用程序使用,这些接口就是所谓的“系统调用”。从DOS时代开始,系统调用就是通过软中断的形式来提供,也就是著名的INT 21,程序把需要调用的功能编号放入AH寄存器,把参数放入其他指定的寄存器,然后调用INT 21,中断返回后,程序从指定的寄存器(通常是AL)里取得返回值。这样的做法一直到奔腾2也就是P6出来之前都没有变,譬如windows通过INT 2E提供系统调用,Linux则是INT 80,只不过后来的寄存器比以前大一些,而且可能再多一层跳转表查询。后来,Intel和AMD分别提供了效率更高的SYSENTER/SYSEXIT和SYSCALL/SYSRET指令来代替之前的中断方式,略过了耗时的特权级别检查以及寄存器压栈出栈的操作,直接完成从RING 3代码段到RING 0代码段的转换。

系统调用都提供什么功能呢?用操作系统的名字加上对应的中断编号到谷歌上一查就可以得到完整的列表 (Windows, Linux),这个列表就是操作系统和应用程序之间沟通的协议,如果需要超出此协议的功能,我们就只能在自己的代码里去实现,譬如,对于内存管理,操作系统只提供进程级别的内存段的管理,譬如Windows的virtualmemory系列,或是Linux的brk,操作系统不会去在乎应用程序如何为新建对象分配内存,或是如何做垃圾回收,这些都需要应用程序自己去实现。如果超出此协议的功能无法自己实现,那我们就说该操作系统不支持该功能,举个例子,Linux在2.6之前是不支持多线程的,无论如何在程序里模拟,我们都无法做出多个可以同时运行的并符合POSIX 1003.1c语义标准的调度单元。

可是,我们写程序并不需要去调用中断或是SYSCALL指令,这是因为操作系统提供了一层封装,在Windows上,它是NTDLL.DLL,也就是常说的Native API,我们不但不需要去直接调用INT 2E或SYSCALL,准确的说,我们不能直接去调用INT 2E或SYSCALL,因为Windows并没有公开其调用规范,直接使用INT 2E或SYSCALL无法保证未来的兼容性。在Linux上则没有这个问题,系统调用的列表都是公开的,而且Linus非常看重兼容性,不会去做任何更改,glibc里甚至专门提供了syscall(2)来方便用户直接用编号调用,不过,为了解决glibc和内核之间不同版本兼容性带来的麻烦,以及为了提高某些调用的效率(譬如__NR_ gettimeofday),Linux上还是对部分系统调用做了一层封装,就是VDSO (早期叫linux-gate.so)。

可是,我们写程序也很少直接调用NTDLL或者VDSO,而是通过更上一层的封装,这一层处理了参数准备和返回值格式转换、以及出错处理和错误代码转换,这就是我们所使用语言的运行库,对于C语言,Linux上是glibc,Windows上是kernel32(或调用msvcrt),对于其他语言,譬如Java,则是JRE,这些“其他语言”的运行库通常最终还是调用glibc或kernel32。

“运行库”这个词其实不止包括用于和编译后的目标执行程序进行链接的库文件,也包括了脚本语言或字节码解释型语言的运行环境,譬如Python,C#的CLR,Java的JRE。

对系统调用的封装只是运行库的很小一部分功能,运行库通常还提供了诸如字符串处理、数学计算、常用数据结构容器等等不需要操作系统支持的功能,同时,运行库也会对操作系统支持的功能提供更易用更高级的封装,譬如带缓存和格式的IO、线程池。

所以,在我们说“某某语言新增了某某功能”的时候,通常是这么几种可能:

  1. 支持新的语义或语法,从而便于我们描述和解决问题。譬如Java的泛型、Annotation、lambda表达式。
  2. 提供了新的工具或类库,减少了我们开发的代码量。譬如Python 2.7的argparse
  3. 对系统调用有了更良好更全面的封装,使我们可以做到以前在这个语言环境里做不到或很难做到的事情。譬如Java NIO

但任何一门语言,包括其运行库和运行环境,都不可能创造出操作系统不支持的功能,Go语言也是这样,不管它的特性描述看起来多么炫丽,那必然都是其他语言也可以做到的,只不过Go提供了更方便更清晰的语义和支持,提高了开发的效率。

二、并发与并行 (Concurrency and Parallelism)

并发是指程序的逻辑结构。非并发的程序就是一根竹竿捅到底,只有一个逻辑控制流,也就是顺序执行的(Sequential)程序,在任何时刻,程序只会处在这个逻辑控制流的某个位置。而如果某个程序有多个独立的逻辑控制流,也就是可以同时处理(deal)多件事情,我们就说这个程序是并发的。这里的“同时”,并不一定要是真正在时钟的某一时刻(那是运行状态而不是逻辑结构),而是指:如果把这些逻辑控制流画成时序流程图,它们在时间线上是可以重叠的。

并行是指程序的运行状态。如果一个程序在某一时刻被多个CPU流水线同时进行处理,那么我们就说这个程序是以并行的形式在运行。(严格意义上讲,我们不能说某程序是“并行”的,因为“并行”不是描述程序本身,而是描述程序的运行状态,但这篇小文里就不那么咬文嚼字,以下说到“并行”的时候,就是指代“以并行的形式运行”)显然,并行一定是需要硬件支持的。

而且不难理解:

  1. 并发是并行的必要条件,如果一个程序本身就不是并发的,也就是只有一个逻辑控制流,那么我们不可能让其被并行处理。
  2. 并发不是并行的充分条件,一个并发的程序,如果只被一个CPU流水线进行处理(通过分时),那么它就不是并行的。
  3. 并发只是更符合现实问题本质的表达方式,并发的最初目的是简化代码逻辑,而不是使程序运行的更快;

这几段略微抽象,我们可以用一个最简单的例子来把这些概念实例化:用C语言写一个最简单的HelloWorld,它就是非并发的,如果我们建立多个线程,每个线程里打印一个HelloWorld,那么这个程序就是并发的,如果这个程序运行在老式的单核CPU上,那么这个并发程序还不是并行的,如果我们用多核多CPU且支持多任务的操作系统来运行它,那么这个并发程序就是并行的。

还有一个略微复杂的例子,更能说明并发不一定可以并行,而且并发不是为了效率,就是Go语言例子里计算素数的sieve.go。我们从小到大针对每一个因子启动一个代码片段,如果当前验证的数能被当前因子除尽,则该数不是素数,如果不能,则把该数发送给下一个因子的代码片段,直到最后一个因子也无法除尽,则该数为素数,我们再启动一个它的代码片段,用于验证更大的数字。这是符合我们计算素数的逻辑的,而且每个因子的代码处理片段都是相同的,所以程序非常的简洁,但它无法被并行,因为每个片段都依赖于前一个片段的处理结果和输出。

并发可以通过以下方式做到:

  1. 显式地定义并触发多个代码片段,也就是逻辑控制流,由应用程序或操作系统对它们进行调度。它们可以是独立无关的,也可以是相互依赖需要交互的,譬如上面提到的素数计算,其实它也是个经典的生产者和消费者的问题:两个逻辑控制流A和B,A产生输出,当有了输出后,B取得A的输出进行处理。线程只是实现并发的其中一个手段,除此之外,运行库或是应用程序本身也有多种手段来实现并发,这是下节的主要内容。
  2. 隐式地放置多个代码片段,在系统事件发生时触发执行相应的代码片段,也就是事件驱动的方式,譬如某个端口或管道接收到了数据(多路IO的情况下),再譬如进程接收到了某个信号(signal)。

并行可以在四个层面上做到:

  1. 多台机器。自然我们就有了多个CPU流水线,譬如Hadoop集群里的MapReduce任务。
  2. 多CPU。不管是真的多颗CPU还是多核还是超线程,总之我们有了多个CPU流水线。
  3. 单CPU核里的ILP(Instruction-level parallelism),指令级并行。通过复杂的制造工艺和对指令的解析以及分支预测和乱序执行,现在的CPU可以在单个时钟周期内执行多条指令,从而,即使是非并发的程序,也可能是以并行的形式执行。
  4. 单指令多数据(Single instruction, multiple data. SIMD),为了多媒体数据的处理,现在的CPU的指令集支持单条指令对多条数据进行操作。

其中,1牵涉到分布式处理,包括数据的分布和任务的同步等等,而且是基于网络的。3和4通常是编译器和CPU的开发人员需要考虑的。这里我们说的并行主要针对第2种:单台机器内的多核CPU并行。

关于并发与并行的问题,Go语言的作者Rob Pike专门就此写过一个幻灯片:http://talks.golang.org/2012/waza.slide

在CMU那本著名的《Computer Systems: A Programmer’s Perspective》里的这张图也非常直观清晰:

三、 线程的调度

上一节主要说的是并发和并行的概念,而线程是最直观的并发的实现,这一节我们主要说操作系统如何让多个线程并发的执行,当然在多CPU的时候,也就是并行的执行。我们不讨论进程,进程的意义是“隔离的执行环境”,而不是“单独的执行序列”。

我们首先需要理解IA-32 CPU的指令控制方式,这样才能理解如何在多个指令序列(也就是逻辑控制流)之间进行切换。CPU通过CS:EIP寄存器的值确定下一条指令的位置,但是CPU并不允许直接使用MOV指令来更改EIP的值,必须通过JMP系列指令、CALL/RET指令、或INT中断指令来实现代码的跳转;在指令序列间切换的时候,除了更改EIP之外,我们还要保证代码可能会使用到的各个寄存器的值,尤其是栈指针SS:ESP,以及EFLAGS标志位等,都能够恢复到目标指令序列上次执行到这个位置时候的状态。

线程是操作系统对外提供的服务,应用程序可以通过系统调用让操作系统启动线程,并负责随后的线程调度和切换。我们先考虑单颗单核CPU,操作系统内核与应用程序其实是也是在共享同一个CPU,当EIP在应用程序代码段的时候,内核并没有控制权,内核并不是一个进程或线程,内核只是以实模式运行的,代码段权限为RING 0的内存中的程序,只有当产生中断或是应用程序呼叫系统调用的时候,控制权才转移到内核,在内核里,所有代码都在同一个地址空间,为了给不同的线程提供服务,内核会为每一个线程建立一个内核堆栈,这是线程切换的关键。通常,内核会在时钟中断里或系统调用返回前(考虑到性能,通常是在不频繁发生的系统调用返回前),对整个系统的线程进行调度,计算当前线程的剩余时间片,如果需要切换,就在“可运行”的线程队列里计算优先级,选出目标线程后,则保存当前线程的运行环境,并恢复目标线程的运行环境,其中最重要的,就是切换堆栈指针ESP,然后再把EIP指向目标线程上次被移出CPU时的指令。Linux内核在实现线程切换时,耍了个花枪,它并不是直接JMP,而是先把ESP切换为目标线程的内核栈,把目标线程的代码地址压栈,然后JMP到__switch_to(),相当于伪造了一个CALL __switch_to()指令,然后,在__switch_to()的最后使用RET指令返回,这样就把栈里的目标线程的代码地址放入了EIP,接下来CPU就开始执行目标线程的代码了,其实也就是上次停在switch_to这个宏展开的地方。

这里需要补充几点:(1) 虽然IA-32提供了TSS (Task State Segment),试图简化操作系统进行线程调度的流程,但由于其效率低下,而且并不是通用标准,不利于移植,所以主流操作系统都没有去利用TSS。更严格的说,其实还是用了TSS,因为只有通过TSS才能把堆栈切换到内核堆栈指针SS0:ESP0,但除此之外的TSS的功能就完全没有被使用了。(2) 线程从用户态进入内核的时候,相关的寄存器以及用户态代码段的EIP已经保存了一次,所以,在上面所说的内核态线程切换时,需要保存和恢复的内容并不多。(3) 以上描述的都是抢占式(preemptively)的调度方式,内核以及其中的硬件驱动也会在等待外部资源可用的时候主动调用schedule(),用户态的代码也可以通过sched_yield()系统调用主动发起调度,让出CPU。

现在我们一台普通的PC或服务里通常都有多颗CPU (physical package),每颗CPU又有多个核 (processor core),每个核又可以支持超线程 (two logical processors for each core),也就是逻辑处理器。每个逻辑处理器都有自己的一套完整的寄存器,其中包括了CS:EIP和SS:ESP,从而,以操作系统和应用的角度来看,每个逻辑处理器都是一个单独的流水线。在多处理器的情况下,线程切换的原理和流程其实和单处理器时是基本一致的,内核代码只有一份,当某个CPU上发生时钟中断或是系统调用时,该CPU的CS:EIP和控制权又回到了内核,内核根据调度策略的结果进行线程切换。但在这个时候,如果我们的程序用线程实现了并发,那么操作系统可以使我们的程序在多个CPU上实现并行。

这里也需要补充两点:(1) 多核的场景里,各个核之间并不是完全对等的,譬如在同一个核上的两个超线程是共享L1/L2缓存的;在有NUMA支持的场景里,每个核访问内存不同区域的延迟是不一样的;所以,多核场景里的线程调度又引入了“调度域”(scheduling domains)的概念,但这不影响我们理解线程切换机制。(2) 多核的场景下,中断发给哪个CPU?软中断(包括除以0,缺页异常,INT指令)自然是在触发该中断的CPU上产生,而硬中断则又分两种情况,一种是每个CPU自己产生的中断,譬如时钟,这是每个CPU处理自己的,还有一种是外部中断,譬如IO,可以通过APIC来指定其送给哪个CPU;因为调度程序只能控制当前的CPU,所以,如果IO中断没有进行均匀的分配的话,那么和IO相关的线程就只能在某些CPU上运行,导致CPU负载不均,进而影响整个系统的效率。

四、并发编程框架

以上大概介绍了一个用多线程来实现并发的程序是如何被操作系统调度以及并行执行(在有多个逻辑处理器时),同时大家也可以看到,代码片段或者说逻辑控制流的调度和切换其实并不神秘,理论上,我们也可以不依赖操作系统和其提供的线程,在自己程序的代码段里定义多个片段,然后在我们自己程序里对其进行调度和切换。

为了描述方便,我们接下来把“代码片段”称为“任务”。

和内核的实现类似,只是我们不需要考虑中断和系统调用,那么,我们的程序本质上就是一个循环,这个循环本身就是调度程序schedule(),我们需要维护一个任务的列表,根据我们定义的策略,先进先出或是有优先级等等,每次从列表里挑选出一个任务,然后恢复各个寄存器的值,并且JMP到该任务上次被暂停的地方,所有这些需要保存的信息都可以作为该任务的属性,存放在任务列表里。

看起来很简单啊,可是我们还需要解决几个问题:

(1) 我们运行在用户态,是没有中断或系统调用这样的机制来打断代码执行的,那么,一旦我们的schedule()代码把控制权交给了任务的代码,我们下次的调度在什么时候发生?答案是,不会发生,只有靠任务主动调用schedule(),我们才有机会进行调度,所以,这里的任务不能像线程一样依赖内核调度从而毫无顾忌的执行,我们的任务里一定要显式的调用schedule(),这就是所谓的协作式(cooperative)调度。(虽然我们可以通过注册信号处理函数来模拟内核里的时钟中断并取得控制权,可问题在于,信号处理函数是由内核调用的,在其结束的时候,内核重新获得控制权,随后返回用户态并继续沿着信号发生时被中断的代码路径执行,从而我们无法在信号处理函数内进行任务切换)

(2) 堆栈。和内核调度线程的原理一样,我们也需要为每个任务单独分配堆栈,并且把其堆栈信息保存在任务属性里,在任务切换时也保存或恢复当前的SS:ESP。任务堆栈的空间可以是在当前线程的堆栈上分配,也可以是在堆上分配,但通常是在堆上分配比较好:几乎没有大小或任务总数的限制、堆栈大小可以动态扩展(gcc有split stack,但太复杂了)、便于把任务切换到其他线程。

到这里,我们大概知道了如何构造一个并发的编程框架,可如何让任务可以并行的在多个逻辑处理器上执行呢?只有内核才有调度CPU的权限,所以,我们还是必须通过系统调用创建线程,才可以实现并行。在多线程处理多任务的时候,我们还需要考虑几个问题:

(1) 如果某个任务发起了一个系统调用,譬如长时间等待IO,那当前线程就被内核放入了等待调度的队列,岂不是让其他任务都没有机会执行?

在单线程的情况下,我们只有一个解决办法,就是使用非阻塞的IO系统调用,并让出CPU,然后在schedule()里统一进行轮询,有数据时切换回该fd对应的任务;效率略低的做法是不进行统一轮询,让各个任务在轮到自己执行时再次用非阻塞方式进行IO,直到有数据可用。

如果我们采用多线程来构造我们整个的程序,那么我们可以封装系统调用的接口,当某个任务进入系统调用时,我们就把当前线程留给它(暂时)独享,并开启新的线程来处理其他任务。

(2) 任务同步。譬如我们上节提到的生产者和消费者的例子,如何让消费者在数据还没有被生产出来的时候进入等待,并且在数据可用时触发消费者继续执行呢?

在单线程的情况下,我们可以定义一个结构,其中有变量用于存放交互数据本身,以及数据的当前可用状态,以及负责读写此数据的两个任务的编号。然后我们的并发编程框架再提供read和write方法供任务调用,在read方法里,我们循环检查数据是否可用,如果数据还不可用,我们就调用schedule()让出CPU进入等待;在write方法里,我们往结构里写入数据,更改数据可用状态,然后返回;在schedule()里,我们检查数据可用状态,如果可用,则激活需要读取此数据的任务,该任务继续循环检测数据是否可用,发现可用,读取,更改状态为不可用,返回。代码的简单逻辑如下:

struct chan {
    bool ready,
    int data
};
int read (struct chan *c) {
    while (1) {
        if (c->ready) {
            c->ready = false;
            return c->data;
        } else {
            schedule();
        }
    }
}
void write (struct chan *c, int i) {
    while (1) {
        if (c->ready) {
            schedule(); 
        } else {
            c->data = i;
            c->ready = true;
            schedule(); // optional
            return;
        }
    }
}

很显然,如果是多线程的话,我们需要通过线程库或系统调用提供的同步机制来保护对这个结构体内数据的访问。

以上就是最简化的一个并发框架的设计考虑,在我们实际开发工作中遇到的并发框架可能由于语言和运行库的不同而有所不同,在功能和易用性上也可能各有取舍,但底层的原理都是殊途同归。

譬如,glic里的getcontext/setcontext/swapcontext系列库函数可以方便的用来保存和恢复任务执行状态;Windows提供了Fiber系列的SDK API;这二者都不是系统调用,getcontext和setcontext的man page虽然是在section 2,但那只是SVR4时的历史遗留问题,其实现代码是在glibc而不是kernel;CreateFiber是在kernel32里提供的,NTDLL里并没有对应的NtCreateFiber。

在其他语言里,我们所谓的“任务”更多时候被称为“协程”,也就是Coroutine。譬如C++里最常用的是Boost.Coroutine;Java因为有一层字节码解释,比较麻烦,但也有支持协程的JVM补丁,或是动态修改字节码以支持协程的项目;PHP和Python的generator和yield其实已经是协程的支持,在此之上可以封装出更通用的协程接口和调度;另外还有原生支持协程的Erlang等,笔者不懂,就不说了,具体可参见Wikipedia的页面:http://en.wikipedia.org/wiki/Coroutine

由于保存和恢复任务执行状态需要访问CPU寄存器,所以相关的运行库也都会列出所支持的CPU列表。

从操作系统层面提供协程以及其并行调度的,好像只有OS X和iOS的Grand Central Dispatch,其大部分功能也是在运行库里实现的。

五、 goroutine

Go语言通过goroutine提供了目前为止所有(我所了解的)语言里对于并发编程的最清晰最直接的支持,Go语言的文档里对其特性也描述的非常全面甚至超过了,在这里,基于我们上面的系统知识介绍,列举一下goroutine的特性,算是小结:

(1) goroutine是Go语言运行库的功能,不是操作系统提供的功能,goroutine不是用线程实现的。具体可参见Go语言源码里的pkg/runtime/proc.c

(2) goroutine就是一段代码,一个函数入口,以及在堆上为其分配的一个堆栈。所以它非常廉价,我们可以很轻松的创建上万个goroutine,但它们并不是被操作系统所调度执行

(3) 除了被系统调用阻塞的线程外,Go运行库最多会启动$GOMAXPROCS个线程来运行goroutine

(4) goroutine是协作式调度的,如果goroutine会执行很长时间,而且不是通过等待读取或写入channel的数据来同步的话,就需要主动调用Gosched()来让出CPU

(5) 和所有其他并发框架里的协程一样,goroutine里所谓“无锁”的优点只在单线程下有效,如果$GOMAXPROCS > 1并且协程间需要通信,Go运行库会负责加锁保护数据,这也是为什么sieve.go这样的例子在多CPU多线程时反而更慢的原因

(6) Web等服务端程序要处理的请求从本质上来讲是并行处理的问题,每个请求基本独立,互不依赖,几乎没有数据交互,这不是一个并发编程的模型,而并发编程框架只是解决了其语义表述的复杂性,并不是从根本上提高处理的效率,也许是并发连接和并发编程的英文都是concurrent吧,很容易产生“并发编程框架和coroutine可以高效处理大量并发连接”的误解。

(7) Go语言运行库封装了异步IO,所以可以写出貌似并发数很多的服务端,可即使我们通过调整$GOMAXPROCS来充分利用多核CPU并行处理,其效率也不如我们利用IO事件驱动设计的、按照事务类型划分好合适比例的线程池。在响应时间上,协作式调度是硬伤。

(8) goroutine最大的价值是其实现了并发协程和实际并行执行的线程的映射以及动态扩展,随着其运行库的不断发展和完善,其性能一定会越来越好,尤其是在CPU核数越来越多的未来,终有一天我们会为了代码的简洁和可维护性而放弃那一点点性能的差别。

参考

http://www.sizeofvoid.net/

Docker 持续集成过程中的性能问题及解决方法

Docker 持续集成过程中的性能问题及解决方法

Docker 的出现使开发测试生产环境的统一变得更加容易,然而在使用 docker 搭建这一整套流水线之后,却发现它运行的却不能像丝般润滑,总是感觉没有直接本地开发测试来的效率高。为了能达到一个高效流水般的持续构建,我们来看一下这个过程中 docker 的使用以及 docker 自身存在着哪些问题,我们又该如何克服这些问题,达到如丝般的润滑。

我们首先来分解一下现在常见的一种利用 docker 做持续部署的流程:

  • 开发者提交代码
  • 触发镜像构建
  • 构建镜像上传至私有仓库
  • 镜像下载至执行机器
  • 镜像运行

在这五步中,1 和 5 的耗时都比较短,主要耗时集中在中间 3 步,也就是 docker build, push, pull 的时间消耗,我们就来分别看一下如何加速这三个步骤。

Docker build

选择国外构建

由于 dockerhub 的官方镜像再国外,而这些基础镜像的软件源都在国外,国内构建的时候网络会是很大的瓶颈,有能力在国外机器进行构建,并且可以通过专线和国内进行传输的话,还是优先将构建节点放在国外,会省很多无谓的在网络上的纠缠,并且很多软件源国外的也要更稳定写,更新也更及时。

如果只能在国内进行构建的话,建议使用国内的镜像,或者自己在私有仓库存一份官方镜像,并且对镜像进行改造,做一份软件源都在国内的基础镜像,把构建过程中的网络传输都控制在国内或者内网,这样就不用和网络进行纠缠了。

善用 .dockerignore

.dockerignore 可以减少构建时的文件传输,一般通过 git 进行持续构建的时候不做设置都会把 .git 文件夹进行传输造成很多无用的传输,一些与构建无关的代码也尽量卸载 .dockerigonre 文件中。

缓存优化的 dockerfile

dockerfile 的优化也是一个比较直接的优化方式,优化的核心就是能充分利用 build cache,把每次变化的部分放在最后,一般把加入代码放在最后一步,这样每次构建只有最后一层是新的,其他部分都是可以用 cache 的。对于 node、python、go 之类要在构建过程中安装依赖的服务,可以把安装依赖和加入代码分两步完成,这样在依赖不变的情况下这部分的缓存也是可以利用的。以 node 为例:

COPY package.json /usr/src/app/
RUN npm install
COPY . /usr/src/app

其他关于 dockerfile 优化的建议可以再单独开一篇了,基本上每个命令都需要特殊对待才能不掉坑里,可以参考一个在线 dockerfile 语法优化器,里面会提供一些相关的 dockerfile 优化建议和一些资源,作者一定是个大好人。

smart cache

在单机模式下充分利用 build cache 是个不错的注意,但是在多个构建机器的情况下就会有问题了。出于磁盘空间考量不可能所有机器都存着所有的镜像,这样缓存优化的 dockerfile 就没有用武之地了。为了让 cache 重新发挥作用我们可以在构建开始时将旧的镜像 pull 下来,这样一来就可以再次利用 cache 了。但是一来 pull 镜像也是需要很多时间的,并且 pull 下来的镜像并不会全部有用,会浪费一定的时间;而来如果 dockerfile 变化比较大有可能没有一层能用 pull 下来反而会浪费更多的时间;三来仓库内可能会有其他的镜像更适合做当前构建的缓存所以我们需要实现一个精准的镜像拉取,不能出错也不能浪费。

Docker push

docker registry 在升级到 v2 后加入了很多安全相关检查,使得原来很多在 v1 already exist 的层依然要 push 到 registry,并且由于 v2 中的存储格式变成了 gzip,在镜像压缩过程中占用的时间很有可能比网络传输还要多。我们简单分解一下 docker push 的流程。

  • buffer to diske 将该层文件系统压缩成本地的一个临时文件
  • 上传文件至 registry
  • 本地计算压缩包 digest,删除临时文件,digest 传给 registry
  • registry 计算上传压缩包 digest 并进行校验
  • registry 将压缩包传输至后端存储文件系统
  • 重复 1-5 直至所有层传输完毕
  • 计算镜像的 manifest 并上传至 registry 重复

此外判断 already exist skip pushing 的条件变严格了,必须是本地计算过digest 且 该 digest 对应的文件属在对应 repo 存在才可以。

换句话说就是如果这个镜像层是 pull 下来的,那么是没有digest的还是要把整个压缩包传输并计算 digest,如果这个镜像你之前并没有比如 ubuntu 的 base image 你的 repo 第一次创建之前没传输过,那么第一次也要你传输一次确认你真的有 ubuntu。

这里面的改进点就是在太多了,先列举 Docker 官方已经做得和正在做的。

  • 1.9.1 后 push 是 streaming 式的,也就是把 1 和 2 合并去掉临时文件,直接一边压缩一边传输。
  • pull 镜像后 digest 保存,大概是 1.8.3 之后添加的省去了重复计算。
  • registry 可以直接 mount 别人 repo 中的一层到自己的 repo,只要有pull权限即可,这个工作还在进行中。

但是这只解决了一小部分问题,push 依然会很慢,docker 和 registry 的设计更多的考虑了公有云的环境设置了过多的安全防范为了防止镜像的伪造和越权获取,但是在一个可信的环境内如果 build 和 push 过程都是自己掌控的,那么很多措施都是多余的,我们可以设计一个自己的 smart pusher 挖掘性能的最大潜力:

  • 压缩传输 streaming 化和 docker 1.9.1 实现的类似
  • 越过 registry 直接和存储系统通信,直接拿掉上面 5 的传输时间
  • 如果 digest 在 存储系统中存在则不再重复传输,在 manifest 中写好层次关系就好
  • 将多层的传输并行化,多个层一块传,这样才能充分发挥多核的优势,docker 自带的串行push效率实在是太低了
  • 另外针对 build 结果进行 push 的 smart pusher 可以将流水线发挥到极致,build 每构建出一层就进行传输,将 build 和 push 的时间重叠利用

有了 smart pusher,push 时间的绝大多数都被隐藏到了 build 的时间中,我们把并发和流水线的技术都用上,充分发挥了多核的优势。

Docker pull

docker pull 镜像的速度对服务的启动速度至关重要,好在 registry v2 后可以并行 pull 了,速度有了很大的改善。但是依然有一些小的问题影响了启动的速度:

  • 下载镜像和解压镜像是串行的
  • 串行解压,由于 v2 都是 gzip 要解压,尽管并行下载了还是串行解压,内网的话解压时间比网络传输都要长
  • 和 registry 通信,registry 在 pull 的过程中并不提供下载内容只是提供下载 url 和鉴权,这一部分加长了网络传输而且一些 metadata 还是要去后端存储获取,延时还是有一些的
  • docker pull 某些情况会卡死,不 docker restart 很难解决,而 restart 又会停止所有服务,严重影响服务稳定性。

为此我们还需要一个独特设计的 smart puller 帮助我们解决最后的问题: 1. streaming downloading and extracting 和在 smarter pusher 做的类似将这两步合为一步 2. 并行解压,也和 smarter pusher 并行压缩类似 3. 越过 registry 直接和后端存储通信 4. redis 缓存 metadata

有了 smart puller 我们自然的将 docker pull 的工作和 docker daemon 解耦了,这样再不会发生 pull 导致的 docker hang,服务稳定性也得到了增强,解绑后其实 docker 只是做一个 runtime 这一部分也可考虑改成 runc 去除掉 daemon 这个单点,不过这个工作量就比较大了。此外 smart puller 也可以帮助我们实现在 smart cache 中的精确 pull 以及 pull cache 的加速,可谓一举多得。

总结

将 push 和 pull 的工作和 daemon 解绑,把 smart cache,smart puller 和 smart pusher 用上后,持续集成如丝般润滑。

参考

http://oilbeater.com/docker/2016/01/02/use-docker-performance-issue-and-solution.html

关于跨域攻击

关于跨域攻击

先吐为快:
一种是请求的参数中添加一些特殊的参数(比如token)来验证
另一种方式是直接在服务器端对POST请求做一个来路验证,HTTP头中的REFERER字段如果不是同域的就拒绝提交。

   在JavaScript中到处都是安全性限制,最典型的就是POST跨域问题。但是即使它有如此严密的安全性限制,一些东西依然可以被攻击者利用。跨域限制主要是为了防止数据被盗取,但是如果不盗取数据而只是攻击呢?或者说,只需要跨域POST而不需要接收返回的数据呢?

  如果不需要接收返回数据,跨域POST请求完全是可以在JavaScript中构造出来。只需要生成一个FORM,设置一些提交的参数,JavaScript调用它的提交方法即可,FORM的提交是可以跨域的。如果不想在页面上显示出这个过程,可以嵌入隐藏的IFRAME中,同样可以发出跨域的POST请求。
  那么,跨域的POST请求有什么用呢?假如一个网站的留言板之类的程序只是一个简单的表单提交接口,我们就很容易通过这个接口来提交留言之类的吧?这个接口地址如果没有特殊的验证或服务器上的来路限制,任何网站上的表单都可以往上面提交东西。这时候如果自己在互联网上有个静态页面,里面包含了跨域POST到这个接口上的程序。那么,任何人点开这个静态页面的URL就都会自动往目标留言板上提交一条数据。并且,这个POST请求会包含对方自己的COOKIE。这就意味着,即使是一个需要登陆才能发布信息的留言板也可以攻击,只要客户端曾经登陆过,并且COOKIE有保留就行。
  要解决这个问题一般有两种方法,一种是在POST请求的参数中添加一些特殊的参数来验证。由于JavaScript无法跨域读取这些参数,所以就构造不出有效的POST请求。另一种方式是直接在服务器端对POST请求做一个来路验证,HTTP头中的REFERER字段如果不是同域的就拒绝提交
第二种以PHP示例:

//下面这个可以防止盗链,因为通过window.open()、 window.location=...、window.showModelessDialog()等方式来到当前页面的都没有$_SESSION['HTTP_REFERER']变量
     if(!isset($_SESSION['HTTP_REFERER']))   {   
            header("location:login.php");exit;
     }   
//下面这个可以防止跨域攻击
     if(parse_url($_SESSION['HTTP_REFERER']) !=$_SERVER['HTTP_HOST']){
           header('location.php');exit;
     }

暂别杭州

暂别杭州

今天是我来杭州的第5个月,也是我暂别杭州的日子。

从去年11月第一次踏足杭州的土地,我就有种浮萍在漂流许久之后难得着陆的亲切与踏实。我发现我和杭州是有缘分的,这也许跟我小时候就来过杭州有关吧。

杭州是美丽的,我住在离西湖很近的一个地方,每逢节假日,我便会被西湖美景给吸引去,在人流窜动的西湖边上,喝杯饮料,看人来人往,樱花肆意绽放;杭州是可爱的,每天上班路上,我总会手拿一杯纯牛奶,看清晨小河边打太极的老爷爷,看杨柳垂下来的清新柳条,看送小朋友上学的奶奶的笑容,看公交司机停下车来看着匆忙过道的上班族的眼神;杭州是发展的,每天的生活都充实而精彩,来我现在的这家公司上班我每天都感觉是充满挑战,我很感激我可以再这里学习,老大风鸽技术大牛待人亲切,同事们细心负责乐于助人,老板平时又像兄弟一样嬉戏打闹,在杭州的这5个月里,我每天都在进步,我很开心,在这里想对所有遇过的人说一声:谢谢。

文鼎苑的翟兄,考研加油!

离别是暂时的,杭州我会再回来的。 等我 :)

                                                              编辑于:2016-4-10 10:11:45

Golang实现几个简单排序算法

Golang实现几个简单排序算法

package main

type Array []int32
//冒泡
func (a Array) BubbleSort() {
    for i := 0; i < len(a); i++ {
        for j := 0; j < len(a)-i-1; j++ {
            if a[j] > a[j+1] {
                a[j], a[j+1] = a[j+1], a[j]
            }
        }
    }
}
//插入
func (a Array) InsertSort() {
    for i := 0; i < len(a); i++ {
        tmp := a[i]
        j := i - 1
        for j >= 0 && a[j] > tmp {
            a[j+1] = a[j]
            j--
        }
        a[j+1] = tmp
    }

}
//快速
func (a Array) QuickSort(left, right int) {
    if left < right {
        key := a[left]
        low := left
        high := right
        for low < high {
            for low < high && a[high] > key {
                high--
            }
            a[low] = a[high]
            for low < high && a[low] < key {
                low++
            }
            a[high] = a[low]
        }
        a[low] = key
        a.QuickSort(left, low-1)
        a.QuickSort(low+1, right)
    }
}
func main() {
    var a Array = []int32{1, 5, 2, 6, 3, 9, 7, 4}

    //a.Bubble()
    //a.Insert()
    a.QuickSort(0, len(a)-1)
    for _, i := range a {
        print(i)
    }

}

冒泡排序原理图:
https://github.com/dongjun111111/notes/blob/master/工具源码/冒泡排序.gif
快速排序原理图:
https://github.com/dongjun111111/notes/blob/master/工具源码/快速排序.gif
插入排序原理图:
https://github.com/dongjun111111/notes/blob/master/工具源码/插入排序.gif

物联网通信协议

物联网通信协议

概要

物联网技术发展了不少年头,但依然处在一片混沌之中。要实现真正的互联互通,通信协议是重要的一环。然而,当前的物联网通信协议可谓是百家争鸣,各有千秋,让人眼花缭乱。这些协议都适用于何场景?各有何优缺点?在物联网应用中如何选型?本次分享,曾锐将结合物联网系统建设经验,跟大家聊一聊物联网通信协议的前世今生,以及应用实践。

我想将物联网通信协议大致分为两大类,一类是接入协议,一类是通讯协议。接入协议一般负责子网内设备间的组网及通信;通讯协议主要是运行在传统互联网TCP/IP协议之上的设备通讯协议,负责设备通过互联网进行数据交换及通信。这个分类只是为了方便,并非标准。

接入协议

目前市场上常见的接入协议有zigbee、蓝牙以及wifi协议等等

zigbee

zigbee目前在工业控制领域应用广泛,在智能家居领域也有一定应用。它有以下主要优势:

  1. 低成本。zigbee协议数据传输速率低,协议简单,所以开发成本也比较低。并且zigbee协议还免收专利费用~
  2. 低功耗。由于zigbee协议传输速率低,节点所需的发射功率仅1mW,并采用休眠+唤醒模式,功耗极低。
  3. 自组网。通过zigbee协议自带的mesh功能,一个子网络内可以支持多达65000个节点连接,可以快速实现一个大规模的传感网络。
  4. 安全性。使用crc校验数据包的完整性,支持鉴权和认证,并且采用aes-128对传输数据进行加密。

zigbee协议的最佳应用场景是无线传感网络,比如水质监测、环境控制等节点之间需要自组网以相互之间传输数据的工业场景中。在这些场景中zigbee协议的优势发挥的非常明显。目前国内外很多厂商也将zigbee运用在智能家居方案中,比如今年年初小米发布的“小米智能家居套装”。

为什么厂商会抛弃使用比较广泛的wifi及蓝牙协议,而采用zigbee呢,个人认为主要有以下原因: 1. 刚才提到zigbee协议有很强的自组网能力,可以支持几万设备,特别对于小米这种想构建智能家居生态链的企业,wifi和蓝牙的设备连接数量目前都是硬伤。
2. 目前zigbee协议还很难轻易被破解,而其他协议在安全性上一直为人诟病。
3. 很多智能家居产品如门磁为了使用方便,一般采用内置电池。此时zigbee的超低功耗大大提升了产品体验。

但是zigbee协议也有不足,主要就是它虽然可以方便的组网但不能接入互联网,所以zigbee网络中必须有
一个节点充当路由器的角色(比如小米智能家居套装中的智能网关),这提高了一定的成本并且让用户使用起来麻烦了一些。同时由于zigbee协议数据传输速率低,对于大流量应用如流媒体、视频等,基本是不可能。我个人认为,相对wifi和蓝牙协议这些年的快速发展和商业普及,zigbee协议尽管在技术设计和架构上拥有很大优势,但是技术更新太慢,同时在市场推广中也被竞争对手拉开了差距。后续zigbee协议在行业领域还是有很大空间,但是家用及消费领域要挑战wifi及蓝牙协议不是那么容易了。

蓝牙

蓝牙协议大家都非常熟悉了,特别是随着蓝牙4.0协议推出后发展迅速,目前已经成为智能手机的标配通信组件。蓝牙4.0之所有在近几年发展迅速,主要有以下几点原因: 1. 低功耗。我认为这个是蓝牙4.0的大杀器~使用纽扣电池的蓝牙4.0设备可运行一年以上,这对不希望频繁充电的可穿戴设备具有十分大的吸引力。当前基本世面上的可穿戴设备基本都选用蓝牙4.0方案。
2. 智能手机的普及。近年来支持蓝牙协议基本成为智能手机的标配,用户无需购买额外的接入模块。

值得关注的是蓝牙4.2版本近期推出,加入mesh组网功能,向zigbee发出了强有力的挑战。

WiFi

wifi协议和蓝牙协议一样,目前也得到了非常大的发展。由于前几年家用wifi路由器以及智能手机的迅速普及,wifi协议在智能家居领域也得到了广泛应用。wifi协议最大的优势是可以直接接入互联网。相对于zigbee,采用wifi协议的智能家居方案省去了额外的网关,相对于蓝牙协议,省去了对手机等移动终端的依赖。

相当于蓝牙和zigbee,wifi协议的功耗成为其在物联网领域应用的一大瓶颈。但是随着现在各大芯片厂商陆续推出低功耗、低成本的wifi soc(如esp8266),这个问题也在逐渐被解决。

wifi协议和蓝牙协议谁会在物联网领域一统江湖?这是目前讨论比较多的一个话题。个人认为wifi和蓝牙的各自在技术的优势双方都可以在协议升级的过程中互相完善,目前两个协议都在往“各取所长”的方向发展。最终谁能占据主导,可能更重要的是商业力量和市场决定的。短期内各个协议肯定是适用不同的场景,都有存在的价值。

通讯协议

刚才讲的都是物联网设备接入协议,对于物联网,最重要的是在互联网中设备与设备的通讯,下面重点跟大家分享下现在物联网在internet通信中比较常见的通讯协议。

HTTP

大家知道,在互联网时代,TCP/IP协议已经一统江湖,现在的物联网的通信架构也是构建在传统互联网基础架构之上。在当前的互联网通信协议中,HTTP协议由于开发成本低,开放程度高,几乎占据大半江山,所以很多厂商在构建物联网系统时也基于http协议进行开发。包括google主导的physic web项目,都是期望在传统web技术基础上构建物联网协议标准。

HTTP协议是典型的CS通讯模式,由客户端主动发起连接,向服务器请求XML或JSON数据。该协议最早是为了适用web 浏览器的上网浏览场景和设计的,目前在PC、手机、pad等终端上都应用广泛,但是我认为其并不适用于物联网场景。在物联网场景中其有三大弊端:

由于必须由设备主动向服务器发送数据,难以主动向设备推送数据。对于单单的数据采集等场景还秒抢适用,但是对于频繁的操控场景,只能推过设备定期主动拉取的的方式,实现成本和实时性都大打折扣。

安全性不高。web的不安全相信大家都是妇孺皆知,HTTP是明文协议,在很多要求高安全性的物联网场景,如果不做很多安全准备工作(如采用https等),后果不堪设想...

不同于用户交互终端如pc、手机,物联网场景中的设备多样化,对于运算和存储资源都十分受限的设备,http协议实现、XML/JSON数据格式的解析,都是“mission impossible”

所以,我们团队在设计物联网云平台时,也是只在针对手机或PC的用户时,采用HTTP协议,针对设备的物联网接入没有采用HTTP协议。

当然,依然有不少厂商由于开发方便的原因,选择基于HTTP协议构架物联网系统,在设备资源允许的情况下,怎么避免上面提到的数据推送实时性低的问题呢?websocket是一个可行的办法。

websocket

websocket是HTML5提出的基于TCP之上的可支持全双工通信的协议标准,其在设计上基本遵循HTTP的思路,对于基于HTTP协议的物联网系统是一个很好的补充。

由于物联网设备通信的模式和互联网中的即时通讯应用非常相似,互联网中常用的及时通讯协议也被大量运用于物联网系统构建中,这其中的典型是XMPP

XMPP是基于XML的协议,其由于开放性和易用性,在互联网及时通讯应用中运用广泛。相对HTTP,XMPP在通讯的业务流程上是更适合物联网系统的,开发者不用花太多心思去解决设备通讯时的业务通讯流程,相对开发成本会更低。但是HTTP协议中的安全性以及计算资源消耗的硬伤并没有得到本质的解决。前段时间报出的黑客轻松破解的TCL洗衣机,正是采用XMPP协议。

感兴趣的朋友可以稍候详细了解下破解过程~

CoaP

上面提到的无论是HTTP、websocket还是XMPP都是在设计时根据互联网应用场景设计的,虽然很多厂商把他们应用在物联网系统中,但是必然会面临水土不服,这些协议的通病就是根本无法适用物联网设备的多样性,无法适用很多物联网设备对低功耗、低成本的需求,难以在极低资源的物联网设备中运用。能不能有协议既可以借用web技术的设计**,同时又能适应恶劣的物联网设备运行环境呢?COAP协议应运而生了。

COAP协议的设计目标就是在低功耗低速率的设备上实现物联网通信。coap和HTTP协议一样,采用URL标示需要向发送的数据,在协议格式的设计上也基本是参考HTTP协议,非常容易理解。同时做了以下几点优化:

采用UDP而不是TCP。这省去了TCP建立连接的成本及协议栈的开销。
将数据包头部都采用二进制压缩,减小数据量以适应低网络速率场景。
发送和接受数据可以异步进行,这样提升了设备响应速度。
COAP协议就像一个针对物联网场景的http移植品,很多设计保留了HTTP协议的影子,拥有web背景的开发者也能快速上手。但是由于很多物联网设备隐藏在局域网内部,coap设备作为服务器无法被外部设备寻址,在ipv6没有普及之前,coap只能适用于局域网内部(如wifi)通信,这也很大限制了它的发展。

MQTT

MQTT协议就很好的解决了coap存在的问题。MQTT协议是由IBM开发的即时通讯协议,我认为是目前来说比较适合物联网场景的通讯协议。MQTT协议采用发布/订阅模式,所有的物联网终端都通过TCP连接连接到云端,云端通过主题的方式管理各个设备关注的通讯内容,负责将设备与设备之间消息的转发。

MQTT在协议设计时就考虑到不同设备的计算性能的差异,所以所有的协议都是采用二进制格式编解码,并且编解码格式都非常易于开发和实现。

最小的数据包只有2个字节,对于低功耗低速网络也有很好的适应性。

有非常完善的QOS机制,根据业务场景可以选择最多一次、至少一次、刚好一次三种消息送达模式。

运行在TCP协议之上,同时支持TLS(TCP+SSL)协议,并且由于所有数据通信都经过云端,安全性得到了较好地保障。

总结

我们在设计物联网云平台时,经过仔细对比和分析,最终也选用了mqtt协议作为主要通讯协议。但是mqtt协议的局限性是不支持设备的直连,对于可直接连接(如同一个局域网内)的设备也必须通过云端进行消息转发。为此我们在mqtt的基础上设计了mqtt-lan,增加了局域网内的设备发现和设备通讯。近期我们也会将我们的协议设计开源,欢迎感兴趣的朋友们一起交流和改进。

大家可以发现,当前的物联网通信协议真的是百花齐放,没有任何协议能够在市场上占有**地位。但是我认为,要实现物联网设备互联互通(不同厂商、不同平台、不同架构),关键点并不在上述接入协议或通讯协议的统一,而在于上层业务应用层协议的统一。无论是wifi、蓝牙、亦或是mqtt、http都是设备进行数据通讯和交换的通道,规定的是通讯的格式;而通讯的内容的统一才是实现互联互通的关键。如果把通信协议比作声音,光有通信协议,任何人之间还是无法交流。只有统一语言,大家才能顺畅沟通。

参考

http://ruizeng.net/iot-protocols/

MongoDB的真正性能-实战百万用户一-一亿的道具

MongoDB的真正性能-实战百万用户一-一亿的道具

使用情景
开始之前,我们先设定这样一个情景:

1.一百万注册用户的页游或者手游,这是不温不火的一个状态,刚好是数据量不上不下的一个情况。也刚好是传统MySql数据库性能开始吃紧的时候。

2.数据库就用一台很普通的服务器,只有一台。读写分离、水平扩展、内存缓存都不谈。一百万注册用户如果贡献度和活跃度都不高,恐怕公司的日子还不是那么宽裕,能够在数据库上的投资也有限。

以此情景为例,设每个用户都拥有100个道具,用户随时会获得或失去道具。

我们就来看看这一亿的道具怎么搞。

道具一般要使用原型、实例的设计方法,这个不属于数据库的范畴。

道具类型001 是屠龙刀,屠龙刀价格1500,基础攻击150,这些,我们把它们称为道具原型,保存在原型数据文件中。

这个原型数据文件,无论是存在何种数据库或者本地文件中,对服务器来说都不是问题,也不干扰数据库设计,所以我们不去讨论他。

关系数据库设计方法
典型的关系数据库设计方法:

用户表:字段 xxx userid xxx ,记录数量100万

xxx是其他字段,userid标示用户

用户道具表:字段 xxx userid itemtype xxx ,记录数量一亿

xxx是其他字段,userid 标示

一个亿的记录数是不是看起来有点头疼,mysql这个时候就要想各种办法了。

MongoDB设计方法
但我们用mongoDB来实现这个需求,直接就没有问题

首先第一个集合:users集合,用UserName 作为_id ,记录数100万

然后道具的组织,我们有两种选择

1.在users集合的值中建立Items对象,用Bson数组保存道具(Mongo官方称为Bson,和Json一模一样的存储方法)

方法一,没有额外的记录数

2.新建userItems集合,同样用UserName作为_id 每个UserItems集合的值中建立一个Item对象,使用一个Bson数组来保存道具

方法二,多了一个集合和100万记录数

我们的道具数据看起来像下面这样:

{_id:xxx,Items:[

{Itemtype:xxx,ItemPower:xxx},

...

...

...

]}

测试方法
测试方法如下:测试客户端随机检查一个用户的道具数量,小于100加一个道具,大于100 删除一个道具。

连续100万次,采用10个线程并发。

如果用关系数据库设计方法+mysql来实现,这是一个很压力很大的数据处理需求。

可是用文档数据库设计方法+MongoDB来实现,这个测试根本算不上有压力。

注意事项
即使我们用了一个如此胜之不武的设计方式,你依然有可能还是能把他写的很慢。

因为MongoDB在接口设计上并没有很好的引导和约束,如果你不注意,你还是能把他用的非常慢。

第一个问题

Key-Value数据库可以有好多的Key,没错,但对MongoDB来说,大错特错 MongoDB的索引代价很大,大到什么程度:

1.巨大的内存占用,100万条索引约占50M内存,如果这个设计中,你一个道具一条记录,5G内存将用于索引。

我们的屌丝情景不可能给你这样的服务器,

2.巨大的性能损失,作为一个数据库,所有的东西终将被写入硬盘,没有关系数据库那样的表结构,MongoDB的索引写入性能看起来很差,如果记录数据较小的时候,你可以观测到这样震撼的景象,加一个索引,性能变成了1/2,加两个索引,性能变成了1/3。

只有当第二个索引的查询不可避免,才值得增加额外索引。因为没索引的数据,查询性能是加几个零的慢,比加索引更惨。

我们既然选择了Key-Value数据库,应尽量避免需要多个索引的情况。

所有的索引只能存在于内存中,而读取记录时,也需要将Bson在内存中处理,内存还承担着更重要的作用:读取缓存。

本来就不充裕的内存,应该严格控制我们的记录条数,能够用Bson存储的,尽量用之。

那么我们之前在MongoDB的设计中怎么还考虑第二种设计方法呢?独立一个userItems 集合,不是又多出100万条记录了吗?

这基于另两个考虑:a.Bson的处理是要反复硬盘和内存交换的,如果每条记录更小,则IO压力更小。内存和硬盘对服务器来说都是稀缺资源,至于多大的数据拆分到另一个集合中更划算,这需要根据业务情况,服务器内存、硬盘情况来测试出一个合适大小,我们暂时使用1024这个数值,单用户的道具表肯定是会突破1024字节的,所以我们要考虑将他独立到一个集合中

b.可以不部署分片集群,将另一个集合挪到另一个服务器上去。只要服务器可以轻松承载100万用户,200万还会远么?在有钱部署分片集群以前,考虑第二组服务器更现实一些。

第二个问题

FindOne({_id:xxx})就快么? 毋庸置疑,FindOne({_id:xxx})就是最直接的用Key取Value。

也的确,用Key取Value 就是我们能用的唯一访问Value的方式,其他就不叫Key-Value数据库了。

但是,由于我们要控制Key的数量,单个Value就会比较大。

不要被FindOne({_id:xxx}).Items[3].ItemType这优雅的代码欺骗,这是非常慢的,他几乎谋杀你所有的流量。

无论后面是什么 FindOne({_id:xxx})总是返回给你完整的Value,我们的100条道具,少说也有6~8K.

这样的查询流量已经很大了,如果你采用MongoDB方案一设计,你的单个Value是包含一个用户的所有数据的,他会更大。

如果查询客户端和数据库服务器不在同一个机房,流量将成为一个很大的瓶颈。

我们应该使用的查询函数是FindOne({_id:xxx},filter),filter里面就是设置返回的过滤条件,这会在发送给你以前就过滤掉

比如FindOne({_id:xxx},{Items:{"$slice":[3,1]}}),这和上面那条优雅的代码是完成同样功能,但是他消耗很少的流量

第三个问题

精细的使用Update 这和问题二相对的,不要暴力的FindOne,也尽量不要暴力的Update一整个节点。虽然MangoDB的性能挺暴力的,IO性能极限约等于MongoDB性能,暴力的Update就会在占用流量的同时迎接IO的性能极限。

除了创建节点时的Insert或者Save之外,所有的Update都应该使用修改器精细修改.

比如Update({_id:xxx},{$set:{"Items.3.Item.Health":38}});//修改第三把武器的健康值

至于一次修改和批量修改,MongoDB默认100ms flush一次(2.x),只要两次修改比较贴近,被一起保存的可能性很高。

但是合并了肯定比不合并强,合并的修改肯定是一起保存,这个也要依赖于是用的开发方式,如果使用php做数据客户端,缓存起来多次操作合并了一起提交,实现起来就比较复杂。

注意以上三点,一百万注册用户并不算很多,4G内存,200G硬盘空间的MongoDB服务器即可轻松应对。性能瓶颈是硬盘IO,可以很容易的使用Raid和固态硬盘提升几倍的吞吐量。不使用大量的Js计算,CPU不会成为问题,不要让索引膨胀,内存不会成为问题。你根本用不着志强的一堆核心和海量的内存,更多的内存可以让缓存的效果更好一些,可是比读写分离还是差远了。如果是高并发时查询性能不足,就要采用读写分离的部署方式。当IO再次成为瓶颈时,就只能采用集群部署MongoDB启用分片功能,或者自行进行分集合与key散列的工作。

参考

http://www.cnblogs.com/crazylights/archive/2013/05/08/3068098.html

Mysql优化临时表使用,性能提升100倍

Mysql优化临时表使用,性能提升100倍

【问题现象】

线上mysql数据库爆出一个慢查询,DBA观察发现,查询时服务器IO飙升,IO占用率达到100%, 执行时间长达7s左右。
SQL语句如下:
SELECT DISTINCT g.*, cp.name AS cp_name, c.name AS category_name, t.name AS type_name FROM gm_game g LEFT JOIN gm_cp cp ON cp.id = g.cp_id AND cp.deleted = 0 LEFT JOIN gm_category c ON c.id = g.category_id AND c.deleted = 0 LEFT JOIN gm_type t ON t.id = g.type_id AND t.deleted = 0 WHERE g.deleted = 0 ORDER BY g.modify_time DESC LIMIT 20 ;

【问题分析】

(使用explain查看执行计划)
这条sql语句的问题其实还是比较明显的:
查询了大量数据(包括数据条数、以及g.* ),然后使用临时表order by,但最终又只返回了20条数据。
DBA观察到的IO高,是因为sql语句生成了一个巨大的临时表,内存放不下,于是全部拷贝到磁盘,导致IO飙升。

【优化方案】

优化的总体思路是拆分sql,将排序操作和查询所有信息的操作分开。
第一条语句:查询符合条件的数据,只需要查询g.id即可
SELECT DISTINCT g.id FROM gm_game g LEFT JOIN gm_cp cp ON cp.id = g.cp_id AND cp.deleted = 0 LEFT JOIN gm_category c ON c.id = g.category_id AND c.deleted = 0 LEFT JOIN gm_type t ON t.id = g.type_id AND t.deleted = 0 WHERE g.deleted = 0 ORDER BY g.modify_time DESC LIMIT 20 ;

第二条语句:查询符合条件的详细数据,将第一条sql的结果使用in操作拼接到第二条的sql
SELECT DISTINCT g.*, cp.name AS cp_name,c.name AS category_name,t.name AS type_name FROM gm_game g LEFT JOIN gm_cp cp ON cp.id = g.cp_id AND cp.deleted = 0 LEFT JOIN gm_category c ON c.id = g.category_id AND c.deleted = 0 LEFT JOIN gm_type t ON t.id = g.type_id AND t.deleted = 0 WHERE g.deleted = 0 and g.id in(…………………) ORDER BY g.modify_time DESC ;

【实测效果】

在SATA机器上测试,优化前大约需要50s,优化后第一条0.3s,第二条0.1s,优化后执行速度是原来的100倍以上,IO从100%降到不到1%
在SSD机器上测试,优化前大约需要7s,优化后第一条0.3s,第二条0.1s,优化后执行速度是原来的10倍以上,IO从100%降到不到1%
可以看出,优化前磁盘io是性能瓶颈,SSD的速度要比SATA明显要快,优化后磁盘不再是瓶颈,SSD和SATA性能没有差别。

【理论分析】

MySQL在执行SQL查询时可能会用到临时表,一般情况下,用到临时表就意味着性能较低。

  • 临时表存储

MySQL临时表分为“内存临时表”和“磁盘临时表”,其中内存临时表使用MySQL的MEMORY存储引擎,磁盘临时表使用MySQL的MyISAM存储引擎;
一般情况下,MySQL会先创建内存临时表,但内存临时表超过配置指定的值后,MySQL会将内存临时表导出到磁盘临时表;
Linux平台上缺省是/tmp目录,/tmp目录小的系统要注意啦。

使用临时表的场景
1)ORDER BY子句和GROUP BY子句不同, 例如:ORDERY BY price GROUP BY name;

2)在JOIN查询中,ORDER BY或者GROUP BY使用了不是第一个表的列 例如:SELECT * from TableA, TableB ORDER BY TableA.price GROUP by TableB.name

3)ORDER BY中使用了DISTINCT关键字 ORDERY BY DISTINCT(price)

4)SELECT语句中指定了SQL_SMALL_RESULT关键字 SQL_SMALL_RESULT的意思就是告诉MySQL,结果会很小,请直接使用内存临时表,不需要使用索引排序 SQL_SMALL_RESULT必须和GROUP BY、DISTINCT或DISTINCTROW一起使用 一般情况下,我们没有必要使用这个选项,让MySQL服务器选择即可。

  • 直接使用磁盘临时表的场景

1)表包含TEXT或者BLOB列;
2)GROUP BY 或者 DISTINCT 子句中包含长度大于512字节的列;
3)使用UNION或者UNION ALL时,SELECT子句中包含大于512字节的列;

  • 临时表相关配置

tmp_table_size:指定系统创建的内存临时表最大大小; http://dev.mysql.com/doc/refman/5.1/en/server-system-variables.html#sysvar_tmp_table_size

max_heap_table_size: 指定用户创建的内存表的最大大小; http://dev.mysql.com/doc/refman/5.1/en/server-system-variables.html#sysvar_max_heap_table_size
注意:最终的系统创建的内存临时表大小是取上述两个配置值的最小值。

  • 表的设计原则

使用临时表一般都意味着性能比较低,特别是使用磁盘临时表,性能更慢,因此我们在实际应用中应该尽量避免临时表的使用。 常见的避免临时表的方法有:
1)创建索引:在ORDER BY或者GROUP BY的列上创建索引;
2)分拆很长的列:一般情况下,TEXT、BLOB,大于512字节的字符串,基本上都是为了显示信息,而不会用于查询条件, 因此表设计的时候,应该将这些列独立到另外一张表。

  • SQL优化

如果表的设计已经确定,修改比较困难,那么也可以通过优化SQL语句来减少临时表的大小,以提升SQL执行效率。
常见的优化SQL语句方法如下:
1)拆分SQL语句
临时表主要是用于排序和分组,很多业务都是要求排序后再取出详细的分页数据,这种情况下可以将排序和取出详细数据拆分成不同的SQL,以降低排序或分组时临时表的大小,提升排序和分组的效率,我们的案例就是采用这种方法。
2)优化业务,去掉排序分组等操作
有时候业务其实并不需要排序或分组,仅仅是为了好看或者阅读方便而进行了排序,例如数据导出、数据查询等操作,这种情况下去掉排序和分组对业务也没有多大影响。

  • 如何判断使用了临时表?

使用explain查看执行计划,Extra列看到Using temporary就意味着使用了临时表。

关于乐观锁

关于乐观锁

背景

这几年O2O火爆,10个创业公司里就有9个是O2O的电商,相信支付抢购什么的以前听起来高大上的东西现在很多码农都会需要自己去实现。

说到支付,就一定会涉及到事务。说到事务,肯定很多码农泪千行。

在原来的数据库单机时代,你写一个事务并没有什么关系,我的支付逻辑涉及的所有表可能都在同一个数据库里,MySQL原生支持这些东西,学校里的老师也会跟你讲,事务是保证数据完整性的重要手段。

然而到了SOA时代,网站服务化,可能你的账户表和流水表都不在一个数据库里了(只是举例,不要太较真)。这种时候就有了分布式事务的概念。然后就有了各种二阶段提交,分布式CAP的取舍,基于消息队列实现的分布式事务等等等等。

本质

其实是一种写入、更新数据库时的逻辑特性。
具体是这样的:

1.在需要加乐观锁的表中加入version字段  
2.update时,在where条件后加入version = [select出来的之前的版本号]  

作用

是并发更新时的数据覆盖问题,诚然,我们可以用悲观锁来避免这种事情的发生,那么可以直接用select for来对多条记录上行锁,之后再对这些数据进行update,这样后面的事务在select时就不会拿到错误的数据。但是悲观锁对数据库的性能影响比较大。而乐观锁可以实现同样的功能。

原理

单条记录
1.select阶段得到该条记录,带有版本号  
2.update时将该条记录与其版本号进行对比,如果版本号与select得到的不一致,那么更新失败,affect_rows应该是0,否则更新成功  

sql比较好写,例子:
select * from [table] where [业务逻辑];
update [table] set [业务逻辑] where id = [上面的id] and version = [上面的version]

多条记录
多条记录的乐观锁法,想了半天还真是很难写SQL,我们可以从业务上来限制这件事情。

比如在进行“订单状态”转移这个批量更新时,我们可以在where条件里加上订单状态的限制,如:

订单批量审核要求其前置状态必须是待审核,所以where status=[待审核]

实际上实现的也是乐观锁的效果

说起来乐观锁一般情况下也不会在批量更新的时候被用到呢,如果想要批量更新时的更通用的乐观锁,看起来不是很好实现。

参考

http://xargin.com/about-optimistic-locking/

响应式设计

响应式设计

1、页面布局
先抛开响应式的概念,来聊聊web页面的布局。根据比较权威的分类,布局可以分为4种类型:固定布局、流体布局、弹性布局、混合布局。

固定布局
在固定布局中,页面宽度会被指定为特定大小的像素。它是web设计中最常见的布局,所有的容器、元素几乎都被设定为固定的像素值。一旦确定了,整个站点的宽度及尺寸就确定了,也就是开发者帮用户做了决定。随着使用不同屏幕尺寸的终端来访问站点的用户越来越多,固定布局的弊端也愈加显现:出现滚动条。

流体布局
在流动布局中,度量的单位不再是像素,而是变成了百分比,这样可使页面具有可变的特性。站点可以根据浏览器的宽度自动调节自身宽度。但是,单独使用流动布局不足以在各个尺寸的终端上保持良好的效果。比如,有些文本的行宽在大屏幕上看起来会太宽,而在小屏幕上又看起来太窄。

弹性布局
弹性布局与流动布局类似,只是它们的度量单位不同,通常情况下它以em作为单位。弹性布局为设计师在排版方面提供了强大的控制权,随着用户增大或缩小字体,元素的宽度也会等比例地变化。但是,在弹性布局中也可能出现令人讨厌的水平滚动条。如果你把字体大小设置为16px,并把容器宽度设置为55em,那么就会在任何宽度小于880px(16x55)的屏幕中出现水平滚动条。

混合布局
混合布局是结合上面两种或两种以上的布局方式。例如,页面上需要有一个300px定宽的广告区域,而其余的列的宽度我们设置为百分比,这样就可以同时达到我们的要求。

那么,哪种布局最具响应性?这取决于你的项目,因为每一种方法都有其优势和不足。大多数情况下,最佳答案是更具灵活性的那几种布局——流动布局、弹性布局或者混合布局。

2、字体大小
web上设置字体的大小,不外乎三种:像素(px)、em、百分比。像素就不说了。em是级联的,是以父元素的字体大小为基准进行设定的。这样有好处也有坏处。好处是,当需要调整页面的字体大小时,只需要改变根元素的字体大小,其余元素的字体就会自动进行调整。坏处时,当页面结构发生改变时,需要重新调整字体大小,如果页面变化很大,调整的工作量也会越大。百分比和em是一样的,以百分比为单位的字体也是级联的,因此理论上来讲,它和em没有太大的区别。另外一种极具潜力并兼具灵活性的单位是rem,它与em的区别在于:rem的大小只与根元素——Html元素——有关。不过它不兼容某些版本的浏览器(IE6、7、8等),关于rem可以参考这篇文章,由于篇幅问题就不展开讨论了,我是挺看好它的并且已经有同事在项目里使用了。

3、媒介查询
媒介查询可以让你在特定环境下查询到各种属性值——分辨率、色彩深度、高度和宽度等,从而决定应用什么样式。它是响应式设计里不可或缺的技术。

设备像素与css像素
这两个概念在我的另一篇译文里阐述了,它对理解视口至关重要。

视口
在桌面浏览器中,视口是个很简单的概念,即浏览器的可视区域,也指浏览器的宽度。而在移动浏览器中,有两种视口需要考虑:布局视口和视觉视口。根据我的理解,布局视口就是浏览器为页面指定的宽度,而视觉视口是指在设备屏幕能显示下的页面宽度。布局视口的值是不会变的(在iphone里是980px),与缩放是无关的。而视觉视口是会变化的,页面被放大时它就变小了(css像素变大),而页面被缩小时它就变大了,结合上面那篇关于设备像素与css像素的译文理解。

视口标签
在里加上<meta name=“viewport” content=“xx=xxx” >元标签可以对视口进行设置。通常的做法是content="width=device-width",即将布局视口设置为设备宽度,这样页面不会被过分地缩放,使内容可以合适地显示在屏幕上。另外,user-scalable属性规定了是否允许用户缩放,书中认为这需要根据具体需求确定。在iOS设备里存在1个bug,如果把视口设置为任意大小并允许缩放,那么横屏之后页面会自动变大。禁用缩放,该问题则不会存在。个人认为,如果响应式做得足够周到,缩放就变得不是很必要,所以我倾向于禁用缩放。

媒介查询顺序
关于媒介查询的结构及用法,网上或书上很容易找到,在此略过。来看看媒介查询的顺序对我们的设计有怎样的影响。你在创建css时,需要选择哪种设计**来建立响应式站点:是要从桌面端开始向下设计,还是从移动端开始向上设计。从桌面端向下设计,这种**创建出的样式表通常会是这样的:

/* base styles */
@media all and (max-width:768px) {
  ...
}
@media all and (max-width: 320px){
  ...
}

这造成了一些问题:虽然目前移动设备对媒介查询的支持有所改善,但仍旧不够完善。在那些不支持媒介查询的移动浏览器上,这些页面就会很不友好甚至浏览体验很差。 如果反过来,优先建立移动体验,然后针对大屏幕使用媒介查询对布局作出调整,那就可以在很大程度上规避上面所遇到的问题。一个采用从移动端向上的设计**创建的样式表通常是这样的:

/* base styles,for the small-screen experience,go here */
@media all and (max-width:320px) {
  ...
}
@media all and (max-width: 768px){
  ...
}

能获得浏览器更好的支持并不是从移动端向上设计的唯一好处,优先创建移动体验还可以降低css文件的复杂性。例如,从桌面端向下设计需要以下代码:

aside{
  display: table-cell;
  width: 300px;
}
@media all and (max-width: 320px){
  aside{
    display: block;
    width: 100%;
  }
}

而如果采用从移动端向上设计,样式表则会这样:

@media all and (min-width: 320px){
  aside{
    display: table-cell;
    width: 300px;
  }
}

确定断点
根据内容来决定应该在哪里设置断点以及需要设置多少断点才是更好的方法。之后你可以再通过缩放浏览器窗口来查看还有哪里有进一步改善的空间。为了能够确定断点,你可以将浏览器窗口缩放至300px左右(假设你的浏览器允许你缩放到这种程度),然后缓慢地拉宽窗口直到有些东西看起来需要进行一点润色。

响应式多媒体
性能在很多项目中扮演着重要的角色。如果在一个有350px宽的图片就足矣的设备上,却下载了一张624px宽的图片,无疑会严重降低页面的性能,这是个大问题。如果你说,ok,那在小屏幕就不显示图片了。于是,你通过媒介查询在小屏幕上设置图片display:none,问题还是一样存在。虽然在小屏幕设备上不会显示图片,但是浏览器仍然会去下载它。

响应式图片策略
1、通过脚本判断

我们把页面上的img标签全部删除,然后通过使用html5的data-属性设置图片的src,例如:

      data-src="images/ball.jpg" 

然后,你的javascript脚本可以这样写:

var lazy = document.querySelectorAll('[data-src]')
for (var i = 0; i < lazy.length; i++){
  var source = lazy[i].getAttribute('data-src')
  var img = new Image()
  img.src = source
  lazy[i].insertBefore(img, lazy[i].firstChild)
}

以上通过javascript实现了图片的懒加载,但是还没有加入屏幕尺寸的判断逻辑,我们可以使用matchMedia方法来实现。matchMedia是javascript内部自带的方法,你可以将css媒介查询作为参数传递给它,它会返回相关媒介查询是否匹配的信息。具体来说,函数会返回一个MediaQueryList对象,该对象具有两个属性:matches和media。matches属性的值可以是true(如果媒介查询匹配)或者false(不匹配)。media属性的值就是你刚刚传递的参数,例如对于window.matchMedia("(min-width:200px)")来说media属性将会返回”(min-width:200px)“。目前支持matchMedia()方法的浏览器有: Chrome、Safari 5.1+、Firefox 9、Android 3+以及iOS5+。不过,Paul Irish为那些不支持该方法的浏览器创建了一个方便使用的polyfill。具体的代码如下:

if (window.matchMedia("(min-width: 37.5em)").matches) {
  //load in the images
  ...
}

2、找服务器帮忙

使用服务器检测技术来决定应该下载哪一张图片。目前一淘的响应式图片就是采用这种策略。但是,这种服务器端处理对未来也不是特别友好。随着请求你站点内容的设备的种类不断增长,维护所有设备的信息会变得越来越困难。

3、Sencha.io Src

Sencha.io Src是最近似于即插即用型的响应式图片解决方案。要使用该服务,你只需在你的图片资源前面加上Sencha.io Src的链接即可:http://src.sencha.io/http://mysite.com/images/ball.jpg。Sencha.io Src会使用发起请求的设备的用户代理字符串来计算出设备屏幕的大小,然后根据该数值来缩放图片。而且,它也足够聪明,会缓存请求以便提高重复请求的效率。但是,如果你除了缩放图片,还想对图片进行重新裁剪,这就会有一些限制。

4、自适应图片

另一个近乎于即插即用的解决方案是由Matt Wilcox创建的自适应图片,它会先确定屏幕的大小,然后创建并缓存一张缩放后的图片。该方案是在服务器端维护一份断点配置。你需要在页面文档头部上加入以下一段代码:

document.cookie = 'resolution=' + Math.max(screen.width, screen.height) + ';path=/';

这行代码会获取屏幕的分辨率,并保存到cookie中。服务器获取到屏幕分辨率后,与配置进行对比,选出最合适的图片尺寸然后输出。图片创建过程是动态的,并辅以缓存以提高响应效率。

高分辨率屏幕
随着iPhone、iPad和MacBook Pro都采用了Retina屏幕(像素密度高达326ppi),意味着图片的显示效果将会异常的细致而清晰——如果图片为此做了优化。如果图片没有做优化,那么他们的显示效果将会是颗粒状并且是模糊的。为高分辨率屏幕创建图片就意味着要创建面积较大的图片,同时也就意味着图片的文件大小也会很大。为此,你可以为非Webkit浏览器使用min-resolution媒介查询,对于基于Webkit的浏览器,你必须使用-webkit-min-device-pixel-ratio媒介查询。

SVG
对于高分辨率屏幕上的显示问题,以及图片在不同尺寸屏幕上显示时的可伸缩问题,可以将可伸缩矢量图形(SVG)作为一个解决方案。

其他
视频与广告也是人们关心的重点。对于视频来说,使用固定比例的方法可以让你根据屏幕尺寸适当地缩放视频。与往常一样,要有意识地关注性能。最好能够为小屏幕用户显示视频链接,而为大屏幕用户直接显示嵌入的视频。对于广告来说,解决技术上的挑战并不困难。如果你是从自己系统中加载广告的,javascript或者一些响应式的html和css都可以为不同分辨率的屏幕上为改变广告提供帮助。更大的问题是如何把销售团队和第三方广告网络也拉上船。

RESS
两种基本检测方法
1、用户代理检测

用户代理检测是通过检测浏览器的用户代理字符串(User Agent)来决定为设备提供哪种站点的方法,这一过程是在服务端完成的。用户代理的名声并不好,在很长一段时间内它都被人们误用或者滥用。那些没有受到青睐的浏览器可以“撒谎”,把它们自己的用户代理字符串修改为那些更受欢迎的浏览器的样子。目前,一淘的仍然使用基于用户代理的检测。

2、功能检测

很多脚本都有功能检测的功能,其中最为著名的就是Modernizr。它可以测试超过40种不同的功能,而且还能提供另外3样有助于开发的东西:1、一个包含测试结果的javascript对象。2、会在html元素中增加类名,以表明对于功能的支持情况。3、提供一个脚本加载器,可以有条件地加载polyfill。

刚才说的Modernizr是在客户端进行检测,还有一个叫modernizr-server的代码库,可以在服务器端获取Modernizr检测结果,从而可以在页面被下载之前改变代码结构。使用时需要下载modernizr-server和最新的javascript库,并将下载好的javascript库命名为modernizr.js,然后放入modernizr-server/modernizr.js/文件夹下。原理是:当访问者第一次访问某一页面时会执行javascript的代码库并获取测试的结果。然后这些结果被添加到cookie中,而页面则会立刻重新加载。当下一次加载页面时,代码库会读取cookie中的信息,并且如果可以的话会将其置于会话变量中传回服务器。

将用户代理检测和功能检测相结合,将服务器端检测和响应式设计相结合,被称为RESS。

计划、设计流程
毫无疑问,响应式设计是一种强大的技术,但它不是银弹。最大化你站点的价值需要花费大量的时间并作出谨慎的决定。你必须将响应式设计整合到项目的计划中去。研究你的分析数据,但是要记住它们也会说谎。仔细考虑一下你的内容,虽然不需要在设计和开发前就把内容最终确定下来,但是你至少应该知道内容的结构。

响应式设计远远不只是一种简单的策略,它为Web项目带来的是一整套全新的、完整的方法,也是一种新的、可以更好地利用这一平台的工作流程。新流程必须是敏捷而灵活的。要去拥抱Web的交互本性,并开始在浏览器中创建模型。平面图片只能描绘出站点有限的一部分,它们没有能力描绘出用户与网站交互时设计看起来会是什么样子,而且还使得交付变得复杂。

参考

http://luckydrq.com/2013-09-09/responsive-web-design/

Screen/Tmux

Screen/Tmux

screen/tmux 是远程ssh session的管理工具。
可以在server端帮你保存工作现场和恢复工作现场。
最典型的应用场景就是,你每天下班关机器的时候,先保存现场(session)。
然后第二天上班的时候再登录上去恢复现场(session) ,可以一下子就进入到之前的工作状态,
比如当时正使用vim编写代码编写到第N行的状态。
.screenrc 配置文件:

hardstatus alwayslastline  
hardstatus string "%{.bW}%-w%{.rY}%n %t%{-}%+w %=%{..G} %c:%s "  
startup_message off  
vbell off  
bind ' ' title 
bindkey -k k; title
bindkey -k F1 prev
bindkey -k F2 next
#defencoding GBK
#encoding GBK UTF-8

bindkey "^[j" prev
bindkey "^[k" next

tmux是screen的增强版。
.tmux.conf 配置文件:

set -g prefix ^a
unbind ^b
bind a send-prefix

#set -g status-right "#[fg=green]#(uptime.pl)#[default] . #[fg=green]#(cut -d ' ' -f 1-3 /proc/loadavg)#[default]"
set -g status-right '%H:%M:%S %d-%b-%y'

set -g status-bg blue
set -g status-fg yellow

setw -g window-status-current-fg white  
setw -g window-status-current-bg red  
setw -g window-status-current-attr bright

# -n means no prefix
bind-key -n F7 command-prompt 'rename-session %%'
bind-key -n F10 command-prompt 'rename-window %%'
bind-key -n F11 previous-window
bind-key -n F12 next-window

参考

http://yanyiwu.com/work/2016/03/24/from-screen-to-tmux.html

MySQL调整或删除binlog

MySQL调整或删除binlog

mysql基于binlog进行复制,一般用于复制的配置类似这样:

####replaction########
server-id=17
#log-slave-updates
log-bin=mysql-bin
log-bin-index=mysql-bin.index
binlog_cache_size=4M
#binlog-format=MIXED
expire_logs_days=10
max_binlog_size=1024M
sync_binlog=0

某次测试服上,开启了binlog,造成大量的binlog日志写满磁盘(容器10GB),mysql发生死锁。
由于需要binlog调试sql语句不能关闭binlog,所以先减少binlog保存日期比如2天,并且删除之前保留的binlog。
binlog保留时间由10天降低到2天:

mysql> show global variables like 'expire_logs_days';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| expire_logs_days | 10    |
+------------------+-------+
1 row in set (0.00 sec)
  
mysql> set global expire_logs_days=2;

删除2015-11-29之前的binlog:

mysql> PURGE BINARY LOGS BEFORE '2015-11-29 00:00:00';

参考

http://www.xiaomastack.com/2015/12/01/mysql-delete-binlog/

Golang中的错误和异常管理

Golang中的panic、recover

说道Golang中的错误和异常管理,不得不说说Panic和Recover,如何理解它们呢?Panic和Recover我们可以将他们看成是JAVA中的throw和catch.灵活的使用panic与catch将有利于程序调试的快速进行,保证程序能够应付意外或者突发情况.

package main
import "fmt"
func main() {
    f()
    fmt.Println("Returned normally from f.")
}
func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()
    fmt.Println("Calling g.")
    g(0)
    fmt.Println("Returned normally from g.")
}
func g(i int) {
    if i > 3 {
        fmt.Println("Panicking!")
        panic(fmt.Sprintf("%v", i))
    }
    defer fmt.Println("Defer in g", i)
    fmt.Println("Printing in g", i)
    g(i + 1)
}
output==>
Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.

Golang调用汇编和C

Golang调用汇编和C

只要不使用C的标准库函数,Go中是可以直接调用C和汇编语言的。其实道理很简单,Go的运行时库就是用C和汇编实现的,Go必须是能够调用到它们的。当然,会有一些额外的约束,这就是函数调用协议。

Go中调用汇编

假设我们做一个汇编版本的加法函数。首先GOPATH的src下新建一个add目录,然后在该目录加入add.go的文件,内容如下:

package add

func Add(a, b uint64) uint64 {
    return a+b
}

这个函数将两个uint64的数字相加,并返回结果。我们写一个简单的函数调用它,内容如下:

package main

import (
  "fmt"
  "add"
)

func main() {
     fmt.Println(add.Add(2, 15))
}

可以看到输出了结果为17。好的,接下来让我们删除Add函数的实现,只留下定义部分:

package add

func Add(a, b uint64) uint64

然后在add.go同一目录中建立一个add_amd64.s的文件(假设你使用的是64位系统),内容如下:

TEXT    ·Add+0(SB),$0-24
MOVQ    a+0(FP),BX
MOVQ    b+8(FP),BP
ADDQ    BP,BX
MOVQ    BX,res+16(FP)
RET  
,

虽然汇编是相当难理解的,但我相信读懂上面这段不会有困难。前两条MOVQ指令分别将第一个参数放到寄存器BX,第二个参数放到寄存器BP,然后ADDQ指令将两者相加后,最后的MOVQ和RET指令返回结果。

现在,再次运行前面的main函数,它将使用自定义的汇编版本函数,可以看到成功的输出了结果17。从这个例子中可以看出Go是可以直接调用汇编实现的函数的。大多时候不必要你去写汇编,即使是研究Go的内部实现,能读懂汇编已经很足够了。

也许你真的觉得在Go中写汇编很酷,但是不要忽视了这些忠告:

  • 汇编很难编写,特别是很难写好。通常编译器会比你写出更快的代码。
  • 汇编仅能运行在一个平台上。在这个例子中,代码仅能运行在 amd64 上。这个问题有一个解决方案是给 - Go 对于 x86 和 不同版本的代码分别写一套代码,文件名相应的以_386.s和_arm.s结尾。
  • 汇编让你和底层绑定在一起,而标准的 Go 不会。例如,slice 的长度当前是 32 位整数。但是也不是不可能为长整型。当发生这些变化时,这些代码就被破坏了。

当前Go编译器不能将汇编编译为函数的内联,但是对于小的Go函数是可以的。因此使用汇编可能意味着让你的程序更慢。

有时需要汇编给你带来一些力量(不论是性能方面的原因,还是一些相当特殊的关于CPU的操作)。对于什么时候应该使用它,Go源码包括了若干相当好的例子(可以看看 crypto 和 math)。由于它非常容易实践,所以这绝对是个学习汇编的好途径。

Go中调用C

接下来,我们继续尝试在Go中调用C,跟调用汇编的过程很类似。首先删掉前面的add_amd64.s文件,并确保add.go文件中只是给出了Add函数的声明部分:
package add

func Add(a, b uint64) uint64

然后在add.go同目录中,新建一个add.c文件,内容如下:

#include "runtime.h"

void ·Add(uint64 a, uint64 b, uint64 ret) {
    ret = a + b;
    FLUSH(&ret);
}

编译该包,运行前面的测试函数:

go install add

会发现输出结果为17,说明Go中成功地调用到了C写的函数。

要注意的是不管是C或是汇编实现的函数,其函数名都是以·开头的。还有,C文件中需要包含runtime.h头文件。这个原因在该文件中有说明: Go用了特殊寄存器来存放像全局的struct G和struct M。包含这个头文件可以让所有链接到Go的C文件都知道这一点,这样编译器可以避免使用这些特定的寄存器作其它用途。

让我们仔细看一下这个C实现的函数。可以看到函数的返回值为空,而参数多了一个,第三个参数实际上被作为了返回值使用。其中FLUSH是在pkg/runtime/runtime.h中定义为USED(x),这个定义是Go的C编译器自带的primitive,作用是抑制编译器优化掉对*x的赋值的。如果你很好奇USED是怎样定义的,可以去$GOROOT/include/libc.h文件里去找找。

被调函数中对参数ret的修改居然返回到了调用函数,这个看起来似乎不可理解,不过早期的C编译器确实是可以这么做的。

函数调用时的内存布局

Go中使用的C编译器其实是plan9的C编译器,和我们平时理解的gcc等会有一些区别。我们将上面的add.c汇编一下:

go tool 6c -I $GOROOT/src/pkg/runtime -S add.c

生成的汇编代码大概是这个样子的:

"".Add t=1 size=16 value=0 args=0x18 locals=0
000000 00000 (add.c:3)    TEXT    "".Add+0(SB),4,$0-24
000000 00000 (add.c:3)    NOP    ,
000000 00000 (add.c:3)    NOP    ,
000000 00000 (add.c:3)    FUNCDATA    $2,gcargs.0<>+0(SB)
000000 00000 (add.c:3)    FUNCDATA    $3,gclocals.1<>+0(SB)
000000 00000 (add.c:4)    MOVQ    a+8(FP),AX
0x0005 00005 (add.c:4)    ADDQ    b+16(FP),AX
0x000a 00010 (add.c:4)    MOVQ    AX,c+24(FP)
0x000f 00015 (add.c:5)    RET    ,
000000 48 8b 44 24 08 48 03 44 24 10 48 89 44 24 18 c3  H.D$.H.D$.H.D$..

这是Go使用的汇编代码,是一种类似plan9的汇编代码。类似a+8(FP)这种表示的含义是“变量名+偏移(寄存器)”。其中FP是帧寄存器,它是一个伪寄存器,实际上是内存位置的一个引用,其实就是BP(栈基址寄存器)上移一个机器字长位置的内存地址。

函数调用之前,a+8(FP),b+16(FP)分别表示参数a和b,而参数3的位置被空着,在被调函数中,这个位置将用于存放返回值。此时的其内存布局如下所示:

参数3
参数2
参数1  <-SP 

进入被调函数之后,内存布局如下所示:

参数3
参数2
参数1  <-FP
保存PC <-SP
...
...

CALL指令会使得SP下移,SP位置的内存用于保存返回地址。帧寄存器FP此时位置在SP上面。在plan9汇编中,进入函数之后的前几条指令并没有出现push ebp; mov esp ebp这种模式。plan9函数调用协议中采用的是caller-save的模式,也就是由调用者负责保存寄存器。注意这和传统的C是不同的。传统C中是callee-save的模式,被调函数要负责保存它想使用的寄存器,在函数退出时恢复这些寄存器。

需要注意的是参数和返回值都是有对齐的。这里是按Structrnd对齐的,Structrnd在源代码中义为sizeof(uintptr)。

参考

https://tiancaiamao.gitbooks.io/go-internals/content/zh/03.1.html

用Golang实现常见排序算法

用Golang实现常见排序算法

在编程界,算法与数据结构与操作系统无疑是重中之重,无奈大学几年对这些也没有放在心上,看完就忘,直到现在才意识到他们的重要地位。是时候重新拿起来了。

package main
import (
    "fmt"
)

func main() {
    //保存需要排序的Slice
    arr := []int{9, 3, 4, 7, 2, 1, 0, 11, 12, 11, 13, 4, 7, 2, 1, 0, 11, 12, 11}
    //实际用于排序的Slice
    list := make([]int, len(arr))

    copy(list, arr)
    BubbleSortX(list)
    fmt.Println("冒泡排序:\t", list)

    copy(list, arr)
    QuickSort(list, 0, len(arr)-1)
    fmt.Println("快速排序:\t", list)

    copy(list, arr) //将arr的数据覆盖到list,重置list
    InsertSort(list)
    fmt.Println("直接插入排序:\t", list)

    copy(list, arr)
    ShellSort(list)
    fmt.Println("希尔排序:\t", list)

    copy(list, arr)
    MergeSort(list)
    fmt.Println("二路归并排序:\t", list)

    copy(list, arr)
    SelectSort(list)
    fmt.Println("简单选择排序:\t", list)

    copy(list, arr)
    HeapSort(list)
    fmt.Println("堆排序:     \t", list)

}

//region 冒泡排序
//1,正宗的冒泡排序
/*
每趟排序过程中通过两两比较,找到第 i 个小(大)的元素,将其往上排。
*/
func BubbleSort(list []int) {
    var temp int // 用来交换的临时数
    var i int
    var j int
    // 要遍历的次数
    for i = 0; i < len(list)-1; i++ {
        // 从后向前依次的比较相邻两个数的大小,遍历一次后,把数组中第i小的数放在第i个位置上
        for j = len(list) - 1; j > i; j-- {
            // 比较相邻的元素,如果前面的数大于后面的数,则交换
            if list[j-1] > list[j] {
                temp = list[j-1]
                list[j-1] = list[j]
                list[j] = temp
            }
        }
    }
}

//2,冒泡排序优化
/*
对冒泡排序常见的改进方法是加入标志性变量exchange,用于标志某一趟排序过程中是否有数据交换。
如果进行某一趟排序时并没有进行数据交换,则说明所有数据已经有序,可立即结束排序,避免不必要的比较过程。
*/
func BubbleSortX(list []int) {
    var exchange bool = false
    var temp int // 用来交换的临时数
    var i int
    var j int
    // 要遍历的次数
    for i = 0; i < len(list)-1; i++ {
        // 从后向前依次的比较相邻两个数的大小,遍历一次后,把数组中第i小的数放在第i个位置上
        for j = len(list) - 1; j > i; j-- {
            // 比较相邻的元素,如果前面的数大于后面的数,则交换
            if list[j-1] > list[j] {
                temp = list[j-1]
                list[j-1] = list[j]
                list[j] = temp
                exchange = true
            }
        }
        if !exchange {
            break
        }
        exchange = false
    }
}

//endregion

//region 快速排序
func division(list []int, left int, right int) int {

    // 以最左边的数(left)为基准
    var base int = list[left]
    for left < right {
        // 从序列右端开始,向左遍历,直到找到小于base的数
        for left < right && list[right] >= base {
            right--
        }
        // 找到了比base小的元素,将这个元素放到最左边的位置
        list[left] = list[right]
        // 从序列左端开始,向右遍历,直到找到大于base的数
        for left < right && list[left] <= base {
            left++
        }
        // 找到了比base大的元素,将这个元素放到最右边的位置
        list[right] = list[left]

    }
    // 最后将base放到left位置。此时,left位置的左侧数值应该都比left小
    // 而left位置的右侧数值应该都比left大。
    list[left] = base //此时left == right
    //fmt.Println("DONE: base:", base, "\tleft:", left, "\tright:", right)
    return left
}

func QuickSort(list []int, left int, right int) {
    // 左下标一定小于右下标,否则就越界了
    if left < right {
        //对数组进行分割,取出下次分割的基准标号
        var base int = division(list, left, right)
        //对“基准标号“左侧的一组数值进行递归的切割,以至于将这些数值完整的排序
        QuickSort(list, left, base-1)
        //对“基准标号“右侧的一组数值进行递归的切割,以至于将这些数值完整的排序
        QuickSort(list, base+1, right)
    }

}

//endregion

//region 直接插入排序
func InsertSort(list []int) {
    var temp int
    var i int
    var j int
    // 第1个数肯定是有序的,从第2个数开始遍历,依次插入有序序列
    for i = 1; i < len(list); i++ {
        temp = list[i] // 取出第i个数,和前i-1个数比较后,插入合适位置
        // 因为前i-1个数都是从小到大的有序序列,所以只要当前比较的数(list[j])比temp大,就把这个数后移一位
        for j = i - 1; j >= 0 && temp < list[j]; j-- {
            list[j+1] = list[j]
        }
        list[j+1] = temp
    }
}

//endregion

//region 希尔排序
func ShellSort(list []int) {
    for gap := (len(list) + 1) / 2; gap >= 1; gap = gap / 2 {
        for i := 0; i < len(list)-gap; i++ {
            InsertSort(list[i:(gap + i + 1)]) //list[i:(gap + i + 1)]表示list索引i到gap+i的元素组成的slice
        }
    }
}

//region

//region 简单选择排序
/*
简单排序处理流程:
(1)从待排序序列中,找到关键字最小的元素;
(2)如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换;
(3)从余下的 N - 1 个元素中,找出关键字最小的元素,重复(1)、(2)步,直到排序结束。
*/
func SelectSort(list []int) {
    var temp int
    var index int
    var i int
    var j int

    // 需要遍历获得最小值的次数
    // 要注意一点,当要排序 N 个数,已经经过 N-1 次遍历后,已经是有序数列
    for i = 0; i < len(list)-1; i++ {
        temp = 0
        index = i // 用来保存最小值得索引
        // 寻找第i个小的数值
        for j = i + 1; j < len(list); j++ {
            if list[index] > list[j] {
                index = j
            }
        }
        // 将找到的第i个小的数值放在第i个位置上
        temp = list[index]
        list[index] = list[i]
        list[i] = temp
    }
}

//endregion

//region 堆排序
func heapAdjust(list []int, parent int, length int) {
    temp := list[parent]  // temp保存当前父节点
    child := 2*parent + 1 // 先获得左孩子

    for child < length {
        // 如果有右孩子结点,并且右孩子结点的值大于左孩子结点,则选取右孩子结点
        if child+1 < length && list[child] < list[child+1] {
            child++
        }

        // 如果父结点的值已经大于孩子结点的值,则直接结束
        if temp >= list[child] {
            break
        }

        // 把孩子结点的值赋给父结点
        list[parent] = list[child]

        // 选取孩子结点的左孩子结点,继续向下筛选
        parent = child
        child = 2*child + 1
    }

    list[parent] = temp
}

func HeapSort(list []int) {
    // 循环建立初始堆
    for i := len(list) / 2; i >= 0; i-- {
        heapAdjust(list, i, len(list)-1)
    }

    // 进行n-1次循环,完成排序
    for i := len(list) - 1; i > 0; i-- {
        // 最后一个元素和第一元素进行交换
        temp := list[i]
        list[i] = list[0]
        list[0] = temp

        // 筛选 R[0] 结点,得到i-1个结点的堆
        heapAdjust(list, 0, i)
    }
}

//endregion

//region 归并排序(二路归并)
func merge(list []int, low int, mid int, high int) {
    var i int = low                  // i是第一段序列的下标
    var j int = mid + 1              // j是第二段序列的下标
    var k int = 0                    // k是临时存放合并序列的下标
    list2 := make([]int, high-low+1) // list2是临时合并序列
    // 扫描第一段和第二段序列,直到有一个扫描结束
    for i <= mid && j <= high {
        // 判断第一段和第二段取出的数哪个更小,将其存入合并序列,并继续向下扫描
        if list[i] <= list[j] {
            list2[k] = list[i]
            i++
            k++
        } else {
            list2[k] = list[j]
            j++
            k++
        }
    }
    // 若第一段序列还没扫描完,将其全部复制到合并序列
    for i <= mid {
        list2[k] = list[i]
        i++
        k++
    }

    // 若第二段序列还没扫描完,将其全部复制到合并序列
    for j <= high {
        list2[k] = list[j]
        j++
        k++
    }
    // 将合并序列复制到原始序列中
    k = 0
    for i = low; i <= high; i++ {
        list[i] = list2[k]
        k++
    }
}

func MergeSort(list []int) {
    for gap := 1; gap < len(list); gap = 2 * gap {
        var i int
        // 归并gap长度的两个相邻子表
        for i = 0; i+2*gap-1 < len(list); i = i + 2*gap {
            merge(list, i, i+gap-1, i+2*gap-1)
        }
        // 余下两个子表,后者长度小于gap
        if i+gap-1 < len(list) {
            merge(list, i, i+gap-1, len(list)-1)
        }
    }
}
output==>
冒泡排序:  [0 0 1 1 2 2 3 4 4 7 7 9 11 11 11 11 12 12 13]
快速排序:  [0 0 1 1 2 2 3 4 4 7 7 9 11 11 11 11 12 12 13]
直接插入排序:    [0 0 1 1 2 2 3 4 4 7 7 9 11 11 11 11 12 12 13]
希尔排序:  [0 0 1 1 2 2 3 4 4 7 7 9 11 11 11 11 12 12 13]
二路归并排序:    [0 0 1 1 2 2 3 4 4 7 7 9 11 11 11 11 12 12 13]
简单选择排序:    [0 0 1 1 2 2 3 4 4 7 7 9 11 11 11 11 12 12 13]
堆排序:         [0 0 1 1 2 2 3 4 4 7 7 9 11 11 11 11 12 12 13]

链表排序

package main 
import ( 
    "container/list"
    "fmt"
) 
type SortedLinkedList struct { 
    *list.List 
    Limit int
    compareFunc func (old, new interface{}) bool 
} 
func NewSortedLinkedList(limit int, compare func (old, new interface{}) bool) *SortedLinkedList { 
    return &SortedLinkedList{list.New(), limit, compare} 
} 
func (this SortedLinkedList) findInsertPlaceElement(value interface{}) *list.Element { 
    for element := this.Front(); element != nil; element = element.Next() { 
        tempValue := element.Value 
        if this.compareFunc(tempValue, value) { 
            return element 
        } 
    } 
    return nil 
} 
func (this SortedLinkedList) PutOnTop(value interface{}) { 
    if this.List.Len() == 0 { 
        this.PushFront(value) 
        return
    } 
    if this.List.Len() < this.Limit && this.compareFunc(value, this.Back().Value) { 
        this.PushBack(value) 
        return
    } 
    if this.compareFunc(this.List.Front().Value, value) { 
        this.PushFront(value) 
    } else if this.compareFunc(this.List.Back().Value, value) && this.compareFunc(value, this.Front().Value) { 
        element := this.findInsertPlaceElement(value) 
        if element != nil { 
            this.InsertBefore(value, element) 
        } 
    } 
    if this.Len() > this.Limit { 
        this.Remove(this.Back()) 
    } 
}

type WordCount struct { 
    Word  string 
    Count int
} 
func compareValue(old, new interface {}) bool { 
    if new.(WordCount).Count > old.(WordCount).Count { 
        return true
    } 
    return false
} 
func main() { 
    wordCounts := []WordCount{ 
        WordCount{"kate", 87}, 
        WordCount{"herry", 92}, 
        WordCount{"james", 81},
        WordCount{"jason",67},
        WordCount{"jack",97},
        WordCount{"bob",107}}
    var aSortedLinkedList = NewSortedLinkedList(10, compareValue) 
    for _, wordCount := range wordCounts { 
        aSortedLinkedList.PutOnTop(wordCount) 
    } 
    for element := aSortedLinkedList.List.Front(); element != nil; element = element.Next() { 
        fmt.Println(element.Value.(WordCount)) 
    } 
}
output==>
{bob 107}
{jack 97}
{herry 92}
{kate 87}
{james 81}
{jason 67}

基数排序

实现思路:任何一个阿拉伯数,它的各个位数上的基数都是以0~9来表示的。从个位开始正序排列,一直排到最高位。

Golang的channel阻塞应用

Golang的channel阻塞应用

首先我们来看一下什么是channel阻塞。channel默认上是阻塞的,也就是说,如果Channel满了,就阻塞写,如果Channel空了,就阻塞读。

package main

import (
    "time"
    "fmt"
)
func main(){
    channel :=make(chan string)
    go func(){
        channel <- "hello"
        fmt.Println("write \"hello\"done!")
        channel <- "world" //Reader在Sleep,这里在阻塞
        fmt.Println("write\"world\"done!")
        fmt.Println("write go sleep ...")
        time.Sleep(3*time.Second)
        channel <- "channel"
        fmt.Println("write \"channel\"done!")
    }()
    time.Sleep(2*time.Second)
    fmt.Println("Reader wake up ...")
    
    msg :=<-channel
    fmt.Println("Reader:",msg)
    
    msg =<-channel
    fmt.Println("Reader:",msg)
    
    msg =<-channel //Writer在Sleep,这里在阻塞
    fmt.Println("Reader:",msg)
}
output==>
Reader wake up ...
Reader: hello
write "hello"done!
write"world"done!
write go sleep ...
Reader: world
write "channel"done!
Reader: channel

既然我们知道了什么是channel阻塞,那我们能用它干嘛呢?答案是,做一个类似C语言中的线程池程序。

package main
/*
任务:go语言实现一个线程池,主要功能是:添加total个任务到线程池中,
线程池开启number个线程,每个线程从任务队列中取出一个任务执行,
执行完成后取下一个任务,全部执行完成后回调一个函数。
思路:将任务放到channel里,每个线程不停的从channel中取出任务执行,
并把执行结果写入另一个channel,当得到total个结果后,回调函数。
*/
import (
    "time"
    "io"
    "strings"
    "os"
    "net/http"
    "fmt"
)

type GoroutinePool struct {
      Queue  chan func() error
      Number int
      Total  int
      result         chan error
      finishCallback func()
 }
 
 // 初始化
func (self *GoroutinePool) Init(number int, total int) {
     self.Queue = make(chan func() error, total)
     self.Number = number
     self.Total = total
     self.result = make(chan error, total)
 }
 
 // 开门接客
 func (self *GoroutinePool) Start() {
     // 开启Number个goroutine
     for i := 0; i < self.Number; i++ {
        go func() {
             for {
                task, ok := <-self.Queue
                if !ok {
                     break              
                }
                err := task()
                self.result <- err
             }         
        }()
     }
 
     // 获得每个work的执行结果
     for j := 0; j < self.Total; j++ {
         res, ok := <-self.result
        if !ok {
             break
        }
        if res != nil {
           fmt.Println(res)   
        }
}    // 所有任务都执行完成,回调函数
    if self.finishCallback != nil {     
       self.finishCallback()
    }
}
// 关门送客
func (self *GoroutinePool) Stop() {
    close(self.Queue)
    close(self.result)
}

// 添加任务
func (self *GoroutinePool) AddTask(task func() error) {
    self.Queue <- task
}
// 设置结束回调
func (self *GoroutinePool) SetFinishCallback(callback func()) {
    self.finishCallback = callback
 }

func Download_test() {
      urls := []string{
          "http://dlsw.baidu.com/sw-search-sp/soft/44/17448/Baidusd_Setup_4.2.0.7666.1436769697.exe",
          "http://dlsw.baidu.com/sw-search-sp/soft/3a/12350/QQ_V7.4.15197.0_setup.1436951158.exe",
          "http://dlsw.baidu.com/sw-search-sp/soft/9d/14744/ChromeStandalone_V43.0.2357.134_Setup.1436927123.exe",
        }
     pool := new(GoroutinePool)
     pool.Init(3, len(urls))
 
     for i := range urls {
         url := urls[i]
         pool.AddTask(func() error {
             return download(url)
         })
     }
     isFinish := false
    pool.SetFinishCallback(func() {
        func(isFinish *bool) {
             *isFinish = true
        }(&isFinish)
    })
    pool.Start()
     for !isFinish {
         time.Sleep(time.Millisecond * 100)
     }
     pool.Stop()
     fmt.Println("所有操作已完成!")
}
func download(url string) error {
    fmt.Println("开始下载... ", url)

     sp := strings.Split(url, "/")
     filename := sp[len(sp)-1]
 
     file, err := os.Create("./aa/" + filename)
     if err != nil {
         return err
     }
      res, err := http.Get(url)
     if err != nil {
         return err
     }
 
     length, err := io.Copy(file, res.Body)
     if err != nil {
         return err
     }
 
     fmt.Println("## 下载完成! ", url, " 文件长度:", length)
     return nil
}
func main(){
    Download_test()
}
output==>
开始下载...  http://dlsw.baidu.com/sw-search-sp/soft/44/17448/Baidusd_Setup_4.2.0.7666.1436769697.exe
开始下载...  http://dlsw.baidu.com/sw-search-sp/soft/3a/12350/QQ_V7.4.15197.0_setup.1436951158.exe
开始下载...  http://dlsw.baidu.com/sw-search-sp/soft/9d/14744/ChromeStandalone_V43.0.2357.134_Setup.1436927123.exe
## 下载完成!  http://dlsw.baidu.com/sw-search-sp/soft/44/17448/Baidusd_Setup_4.2.0.7666.1436769697.exe  文件长度: 28500944

Web系统站内消息设计与实现

Web系统站内消息设计与实现

问题

首先站内消息主要包括:个人消息(评论,点赞),系统消息,订阅消息,私信。
其中,订阅区分用户群,即系统消息是一个特殊的所有人订阅的订阅消息,特点是一对多。
前三个实时性比较低,最后一个实时性高,离线状态下是私信,如果双方在线要转为聊天室,特点是一对一。
那么,接下来,该选个方案了,SQL or NoSQL?

Mysql实现

首先,对于个人消息、私信(“UserMessage”),一条消息插一句,Mysql跑跑没问题。
对于系统消息或订阅消息,必然不可以,假如有10万用户,一次性那么要插入10万条消息,Mysql必死。
那么就是说,要设立一个系统库(”SystemMessage”),每当用户登录,就去跑跑系统库(“SystemMessage”),把未读的系统库跑到个人库。
关于订阅消息就比较麻烦了,对用户分组?对消息分组?
关系型数据库处理集合问题是比较麻烦的,目前想到的结论是建立一个表(“RssMessage”)存储消息类型,消息索引。
大致的数据库模型:
-------------------- Message Text( id , content(text) )---------
↓↓ ↓↓
UserMessage SystemMessage
( id, ( id,
title(标题) title(标题)
sender(发送者) tid(内容ID)
receiver(接收者) ctime(创建时间)
status(状态) type(消息类型)
rtime(读取时间) ||
ctime(创建时间) ||
type(消息类型) ||
|| ||
------------------- UserSystemRelation -----------------------------------
id
uid(用户ID)
sid(系统条目)

看完这个数据库设计,还是感觉有不妥的地方。
UserSystemRelation表用于记录用户读取到哪个位置的标记。
可以看到,UserMessage与SystemMessage表中,title、tid、ctime、type字段冗余了,好像也没必要。
但是从用户功能上看,当用户登陆后,查找自己站内消息,必然要用到的有:status,必然要显示的有:title、ctime,type作为用户进入消息面板后,要筛选的方式之一,这样的话,Mysql就只要跑一个表就可以完成显示给用户的最新站内消息了。
由于MessageText可能是一个大信息通知,用户查看个人消息时候,并未查看MessageText内容,所以单独放一张表。

相应处理流程

- 用户登录后,先通过”UserSystemRelation”表查询是否有新的系统消息 - 如果”UserSystemRelation”,查找到自身uid,同步系统消息到个人消息; - 如果”UserSystemRelation”未查找到自身uid,直接插入”UserSystemRelation”,并读取最近50条系统消息。 - 用户点击未读消息,获取”MessageText””,并更新状态(status)为已读。 - 用户通过”status”、”type”,可以筛选系统消息。

Mysql+MongoDB实现

由于Mongodb是一种文档型的数据结构,所以,可以考虑把所有数据转成json直接塞给Mongodb。

基于用户的习惯,读多写少,大部分时候都是看到消息,删除、更新比较少,如果数据没更新直接读Mongodb,如果数据更新,直接删除Mongodb
的索引。

这个考虑是在,用户数量很大的时候,要在”UserSystem”表里查找到用户消息比较慢的时候用,类似于吧Mongodb当缓存。

Redis实现

看了Mysql下站内消息的数据库设计,我也觉得很蛋疼,临时过渡没事,但是还是NoSQL合适。
Redis自带订阅与发布系统,http://redisbook.readthedocs.org/en/latest/feature/pubsub.html.
Redis 通过 PUBLISH 、 SUBSCRIBE 等命令实现了订阅与发布模式, 这个功能提供两种信息机制, 分别是订阅/发布到频道和订阅/发布到模式.

频道的订阅与信息发送

Redis 的 SUBSCRIBE 命令可以让客户端订阅任意数量的频道, 每当有新信息发送到被订阅的频道时, 信息就会被发送给所有订阅指定频道的客户端。

订阅频道

每个 Redis 服务器进程都维持着一个表示服务器状态的 redis.h/redisServer 结构, 结构的 pubsub_channels 属性是一个字典, 这个字典就用于保存订阅频道的信息:

struct redisServer {
    // ...
    dict *pubsub_channels;
    // ...
};

其中,字典的键为正在被订阅的频道, 而字典的值则是一个链表, 链表中保存了所有订阅这个频道的客户端。当客户端调用 SUBSCRIBE 命令时, 程序就将客户端和要订阅的频道在 pubsub_channels 字典中关联起来。
SUBSCRIBE 命令的行为可以用伪代码表示如下:

def SUBSCRIBE(client, channels):

    # 遍历所有输入频道
    for channel in channels:

        # 将客户端添加到链表的末尾
        redisServer.pubsub_channels[channel].append(client)

通过 pubsub_channels 字典, 程序只要检查某个频道是否为字典的键, 就可以知道该频道是否正在被客户端订阅; 只要取出某个键的值, 就可以得到所有订阅该频道的客户端的信息。

发送信息到频道

了解了 pubsub_channels 字典的结构之后, 解释 PUBLISH 命令的实现就非常简单了: 当调用 PUBLISH channel message 命令, 程序首先根据 channel 定位到字典的键, 然后将信息发送给字典值链表中的所有客户端。
PUBLISH 命令的实现可以用以下伪代码来描述:

def PUBLISH(channel, message):
    # 遍历所有订阅频道 channel 的客户端
    for client in server.pubsub_channels[channel]:
        # 将信息发送给它们
        send_message(client, message)

退订频道

使用 UNSUBSCRIBE 命令可以退订指定的频道, 这个命令执行的是订阅的反操作: 它从 pubsub_channels 字典的给定频道(键)中, 删除关于当前客户端的信息, 这样被退订频道的信息就不会再发送给这个客户端。

模式的订阅与信息发送

当使用 PUBLISH 命令发送信息到某个频道时, 不仅所有订阅该频道的客户端会收到信息, 如果有某个/某些模式和这个频道匹配的话, 那么所有订阅这个/这些频道的客户端也同样会收到信息。

订阅模式

redisServer.pubsub_patterns 属性是一个链表,链表中保存着所有和模式相关的信息:

struct redisServer {
    // ...
    list *pubsub_patterns;
    // ...
};

链表中的每个节点都包含一个 redis.h/pubsubPattern 结构:

typedef struct pubsubPattern {
    redisClient *client;
    robj *pattern;
} pubsubPattern;

client 属性保存着订阅模式的客户端,而 pattern 属性则保存着被订阅的模式。

每当调用 PSUBSCRIBE 命令订阅一个模式时, 程序就创建一个包含客户端信息和被订阅模式的 pubsubPattern 结构, 并将该结构添加到 redisServer.pubsub_patterns 链表中。

发送信息到模式

原理伪代码如下:

def PUBLISH(channel, message):

    # 遍历所有订阅频道 channel 的客户端
    for client in server.pubsub_channels[channel]:

        # 将信息发送给它们
        send_message(client, message)

    # 取出所有模式,以及订阅模式的客户端
    for pattern, client in server.pubsub_patterns:

        # 如果 channel 和模式匹配
        if match(channel, pattern):

            # 那么也将信息发给订阅这个模式的客户端
            send_message(client, message)

退订模式

使用 PUNSUBSCRIBE 命令可以退订指定的模式, 这个命令执行的是订阅模式的反操作: 程序会删除 redisServer.pubsub_patterns 链表中, 所有和被退订模式相关联的 pubsubPattern 结构, 这样客户端就不会再收到和模式相匹配的频道发来的信息。

Redis自带订阅与发布系统小结

  • 订阅信息由服务器进程维持的 redisServer.pubsub_channels 字典保存,字典的键为被订阅的频道,字典的值为订阅频道的所有客户端。
  • 当有新消息发送到频道时,程序遍历频道(键)所对应的(值)所有客户端,然后将消息发送到所有订阅频道的客户端上。
  • 订阅模式的信息由服务器进程维持的 redisServer.pubsub_patterns 链表保存,链表的每个节点都保存着一个 pubsubPattern 结构,结构中保存着被订阅的模式,以及订阅该模式的客户端。程序通过遍历链表来查找某个频道是否和某个模式匹配。
  • 当有新消息发送到频道时,除了订阅频道的客户端会收到消息之外,所有订阅了匹配频道的模式的客户端,也同样会收到消息。
  • 退订频道和退订模式分别是订阅频道和订阅模式的反操作。

只要是订阅了相应地频道,就会收到频道的消息。
把用户ID作为频道,私信就是反向的频道订阅,系统消息就是所有用户的订阅,那么离线的消息呢?

1、线上用户

还是存在系统或个人的哈希表里,等上线后再去读取。
在Python中,订阅发布消息(Publish)如下:

import redis,time
queue = redis.StrictRedis(host='localhost', port=6379, db=0)
channel = queue.pubsub()

for i in range(100): 
    queue.publish("test", i)
    time.sleep(0.1)

Python中,订阅监听消息(Subcribe)如下:

import redis,time
r = redis.StrictRedis(host='localhost', port=6379, db=0)
p = r.pubsub()
p.subscribe('test')

while True:
    message = p.get_message()
    if message:
        print "Subscriber: %s" % message['data']

Redis-py的API可以看GitHub:https://github.com/andymccurdy/redis-py

2、线下用户

看过一种做法是建立一个Redis链表,存储登陆用户,当用户登陆就直接发送,没登陆就暂存起来。

这里的话,可以用WebSocket实时监听,定期发送心跳包,如果在线直接返回Redis自带的订阅系统。

系统消息建立一个集合:

SADD system:2015-08-03 7 8 9 10 11

第一段标示系统信息,第二段标示日期,后面的数字标示message id。

个人消息建立一个集合:

SADD user:12345:read 1 2 3 4

第一段标示用户信息集合,第二段标示用户id,下一段标示消息类型为已读,后面的数字标示message id。

关于订阅消息如下:

SADD rss:xiaocao 12 13 14 15

那么你就收到小草的订阅消息,消息ID分别是 12, 13, 14, 15

还有很重要的消息数据存储,

HMSET message:12 title 标题 content 内容 date 2015-08-03

Python创建数据库的例子就是:

import redis,time,threading,random
pool = redis.ConnectionPool(host='localhost', port=6379, db=1)
rs = redis.Redis(connection_pool=pool)

rs.sadd("user:123:read", "1", "2")
rs.sadd("user:123:unread", "4", "5", "6")
rs.sadd("system:2015-08-03", "7", "8", "9", "10", "11")
rs.sadd("rss:xiaocao", "12", "13", "14", "15", "11")

for i in range(15):
    rs.hset("message:"+str(i), "title", "title=>"+str(random.uniform(1, 99999)))
    rs.hset("message:"+str(i), "content","content=>"+str(time.time()))
    rs.hset("message:"+str(i), "date", str(time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime())))

参考

1.http://homeway.me/2015/08/03/website-system-message/
2.http://origin.redisbook.com/

Curl使用

Curl使用

一、查看网页源码 直接在curl命令后加上网址,就可以看到网页源码。我们以网址www.sina.com为例(选择该网址,主要因为它的网页代码较短)

curl www.sina.com

如果要把这个网页保存下来,可以使用-o参数,这就相当于使用wget命令了。

curl -o [文件名] www.sina.com

二、自动跳转 有的网址是自动跳转的。使用-L参数,curl就会跳转到新的网址。

curl -L www.sina.com

键入上面的命令,结果就自动跳转为www.sina.com.cn。

三、显示头信息

-i参数可以显示http response的头信息,连同网页代码一起。

curl -i www.sina.com

-I参数则是只显示http response的头信息。

四、显示通信过程

-v参数可以显示一次http通信的整个过程,包括端口连接和http request头信息。

curl -v www.sina.com

下面的命令可以查看更详细的通信过程。

curl --trace output.txt www.sina.com

或者

curl --trace-ascii output.txt www.sina.com

运行后,请打开output.txt文件查看。

五、发送表单信息

发送表单信息有GET和POST两种方法。GET方法相对简单,只要把数据附在网址后面就行。

curl example.com/form.cgi?data=xxx

POST方法必须把数据和网址分开,curl就要用到--data参数。

curl --data "data=xxx" example.com/form.cgi

如果你的数据没有经过表单编码,还可以让curl为你编码,参数是--data-urlencode。

curl --data-urlencode "date=April 1" example.com/form.cgi

六、HTTP动词

curl默认的HTTP动词是GET,使用-X参数可以支持其他动词。

curl -X POST www.example.com
curl -X DELETE www.example.com

七、文件上传

假定文件上传的表单是下面这样:

  
  


curl --form upload=@localfilename --form press=OK [URL]

八、Referer字段

有时你需要在http request头信息中,提供一个referer字段,表示你是从哪里跳转过来的。

curl --referer http://www.example.com http://www.example.com

九、User Agent字段

这个字段是用来表示客户端的设备信息。服务器有时会根据这个字段,针对不同设备,返回不同格式的网页,比如手机版和桌面版。 curl可以这样模拟:

curl --user-agent "[User Agent]" [URL]

十、cookie

使用--cookie参数,可以让curl发送cookie。

curl --cookie "name=xxx" www.example.com

至于具体的cookie的值,可以从http response头信息的Set-Cookie字段中得到。

十一、增加头信息

有时需要在http request之中,自行增加一个头信息。--header参数就可以起到这个作用。

curl --header "xxx: xxxxxx" http://example.com

十二、HTTP认证

有些网域需要HTTP认证,这时curl需要用到--user参数。

curl --user name:password example.com

十三、配置Host访问

curl -H "host:baidu.com" http://123.123.123.123/ping

参考

http://wiki.zheng-ji.info/Tool/curl.html

分布式系统的事务处理

分布式系统的事务处理【转】

当我们在生产线上用一台服务器来提供数据服务的时候,我会遇到如下的两个问题:

1)一台服务器的性能不足以提供足够的能力服务于所有的网络请求。

2)我们总是害怕我们的这台服务器停机,造成服务不可用或是数据丢失。

于是我们不得不对我们的服务器进行扩展,加入更多的机器来分担性能上的问题,以及来解决单点故障问题。 通常,我们会通过两种手段来扩展我们的数据服务:

1)数据分区:就是把数据分块放在不同的服务器上(如:uid % 16,一致性哈希等)。

2)数据镜像:让所有的服务器都有相同的数据,提供相当的服务。

对于第一种情况,我们无法解决数据丢失的问题,单台服务器出问题时,会有部分数据丢失。所以,数据服务的高可用性只能通过第二种方法来完成——数据的冗余存储(一般工业界认为比较安全的备份数应该是3份,如:Hadoop和Dynamo)。 但是,加入更多的机器,会让我们的数据服务变得很复杂,尤其是跨服务器的事务处理,也就是跨服务器的数据一致性。这个是一个很难的问题。 让我们用最经典的Use Case:“A帐号向B帐号汇钱”来说明一下,熟悉RDBMS事务的都知道从帐号A到帐号B需要6个操作:

从A帐号中把余额读出来。
对A帐号做减法操作。
把结果写回A帐号中。
从B帐号中把余额读出来。
对B帐号做加法操作。
把结果写回B帐号中。
为了数据的一致性,这6件事,要么都成功做完,要么都不成功,而且这个操作的过程中,对A、B帐号的其它访问必需锁死,所谓锁死就是要排除其它的读写操作,不然会有脏数据的问题,这就是事务。那么,我们在加入了更多的机器后,这个事情会变得复杂起来:

1)在数据分区的方案中:如果A帐号和B帐号的数据不在同一台服务器上怎么办?我们需要一个跨机器的事务处理。也就是说,如果A的扣钱成功了,但B的加钱不成功,我们还要把A的操作给回滚回去。这在跨机器的情况下,就变得比较复杂了。

2)在数据镜像的方案中:A帐号和B帐号间的汇款是可以在一台机器上完成的,但是别忘了我们有多台机器存在A帐号和B帐号的副本。如果对A帐号的汇钱有两个并发操作(要汇给B和C),这两个操作发生在不同的两台服务器上怎么办?也就是说,在数据镜像中,在不同的服务器上对同一个数据的写操作怎么保证其一致性,保证数据不冲突?

同时,我们还要考虑性能的因素,如果不考虑性能的话,事务得到保证并不困难,系统慢一点就行了。除了考虑性能外,我们还要考虑可用性,也就是说,一台机器没了,数据不丢失,服务可由别的机器继续提供。 于是,我们需要重点考虑下面的这么几个情况:

1)容灾:数据不丢、结点的Failover

2)数据的一致性:事务处理

3)性能:吞吐量 、 响应时间

前面说过,要解决数据不丢,只能通过数据冗余的方法,就算是数据分区,每个区也需要进行数据冗余处理。这就是数据副本:当出现某个节点的数据丢失时可以从副本读到,数据副本是分布式系统解决数据丢失异常的唯一手段。所以,在这篇文章中,简单起见,我们只讨论在数据冗余情况下考虑数据的一致性和性能的问题。简单说来:

1)要想让数据有高可用性,就得写多份数据。

2)写多份的问题会导致数据一致性的问题。

3)数据一致性的问题又会引发性能问题

这就是软件开发,按下了葫芦起了瓢。

一致性模型
说起数据一致性来说,简单说有三种类型(当然,如果细分的话,还有很多一致性模型,如:顺序一致性,FIFO一致性,会话一致性,单读一致性,单写一致性,但为了本文的简单易读,我只说下面三种):

1)Weak 弱一致性:当你写入一个新值后,读操作在数据副本上可能读出来,也可能读不出来。比如:某些cache系统,网络游戏其它玩家的数据和你没什么关系,VOIP这样的系统,或是百度搜索引擎(呵呵)。

2)Eventually 最终一致性:当你写入一个新值后,有可能读不出来,但在某个时间窗口之后保证最终能读出来。比如:DNS,电子邮件、Amazon S3,Google搜索引擎这样的系统。

3)Strong 强一致性:新的数据一旦写入,在任意副本任意时刻都能读到新值。比如:文件系统,RDBMS,Azure Table都是强一致性的。

从这三种一致型的模型上来说,我们可以看到,Weak和Eventually一般来说是异步冗余的,而Strong一般来说是同步冗余的,异步的通常意味着更好的性能,但也意味着更复杂的状态控制。同步意味着简单,但也意味着性能下降。 好,让我们由浅入深,一步一步地来看有哪些技术:

Master-Slave
首先是Master-Slave结构,对于这种加构,Slave一般是Master的备份。在这样的系统中,一般是如下设计的:

1)读写请求都由Master负责。

2)写请求写到Master上后,由Master同步到Slave上。

从Master同步到Slave上,你可以使用异步,也可以使用同步,可以使用Master来push,也可以使用Slave来pull。 通常来说是Slave来周期性的pull,所以,是最终一致性。这个设计的问题是,如果Master在pull周期内垮掉了,那么会导致这个时间片内的数据丢失。如果你不想让数据丢掉,Slave只能成为Read-Only的方式等Master恢复。

当然,如果你可以容忍数据丢掉的话,你可以马上让Slave代替Master工作(对于只负责计算的结点来说,没有数据一致性和数据丢失的问题,Master-Slave的方式就可以解决单点问题了) 当然,Master Slave也可以是强一致性的, 比如:当我们写Master的时候,Master负责先写自己,等成功后,再写Slave,两者都成功后返回成功,整个过程是同步的,如果写Slave失败了,那么两种方法,一种是标记Slave不可用报错并继续服务(等Slave恢复后同步Master的数据,可以有多个Slave,这样少一个,还有备份,就像前面说的写三份那样),另一种是回滚自己并返回写失败。(注:一般不先写Slave,因为如果写Master自己失败后,还要回滚Slave,此时如果回滚Slave失败,就得手工订正数据了)你可以看到,如果Master-Slave需要做成强一致性有多复杂。

Master-Master
Master-Master,又叫Multi-master,是指一个系统存在两个或多个Master,每个Master都提供read-write服务。这个模型是Master-Slave的加强版,数据间同步一般是通过Master间的异步完成,所以是最终一致性。 Master-Master的好处是,一台Master挂了,别的Master可以正常做读写服务,他和Master-Slave一样,当数据没有被复制到别的Master上时,数据会丢失。很多数据库都支持Master-Master的Replication的机制。

另外,如果多个Master对同一个数据进行修改的时候,这个模型的恶梦就出现了——对数据间的冲突合并,这并不是一件容易的事情。看看Dynamo的Vector Clock的设计(记录数据的版本号和修改者)就知道这个事并不那么简单,而且Dynamo对数据冲突这个事是交给用户自己搞的。就像我们的SVN源码冲突一样,对于同一行代码的冲突,只能交给开发者自己来处理。(在本文后后面会讨论一下Dynamo的Vector Clock)

Two/Three Phase Commit
这个协议的缩写又叫2PC,中文叫两阶段提交。在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。 两阶段提交的算法如下:

第一阶段:

协调者会问所有的参与者结点,是否可以执行提交操作。
各个参与者开始事务执行的准备工作:如:为资源上锁,预留资源,写undo/redo log……
参与者响应协调者,如果事务的准备工作成功,则回应“可以提交”,否则回应“拒绝提交”。
第二阶段:

如果所有的参与者都回应“可以提交”,那么,协调者向所有的参与者发送“正式提交”的命令。参与者完成正式提交,并释放所有资源,然后回应“完成”,协调者收集各结点的“完成”回应后结束这个Global Transaction。
如果有一个参与者回应“拒绝提交”,那么,协调者向所有的参与者发送“回滚操作”,并释放所有资源,然后回应“回滚完成”,协调者收集各结点的“回滚”回应后,取消这个Global Transaction。
我们可以看到,2PC说白了就是第一阶段做Vote,第二阶段做决定的一个算法,也可以看到2PC这个事是强一致性的算法。在前面我们讨论过Master-Slave的强一致性策略,和2PC有点相似,只不过2PC更为保守一些——先尝试再提交。 2PC用的是比较多的,在一些系统设计中,会串联一系列的调用,比如:A -> B -> C -> D,每一步都会分配一些资源或改写一些数据。比如我们B2C网上购物的下单操作在后台会有一系列的流程需要做。如果我们一步一步地做,就会出现这样的问题,如果某一步做不下去了,那么前面每一次所分配的资源需要做反向操作把他们都回收掉,所以,操作起来比较复杂。现在很多处理流程(Workflow)都会借鉴2PC这个算法,使用 try -> confirm的流程来确保整个流程的能够成功完成。 举个通俗的例子,西方教堂结婚的时候,都有这样的桥段:

1)牧师分别问新郎和新娘:你是否愿意……不管生老病死……(询问阶段)

2)当新郎和新娘都回答愿意后(锁定一生的资源),牧师就会说:我宣布你们……(事务提交)

这是多么经典的一个两阶段提交的事务处理。 另外,我们也可以看到其中的一些问题, A)其中一个是同步阻塞操作,这个事情必然会非常大地影响性能。 B)另一个主要的问题是在TimeOut上,比如,

1)如果第一阶段中,参与者没有收到询问请求,或是参与者的回应没有到达协调者。那么,需要协调者做超时处理,一旦超时,可以当作失败,也可以重试。

2)如果第二阶段中,正式提交发出后,如果有的参与者没有收到,或是参与者提交/回滚后的确认信息没有返回,一旦参与者的回应超时,要么重试,要么把那个参与者标记为问题结点剔除整个集群,这样可以保证服务结点都是数据一致性的。

3)糟糕的情况是,第二阶段中,如果参与者收不到协调者的commit/fallback指令,参与者将处于“状态未知”阶段,参与者完全不知道要怎么办,比如:如果所有的参与者完成第一阶段的回复后(可能全部yes,可能全部no,可能部分yes部分no),如果协调者在这个时候挂掉了。那么所有的结点完全不知道怎么办(问别的参与者都不行)。为了一致性,要么死等协调者,要么重发第一阶段的yes/no命令。

两段提交最大的问题就是第3)项,如果第一阶段完成后,参与者在第二阶没有收到决策,那么数据结点会进入“不知所措”的状态,这个状态会block住整个事务。也就是说,协调者Coordinator对于事务的完成非常重要,Coordinator的可用性是个关键。 因些,我们引入三段提交,三段提交在Wikipedia上的描述如下,他把二段提交的第一个段break成了两段:询问,然后再锁资源。最后真正提交。
三段提交的核心理念是:在询问的时候并不锁定资源,除非所有人都同意了,才开始锁资源。

理论上来说,如果第一阶段所有的结点返回成功,那么有理由相信成功提交的概率很大。这样一来,可以降低参与者Cohorts的状态未知的概率。也就是说,一旦参与者收到了PreCommit,意味他知道大家其实都同意修改了。这一点很重要。
其实,三段提交是一个很复杂的事情,实现起来相当难,而且也有一些问题。

看到这里,我相信你有很多很多的问题,你一定在思考2PC/3PC中各种各样的失败场景,你会发现Timeout是个非常难处理的事情,因为网络上的Timeout在很多时候让你无所事从,你也不知道对方是做了还是没有做。于是你好好的一个状态机就因为Timeout成了个摆设。

一个网络服务会有三种状态:1)Success,2)Failure,3)Timeout,第三个绝对是恶梦,尤其在你需要维护状态的时候。

Two Generals Problem(两将军问题)
Two Generals Problem 两将军问题是这么一个思维性实验问题: 有两支军队,它们分别有一位将军领导,现在准备攻击一座修筑了防御工事的城市。这两支军队都驻扎在那座城市的附近,分占一座山头。一道山谷把两座山分隔开来,并且两位将军唯一的通信方式就是派各自的信使来往于山谷两边。不幸的是,这个山谷已经被那座城市的保卫者占领,并且存在一种可能,那就是任何被派出的信使通过山谷是会被捕。 请注意,虽然两位将军已经就攻击那座城市达成共识,但在他们各自占领山头阵地之前,并没有就进攻时间达成共识。两位将军必须让自己的军队同时进攻城市才能取得成功。因此,他们必须互相沟通,以确定一个时间来攻击,并同意就在那时攻击。如果只有一个将军进行攻击,那么这将是一个灾难性的失败。 这个思维实验就包括考虑他们如何去做这件事情。下面是我们的思考:

1)第一位将军先发送一段消息“让我们在上午9点开始进攻”。然而,一旦信使被派遣,他是否通过了山谷,第一位将军就不得而知了。任何一点的不确定性都会使得第一位将军攻击犹豫,因为如果第二位将军不能在同一时刻发动攻击,那座城市的驻军就会击退他的军队的进攻,导致他的军对被摧毁。

2)知道了这一点,第二位将军就需要发送一个确认回条:“我收到您的邮件,并会在9点的攻击。”但是,如果带着确认消息的信使被抓怎么办?所以第二位将军会犹豫自己的确认消息是否能到达。

3)于是,似乎我们还要让第一位将军再发送一条确认消息——“我收到了你的确认”。然而,如果这位信使被抓怎么办呢?

4)这样一来,是不是我们还要第二位将军发送一个“确认收到你的确认”的信息。

靠,于是你会发现,这事情很快就发展成为不管发送多少个确认消息,都没有办法来保证两位将军有足够的自信自己的信使没有被敌军捕获。
这个问题是无解的。两个将军问题和它的无解证明首先由E.A.Akkoyunlu,K.Ekanadham和R.V.Huber于1975年在《一些限制与折衷的网络通信设计》一文中发表,就在这篇文章的第73页中一段描述两个黑帮之间的通信中被阐明。 1978年,在Jim Gray的《数据库操作系统注意事项》一书中(从第465页开始)被命名为两个将军悖论。作为两个将军问题的定义和无解性的证明的来源,这一参考被广泛提及。

这个实验意在阐明:试图通过建立在一个不可靠的连接上的交流来协调一项行动的隐患和设计上的巨大挑战。

从工程上来说,一个解决两个将军问题的实际方法是使用一个能够承受通信信道不可靠性的方案,并不试图去消除这个不可靠性,但要将不可靠性削减到一个可以接受的程度。比如,第一位将军排出了100位信使并预计他们都被捕的可能性很小。在这种情况下,不管第二位将军是否会攻击或者受到任何消息,第一位将军都会进行攻击。另外,第一位将军可以发送一个消息流,而第二位将军可以对其中的每一条消息发送一个确认消息,这样如果每条消息都被接收到,两位将军会感觉更好。然而我们可以从证明中看出,他们俩都不能肯定这个攻击是可以协调的。他们没有算法可用(比如,收到4条以上的消息就攻击)能够确保防止仅有一方攻击。再者,第一位将军还可以为每条消息编号,说这是1号,2号……直到n号。这种方法能让第二位将军知道通信信道到底有多可靠,并且返回合适的数量的消息来确保最后一条消息被接收到。如果信道是可靠的话,只要一条消息就行了,其余的就帮不上什么忙了。最后一条和第一条消息丢失的概率是相等的。

两将军问题可以扩展成更变态的拜占庭将军问题 (Byzantine Generals Problem),其故事背景是这样的:拜占庭位于现在土耳其的伊斯坦布尔,是东罗马帝国的首都。由于当时拜占庭罗马帝国国土辽阔,为了防御目的,因此每个军队都分隔很远,将军与将军之间只能靠信差传消息。 在战争的时候,拜占庭军队内所有将军必需达成一致的共识,决定是否有赢的机会才去攻打敌人的阵营。但是,军队可能有叛徒和敌军间谍,这些叛徒将军们会扰乱或左右决策的过程。这时候,在已知有成员谋反的情况下,其余忠诚的将军在不受叛徒的影响下如何达成一致的协议,这就是拜占庭将军问题。

Paxos算法
Wikipedia上的各种Paxos算法的描述非常详细,大家可以去围观一下。

Paxos 算法解决的问题是在一个可能发生上述异常的分布式系统中如何就某个值达成一致,保证不论发生以上任何异常,都不会破坏决议的一致性。一个典型的场景是,在一个分布式数据库系统中,如果各节点的初始状态一致,每个节点都执行相同的操作序列,那么他们最后能得到一个一致的状态。为保证每个节点执行相同的命令序列,需要在每一条指令上执行一个「一致性算法」以保证每个节点看到的指令一致。一个通用的一致性算法可以应用在许多场景中,是分布式计算中的重要问题。从20世纪80年代起对于一致性算法的研究就没有停止过。

Notes:Paxos算法是莱斯利·兰伯特(Leslie Lamport,就是 LaTeX 中的”La”,此人现在在微软研究院)于1990年提出的一种基于消息传递的一致性算法。由于算法难以理解起初并没有引起人们的重视,使Lamport在八年后1998年重新发表到ACM Transactions on Computer Systems上(The Part-Time Parliament)。即便如此paxos算法还是没有得到重视,2001年Lamport 觉得同行无法接受他的幽默感,于是用容易接受的方法重新表述了一遍(Paxos Made Simple)。可见Lamport对Paxos算法情有独钟。近几年Paxos算法的普遍使用也证明它在分布式一致性算法中的重要地位。2006年Google的三篇论文初现“云”的端倪,其中的Chubby Lock服务使用Paxos作为Chubby Cell中的一致性算法,Paxos的人气从此一路狂飙。(Lamport 本人在 他的blog 中描写了他用9年时间发表这个算法的前前后后)

注:Amazon的AWS中,所有的云服务都基于一个ALF(Async Lock Framework)的框架实现的,这个ALF用的就是Paxos算法。我在Amazon的时候,看内部的分享视频时,设计者在内部的Principle Talk里说他参考了ZooKeeper的方法,但他用了另一种比ZooKeeper更易读的方式实现了这个算法。

简单说来,Paxos的目的是让整个集群的结点对某个值的变更达成一致。Paxos算法基本上来说是个**选举的算法——大多数的决定会成个整个集群的统一决定。任何一个点都可以提出要修改某个数据的提案,是否通过这个提案取决于这个集群中是否有超过半数的结点同意(所以Paxos算法需要集群中的结点是单数)。

这个算法有两个阶段(假设这个有三个结点:A,B,C):

第一阶段:Prepare阶段

A把申请修改的请求Prepare Request发给所有的结点A,B,C。注意,Paxos算法会有一个Sequence Number(你可以认为是一个提案号,这个数不断递增,而且是唯一的,也就是说A和B不可能有相同的提案号),这个提案号会和修改请求一同发出,任何结点在“Prepare阶段”时都会拒绝其值小于当前提案号的请求。所以,结点A在向所有结点申请修改请求的时候,需要带一个提案号,越新的提案,这个提案号就越是是最大的。

如果接收结点收到的提案号n大于其它结点发过来的提案号,这个结点会回应Yes(本结点上最新的被批准提案号),并保证不接收其它<n的提案。这样一来,结点上在Prepare阶段里总是会对最新的提案做承诺。

优化:在上述 prepare 过程中,如果任何一个结点发现存在一个更高编号的提案,则需要通知 提案人,提醒其中断这次提案。

第二阶段:Accept阶段

如果提案者A收到了超过半数的结点返回的Yes,然后他就会向所有的结点发布Accept Request(同样,需要带上提案号n),如果没有超过半数的话,那就返回失败。

当结点们收到了Accept Request后,如果对于接收的结点来说,n是最大的了,那么,它就会修改这个值,如果发现自己有一个更大的提案号,那么,结点就会拒绝修改。

我们可以看以,这似乎就是一个“两段提交”的优化。其实,2PC/3PC都是分布式一致性算法的残次版本,Google Chubby的作者Mike Burrows说过这个世界上只有一种一致性算法,那就是Paxos,其它的算法都是残次品。

我们还可以看到:对于同一个值的在不同结点的修改提案就算是在接收方被乱序收到也是没有问题的。

关于一些实例,你可以看一下Wikipedia中文中的“Paxos样例”一节,我在这里就不再多说了。对于Paxos算法中的一些异常示例,大家可以自己推导一下。你会发现基本上来说只要保证有半数以上的结点存活,就没有什么问题。

多说一下,自从Lamport在1998年发表Paxos算法后,对Paxos的各种改进工作就从未停止,其中动作最大的莫过于2005年发表的Fast Paxos。无论何种改进,其重点依然是在消息延迟与性能、吞吐量之间作出各种权衡。为了容易地从概念上区分二者,称前者Classic Paxos,改进后的后者为Fast Paxos。

总结
前面,我们说过,要想让数据有高可用性,就需要冗余数据写多份。写多份的问题会带来一致性的问题,而一致性的问题又会带来性能问题。从上图我们可以看到,我们基本上来说不可以让所有的项都绿起来,这就是著名的CAP理论:一致性,可用性,分区容忍性,你只可能要其中的两个。

NWR模型
最后我还想提一下Amazon Dynamo的NWR模型。这个NWR模型把CAP的选择权交给了用户,让用户自己的选择你的CAP中的哪两个。

所谓NWR模型。N代表N个备份,W代表要写入至少W份才认为成功,R表示至少读取R个备份。配置的时候要求W+R > N。 因为W+R > N, 所以 R > N-W 这个是什么意思呢?就是读取的份数一定要比总备份数减去确保写成功的倍数的差值要大。

也就是说,每次读取,都至少读取到一个最新的版本。从而不会读到一份旧数据。当我们需要高可写的环境的时候,我们可以配置W = 1 如果N=3 那么R = 3。 这个时候只要写任何节点成功就认为成功,但是读的时候必须从所有的节点都读出数据。如果我们要求读的高效率,我们可以配置 W=N R=1。这个时候任何一个节点读成功就认为成功,但是写的时候必须写所有三个节点成功才认为成功。

NWR模型的一些设置会造成脏数据的问题,因为这很明显不是像Paxos一样是一个强一致的东西,所以,可能每次的读写操作都不在同一个结点上,于是会出现一些结点上的数据并不是最新版本,但却进行了最新的操作。

所以,Amazon Dynamo引了数据版本的设计。也就是说,如果你读出来数据的版本是v1,当你计算完成后要回填数据后,却发现数据的版本号已经被人更新成了v2,那么服务器就会拒绝你。版本这个事就像“乐观锁”一样。

但是,对于分布式和NWR模型来说,版本也会有恶梦的时候——就是版本冲的问题,比如:我们设置了N=3 W=1,如果A结点上接受了一个值,版本由v1 -> v2,但还没有来得及同步到结点B上(异步的,应该W=1,写一份就算成功),B结点上还是v1版本,此时,B结点接到写请求,按道理来说,他需要拒绝掉,但是他一方面并不知道别的结点已经被更新到v2,另一方面他也无法拒绝,因为W=1,所以写一分就成功了。于是,出现了严重的版本冲突。

Amazon的Dynamo把版本冲突这个问题巧妙地回避掉了——版本冲这个事交给用户自己来处理。

于是,Dynamo引入了Vector Clock(矢量钟?!)这个设计。这个设计让每个结点各自记录自己的版本信息,也就是说,对于同一个数据,需要记录两个事:1)谁更新的我,2)我的版本号是什么。

下面,我们来看一个操作序列:

1)一个写请求,第一次被节点A处理了。节点A会增加一个版本信息(A,1)。我们把这个时候的数据记做D1(A,1)。 然后另外一个对同样key的请求还是被A处理了于是有D2(A,2)。这个时候,D2是可以覆盖D1的,不会有冲突产生。

2)现在我们假设D2传播到了所有节点(B和C),B和C收到的数据不是从客户产生的,而是别人复制给他们的,所以他们不产生新的版本信息,所以现在B和C所持有的数据还是D2(A,2)。于是A,B,C上的数据及其版本号都是一样的。

3)如果我们有一个新的写请求到了B结点上,于是B结点生成数据D3(A,2; B,1),意思是:数据D全局版本号为3,A升了两新,B升了一次。这不就是所谓的代码版本的log么?

4)如果D3没有传播到C的时候又一个请求被C处理了,于是,以C结点上的数据是D4(A,2; C,1)。

5)好,最精彩的事情来了:如果这个时候来了一个读请求,我们要记得,我们的W=1 那么R=N=3,所以R会从所有三个节点上读,此时,他会读到三个版本:

A结点:D2(A,2)
B结点:D3(A,2; B,1);
C结点:D4(A,2; C,1)
6)这个时候可以判断出,D2已经是旧版本(已经包含在D3/D4中),可以舍弃。

7)但是D3和D4是明显的版本冲突。于是,交给调用方自己去做版本冲突处理。就像源代码版本管理一样。

很明显,上述的Dynamo的配置用的是CAP里的A和P。

Golang语言解决动态规划问题

问题重现:

小Y最近在甜品店工作,其工作是切蛋糕。现在有n个顾客来购买蛋糕,并且每个顾客有一个到达的时间,以及需要买的蛋糕的长度ai。由于小Y每次只能服务一个顾客,【问题严谨性补充:而顾客如果进店没有服务员立刻为他服务,他将离开】所以对于相冲突的顾客没有办法提供服务。问小Y最多能为多少位顾客提供服务。小Y能够决定是否卖蛋糕给某个顾客。如果答应顾客要买长度为ai的切糕,那么小Y还要将蛋糕切成单位长度给顾客。如果对ai的蛋糕切成x和ai-x,所花的时间代价为x*(ai-x)。例如,当一个用户在1时刻,需要长度为4的蛋糕,此时小Y可以将其先切成2分长度为2的,花费为4,再将两段长度为2的分别切成1,1的,花费分别为1和1,则总花费时间为4+1+1 = 6, 则小Y为该用户服务时间为6.
已知第i位顾客进店时间,以及购买蛋糕大小。

分析:

涉及问题一:大小为n的蛋糕需要多长时间切成单位长度?
f(x)=x_(n-x) 绘制函数草图可以得到:x=1时得到最小值,也就是说每次1单位1单位的切
用数学归纳法证明上述的解得到的最终和解也是最小的
n=2时 f(x)显然得到的最小解
n=3时 f(x)显然得到的最小解
n=4时 f(x)显然得到的最小解
...
假设n=i-1时也能得到最小解
n=i时
切成x 和(i-x)
显然x必然是前面已经推出的n的一个解,i-x也是前面推出的一个解,而f(x)的最优解和f(i-x)就是每次1单位1单位的切
这时只要保证x_(i-x)值最小即可,最小情况x=1,也就证明了每次1单位1单位的切的解得到的最终和解也是最小的。

涉及问题二:已知各位顾客的进店时间和购买蛋糕大小,如何选出最佳服务对象?
这个问题至少可以使用贪心策略来解决,似乎包含了动态规划,看起来很像01背包问题
动态规划:
f[t]表示t时间内在前i个人已服务完的服务对象人数
s表示第i个人需要的服务时长
r表示第i个人达到时间点
㈠对于是否选择服务第i个人有两种情形
①选择,但要满足完成第i人的服务后时间不超过t
(如果选择了第i个人,可能就存在不允许选前i-1个人中的某些人)
f[t]=f[i-1][r]+1 【t>=r+s】
②不选,不改变在t时间的策略
(之所以不选的原因,就是因为第i个人到时,小Y还没有为前面的人服务完,又或者如果选了第i人会耽误后面更多的人)
f[t]=f[i-1][t]
㈡决策退出条件(决策既知条件)
时间没有负数,所以无需判断
i==0 返回1或0
解释:因为i==0即最后一个人,如果满足条件t >= r[0]+s[0],返回1
㈢决策入口条件
t=max{s+r}
于是可以得出:
状态转移方程为:f[t]=max{ f[i-1]t-s,
f[i-1][t]}

实现:

package main

import (
"fmt"
)

/*求最小服务时长,每次1单位1单位的切,得到的是最小解*/
func smin(n int32) int32 {
if n&1 == 0 {
return (n / 2) * (n - 1)
}
return (n - 1) / 2 * n
}

/*求每个顾客的时间*/
func serverTime(s, lenght []int32, maxLen int32) {
for i := range lenght {
s[i] = smin(lenght[i])
}
}

/*求二者最大值*/
func maxInt32(a, b int32) int32 {
if a > b {
return a
}
return b
}

func dptz(i, t int32, r, s []int32) int32 {
if i == 0 {
if t >= r[0]+s[0] {
return 1
}
return 0
}
if t >= r[i]+s[i] {
return maxInt32(dptz(i-1, r[i], r, s)+1, dptz(i-1, t, r, s))
}
return dptz(i-1, t, r, s)
}

/*求最后结束时间*/
func endTime(r, s []int32) int32 {
var max, tmp int32 = 0, 0
for i := range r {
tmp = r[i] + s[i]
if max < tmp {
max = tmp
}
}
return max
}

func main() {
//蛋糕长度、先来后到的时间和服务时间
length := []int32{2, 2, 3, 4}
r := []int32{5, 5, 6, 10}
s := make([]int32, 4)
serverTime(s, length, 4)
fmt.Println(dptz(3, endTime(r, s), r, s))
}
output==>
3

Golang中互斥锁与读写锁

golang中读写锁sync.RWMutex和互斥锁sync.Mutex区别

golang中sync包实现了两种锁Mutex (互斥锁)和RWMutex(读写锁),其中RWMutex是基于Mutex实现的,只读锁的实现使用类似引用计数器的功能.

  • Mutex
    定义:互斥锁是传统的并发程序对共享资源进行访问控制的主要手段。互斥锁中Lock()加锁,Unlock()解锁,使用Lock()加锁后,便不能对其重复加锁,直到利用Unlock()对其解锁后,才能再次加锁;如果在使用Unlock()前未加锁,就会引起一个运行错误.

    适用场景:读写不确定,即读写次数没有明显的区别,并且只允许一个读或者写的场景,所有又称全局锁。

    示例:
    package main
    import (
    "time"
    "fmt"
    "sync"
    )
    func main(){
    var mutex sync.Mutex
    fmt.Println("Lock the lock")
    mutex.Lock()
    fmt.Println("The lock is locked")
    for i:=1;i<4;i++{
        go func(i int){
            fmt.Println("Not lock",i)
            mutex.Lock()
            fmt.Println("Locked",i)
        }(i)
    }
    time.Sleep(time.Second)
    fmt.Println("Unlock the lock")
    mutex.Unlock()
    time.Sleep(time.Second)
    }
    output==>
    Lock the lock
    The lock is locked
    Not lock 1
    Not lock 2
    Not lock 3
    Unlock the lock
    Locked 1
    
    
在需要频繁读,少量写的时候,Mutex的性能比使用channel要高很多,同时还能保证读写同步。
package main

import (
    "fmt"
    "runtime"
    "sync"
)
type counter struct{
    mutex sync.Mutex
    x int64
}
func (c *counter) Inc(){
    c.mutex.Lock()
    defer c.mutex.Unlock()
    c.x++
}
func main(){
    runtime.GOMAXPROCS(runtime.NumCPU())
    c := counter{}
    var wait sync.WaitGroup
    wait.Add(4)
    for k :=4;k> 0;k--{
        go func(){
            for i :=200000;i>0;i--{
                c.Inc()
            }
            wait.Done()
        }()
    }
    wait.Wait()
    fmt.Println(c.x)
}
output==>
800000
  • RWMutex
    定义:它允许任意读操作同时进行 同一时刻,只允许有一个写操作进行.并且一个写操作被进行过程中,读操作的进行也是不被允许的,读写锁控制下的多个写操作之间都是互斥的,写操作与读操作之间也都是互斥的,多个读操作之间却不存在互斥关系.RWMutex是一个读写锁,该锁可以加多个读锁或者一个写锁。写锁,如果在添加写锁之前已经有其他的读锁和写锁,则lock就会阻塞直到该锁可用,为确保该锁最终可用,已阻塞的 Lock 调用会从获得的锁中排除新的读取器,即写锁权限高于读锁,有写锁时优先进行写锁定。写锁解锁,如果没有进行写锁定,则就会引起一个运行时错误.注意:写解锁在进行的时候会试图唤醒所有因欲进行读锁定而被阻塞的Goroutine,也就是在所有写锁上锁之前存在的并且被迫停止的读锁将重新开始工作,读解锁在进行的时候只会在已无任何读锁定的情况下试图唤醒一个因欲进行写锁定而被阻塞的Goroutine。

    适用场景:经常用于读次数远远多于写次数的场景.

    示例:

    package main
    //程序中RUnlock()个数不得多于Rlock()的个数
    import (
    "fmt"
    "sync"
    )
    func main(){
    var g *sync.RWMutex
    g = new(sync.RWMutex)
    g.RLock()
    g.RLock()
    g.RUnlock()
    g.RLock()
    fmt.Println("g")
    g.RUnlock() 
    }
    output==>
    g
    
    package main
    import (
    "fmt"
    "sync"
    "time"
    "os"
    "errors"
    "io"
    )
    type DataFile interface {
    //读取一个数据块
    Read()(rsn int64,d Data,err error)
    // 写入一个数据块
    Write(d Data)(wsn int64,err error)
    // 获取最后读取的数据块的序列号
    Rsn() int64
    // 获取最后写入的数据块的序列号
    Wsn() int64
    // 获取数据块的长度
    DataLen() uint32
    }
    //数据类型
    type Data []byte
    //数据文件的实现类型
    type myDataFile struct {
    f *os.File  //文件
    fmutex sync.RWMutex //被用于文件的读写锁
    woffset int64 // 写操作需要用到的偏移量
    roffset int64 // 读操作需要用到的偏移量
    wmutex sync.Mutex // 写操作需要用到的互斥锁
    rmutex sync.Mutex // 读操作需要用到的互斥锁
    dataLen uint32 //数据块长度
    }
    //初始化DataFile类型值的函数,返回一个DataFile类型的值
    func NewDataFile(path string, dataLen uint32) (DataFile, error){
    f, err := os.OpenFile(path, os.O_APPEND|os.O_RDWR|os.O_CREATE, 0666)
    //f,err := os.Create(path)
    if err != nil {
        fmt.Println("Fail to find",f,"cServer start Failed")
        return nil, err
    }
    
    if dataLen == 0 {
        return nil, errors.New("Invalid data length!")
    }
    
    df := &myDataFile{
        f : f,
        dataLen:dataLen,
    }
    
    return df, nil
    }
    //获取并更新读偏移量,根据读偏移量从文件中读取一块数据,把该数据块封装成一个Data类型值并将其作为结果值并return
    func (df *myDataFile) Read() (rsn int64, d Data, err error){
    // 读取并更新读偏移量
    var offset int64
    // 读互斥锁定
    df.rmutex.Lock()
    offset = df.roffset
    // 更改偏移量, 当前偏移量+数据块长度
    df.roffset += int64(df.dataLen)
    // 读互斥解锁
    df.rmutex.Unlock()
    
    //读取一个数据块,最后读取的数据块序列号
    rsn = offset / int64(df.dataLen)
    bytes := make([]byte, df.dataLen)
    for {
        //读写锁:读锁定
        df.fmutex.RLock()
        _, err = df.f.ReadAt(bytes, offset)
        if err != nil {
            //由于进行写操作的Goroutine比进行读操作的Goroutine少,所以过不了多久读偏移量roffset的值就会大于写偏移量woffset的值
            // 也就是说,读操作很快就没有数据块可读了,这种情况会让df.f.ReadAt方法返回的第二个结果值为代表的非nil且会与io.EOF相等的值
            // 因此不应该把EOF看成错误的边界情况
            // so 在读操作读完数据块,EOF时解锁读操作,并继续循环,尝试获取同一个数据块,直到获取成功为止.
            if err == io.EOF {
                //注意,如果在该for代码块被执行期间,一直让读写所fmutex处于读锁定状态,那么针对它的写操作将永远不会成功.
                //切相应的Goroutine也会被一直阻塞.因为它们是互斥的.
                // so 在每条return & continue 语句的前面加入一个针对该读写锁的读解锁操作
                df.fmutex.RUnlock()
                //注意,出现EOF时可能是很多意外情况,如文件被删除,文件损坏等
                //这里可以考虑把逻辑提交给上层处理.
                continue
            }
        }
        break
    }
    d = bytes
    df.fmutex.RUnlock()
    return
    }
    func (df *myDataFile) Write(d Data) (wsn int64, err error){
    //读取并更新写的偏移量
    var offset int64
    df.wmutex.Lock()
    offset = df.woffset
    df.woffset += int64(df.dataLen)
    df.wmutex.Unlock()
    //写入一个数据块,最后写入数据块的序号
    wsn = offset / int64(df.dataLen)
    var bytes []byte
    if len(d) > int(df.dataLen){
        bytes = d[0:df.dataLen]
    }else{
        bytes = d
    }
    df.fmutex.Lock()
    df.fmutex.Unlock()
    _, err = df.f.Write(bytes)
    return
    }
    func (df *myDataFile) Rsn() int64{
    df.rmutex.Lock()
    defer df.rmutex.Unlock()
    return df.roffset / int64(df.dataLen)
    }
    func (df *myDataFile) Wsn() int64{
    df.wmutex.Lock()
    defer df.wmutex.Unlock()
    return df.woffset / int64(df.dataLen)
    }
    func (df *myDataFile) DataLen() uint32 {
    return df.dataLen
    }
    func main(){
    //简单测试下结果
    var dataFile DataFile
    dataFile,_ = NewDataFile("./mutex_2016_3-21.dat", 10)
    var d=map[int]Data{
        1:[]byte("batu_test1"),
        2:[]byte("batu_test2"),
        3:[]byte("test1_batu"),
    }
    //写入数据
    for i:= 1; i < 4; i++ {
        go func(i int){
            wsn,_ := dataFile.Write(d[i])
            fmt.Println("write i=",i,",wsn=",wsn," ,success.")
        }(i)
    }
    
    //读取数据
    for i:= 1; i < 4; i++ {
        go func(i int){
            rsn,d,_ := dataFile.Read()
            fmt.Println("Read i=",i,",rsn=",rsn,",data=",d," success.")
        }(i)
    }
    
    time.Sleep(10 * time.Second)
    }
    output==>
    write i= 1 ,wsn= 0  ,success.
    write i= 2 ,wsn= 1  ,success.
    write i= 3 ,wsn= 2  ,success.
    Read i= 1 ,rsn= 0 ,data= [98 97 116 117 95 116 101 115 116 49]  success.
    Read i= 2 ,rsn= 1 ,data= [98 97 116 117 95 116 101 115 116 50]  success.
    Read i= 3 ,rsn= 2 ,data= [116 101 115 116 49 95 98 97 116 117]  success.
    

Golang之text/template包

Golang之text/template包

概述

定义
传入string–最简单的替换
传入struct
多模板,介绍New,Name,Lookup
文件模板,介绍ParseFiles
文件模板,介绍ParseGlob
模板的输出,介绍ExecuteTemplate和Execute
模板的复用
模板的回车

定义

模板就是将一组文本嵌入另一组文本里

传入string–最简单的替换

package main

import (
    "os"
    "text/template"
)

func main() {
    name := "jason"
    tmpl, err := template.New("test").Parse("hello, {{.}}") //建立一个模板,内容是"hello, {{.}}"
    if err != nil {   
            panic(err)
    }   
    err = tmpl.Execute(os.Stdout, name)  //将string与模板合成,变量name的内容会替换掉{{.}} 
    //合成结果放到os.Stdout里
    if err != nil {
            panic(err)
    }   
}
//输出 :   hello, jason

传入struct

模板合成那句,第2个参数是interface{},所以可以传入任何类型,现在传入struct看看
要取得struct的值,只要使用成员名字即可.

package main

import (
    "os"
    "text/template"
)

type Inventory struct {
    Material string
    Count    uint
}

func main() {
    sweaters := Inventory{"wool", 17} 
    muban := "{{.Count}} items are made of {{.Material}}"
    tmpl, err := template.New("test").Parse(muban)  //建立一个模板
    if err != nil {   
            panic(err)
    }   
    err = tmpl.Execute(os.Stdout, sweaters) //将struct与模板合成,合成结果放到os.Stdout里
    if err != nil {
            panic(err)
    }   
}
//输出 :   17 items are made of wool

多模板,介绍New,Name,Lookup

//一个模板可以有多种,以Name来区分
muban_eng := "{{.Count}} items are made of {{.Material}}"
muban_chn := "{{.Material}}做了{{.Count}}个项目"
//建立一个模板的名称是china,模板的内容是muban_chn字符串
tmpl, err := template.New("china")
tmpl, err = tmpl.Parse(muban_chn)
//建立一个模板的名称是english,模板的内容是muban_eng字符串
tmpl, err = tmpl.New("english")
tmpl, err = tmpl.Parse(muban_eng)
//将struct与模板合成,用名字是china的模板进行合成,结果放到os.Stdout里,内容为“wool做了17个项目”
err = tmpl.ExecuteTemplate(os.Stdout, "china", sweaters)
//将struct与模板合成,用名字是china的模板进行合成,结果放到os.Stdout里,内容为“17 items are made of wool”
err = tmpl.ExecuteTemplate(os.Stdout, "english", sweaters)

tmpl, err = template.New("english")
fmt.Println(tmpl.Name())  //打印出english
tmpl, err = tmpl.New("china")
fmt.Println(tmpl.Name())  //打印出china
tmpl=tmpl.Lookup("english")//必须要有返回,否则不生效
fmt.Println(tmpl.Name())  //打印出english

文件模板,介绍ParseFiles

//模板可以是一行
muban := "{{.Count}} items are made of {{.Material}}"
//也可以是多行
muban := `items number is {{.Count}}
there made of {{.Material}}
`

把模板的内容发在一个文本文件里,用的时候将文本文件里的所有内容赋值给muban这个变量即可
上面的想法可以自己实现,但其实tamplate包已经帮我们封装好了,那就是template.ParseFiles方法

假设有一个文件mb.txt的内容是muban变量的内容
$cat mb.txt
{{.Count}} items are made of {{.Material}}

那么下面2行
muban := "{{.Count}} items are made of {{.Material}}"
tmpl, err := template.New("test").Parse(muban)  //建立一个模板

等价于
tmpl, err := template.ParseFiles("mb.txt")  //建立一个模板,这里不需要new("name")的方式,因为name自动为文件名

文件模板,介绍ParseGlob

ParseFiles接受一个字符串,字符串的内容是一个模板文件的路径(绝对路径or相对路径)
ParseGlob也差不多,是用正则的方式匹配多个文件

假设一个目录里有a.txt b.txt c.txt的话
用ParseFiles需要写3行对应3个文件,如果有一万个文件呢?
而用ParseGlob只要写成template.ParseGlob("*.txt") 即可

模板的输出,介绍ExecuteTemplate和Execute

模板下有多套模板,其中有一套模板是当前模板
可以使用Name的方式查看当前模板

err = tmpl.ExecuteTemplate(os.Stdout, "english", sweaters)  //指定模板名,这次为english
err = tmpl.Execute(os.Stdout, sweaters)  //模板名省略,打印的是当前模板

模板的复用

模板里可以套模板,以达到复用目的,用template关键字

muban1 := `hi, {{template "M2"}},
hi, {{template "M3"}}
`
muban2 := "我是模板2,{{template "M3"}}"
muban3 := "ha我是模板3ha!"

tmpl, err := template.New("M1").Parse(muban1)
tmpl.New("M2").Parse(muban2)
tmpl.New("M3").Parse(muban3)
err = tmpl.Execute(os.Stdout, nil)

完整代码:

package main

import (
    "os"
    "text/template"
)

func main() {
    muban1 := `hi, {{template "M2"}},
hi, {{template "M3"}}
`
    muban2 := `我是模板2,{{template "M3"}}`
    muban3 := "ha我是模板3ha!"

    tmpl, err := template.New("M1").Parse(muban1)
    if err != nil {
            panic(err)
    }   
    tmpl.New("M2").Parse(muban2)
    if err != nil {
            panic(err)
    }   
    tmpl.New("M3").Parse(muban3)
    if err != nil {
            panic(err)
    }   
    err = tmpl.Execute(os.Stdout, nil)
    if err != nil {
            panic(err)
    }   
}
output==>
hi, 我是模板2,ha我是模板3ha!,
hi, ha我是模板3ha!

模板的回车

模板文件里的回车也是模板的一部分,如果对回车位置控制不好,合成出来的文章会走样 .

const letter = ` Dear {{.Name}},

{{if .Attended}}It was a pleasure to see you at the wedding.
如果Attended是true的话,这句是第二行{{else}}It is a shame you couldn't make it to the wedding.
如果Attended是false的话,这句是第二行{{end}}
{{with .Gift}}Thank you for the lovely {{.}}.
{{end}}
Best wishes,
Josie `

解析:
Dear某某某的Dear应该是在第一行,所以在D前面不能有回车,否则Dear会跑到第2行去
所以Dear要紧贴`
信件的称唿和正文有一行空行,最好显式的打出一行,而标准库里的回车是包在if里,成为正文的一部分,这样排版容易出错
正确的正文排版如下
如果正文就一行,要把true和false的所有内容都写在一行
比如{{if .Attended}}true line,hello true{{else}}false line,hi false{{end}}
如果正文有多行,就等于把一行拆成多行
会发现true的最后一行和false的第一行是在同一行
{{if .Attended}}和ture的第一行在同一行
{{end}}和false的最后一行在同一行
如下:

{{if .Attended}}true line
hello true{{else}}false line
hi false{{end}}

关于{{with .Gift}},意思是如果Gift不是为空的话,就打印整行,如果为空,就不打印
只有这样写法,with对应的end要写在第2行,才会把“Thank you”这句后面带一个回车进去,这样写法,就像“Thank you”这句是插在正文下面的
只有这样写,不管有没有“Thank you”,正文和Best wishes,之间始终只有1行空白

Docker背后的内核知识——CGroups资源限制

Docker背后的内核知识——CGroups资源限制

CGroup 介绍

CGroup 是 Control Groups 的缩写,是 Linux 内核提供的一种可以限制、记录、隔离进程组 (process groups) 所使用的物力资源 (如 cpu memory i/o 等等) 的机制。2007 年进入 Linux 2.6.24 内核,CGroups 不是全新创造的,它将进程管理从 cpuset 中剥离出来,作者是 Google 的 Paul Menage。CGroups 也是 LXC 为实现虚拟化所使用的资源管理手段。CGroup 技术被广泛用于 Linux 操作系统环境下的物理分割,是 Linux Container 技术的底层基础技术,是虚拟化技术的基础。

CGroup 功能及组成

CGroup 是将任意进程进行分组化管理的 Linux 内核功能。CGroup 本身是提供将进程进行分组化管理的功能和接口的基础结构,I/O 或内存的分配控制等具体的资源管理功能是通过这个功能来实现的。这些具体的资源管理功能称为 CGroup 子系统或控制器。CGroup 子系统有控制内存的 Memory 控制器、控制进程调度的 CPU 控制器等。运行中的内核可以使用的 Cgroup 子系统由/proc/cgroup 来确认。
CGroup 提供了一个 CGroup 虚拟文件系统,作为进行分组管理和各子系统设置的用户接口。要使用 CGroup,必须挂载 CGroup 文件系统。这时通过挂载选项指定使用哪个子系统。

CGroups的作用

实现cgroups的主要目的是为不同用户层面的资源管理,提供一个统一化的接口。从单个进程的资源控制到操作系统层面的虚拟化。Cgroups提供了以下四大功能{![参照自:http://en.wikipedia.org/wiki/Cgroups]}。

  • 资源限制(Resource Limitation):cgroups可以对进程组使用的资源总额进行限制。如设定应用运行时使用内存的上限,一旦超过这个配额就发出OOM(Out of Memory)。

  • 优先级分配(Prioritization):通过分配的CPU时间片数量及硬盘IO带宽大小,实际上就相当于控制了进程运行的优先级。

  • 资源统计(Accounting): cgroups可以统计系统的资源使用量,如CPU使用时长、内存用量等等,这个功能非常适用于计费。

  • 进程控制(Control):cgroups可以对进程组执行挂起、恢复等操作。

    CGroup 相关概念解释

  • 任务(task)。在 cgroups 中,任务就是系统的一个进程;

  • 控制族群(control group)。控制族群就是一组按照某种标准划分的进程。Cgroups 中的资源控制都是以控制族群为单位实现。一个进程可以加入到某个控制族群,也从一个进程组迁移到另一个控制族群。一个进程组的进程可以使用 cgroups 以控制族群为单位分配的资源,同时受到 cgroups 以控制族群为单位设定的限制;

  • 层级(hierarchy)。控制族群可以组织成 hierarchical 的形式,既一颗控制族群树。控制族群树上的子节点控制族群是父节点控制族群的孩子,继承父控制族群的特定的属性;
    子系统(subsystem)。一个子系统就是一个资源控制器,比如 cpu 子系统就是控制 cpu 时间分配的一个控制器。子系统必须附加(attach)到一个层级上才能起作用,一个子系统附加到某个层级以后,这个层级上的所有控制族群都受到这个子系统的控制。

相互关系

  • 每次在系统中创建新层级时,该系统中的所有任务都是那个层级的默认 cgroup(我们称之为 root cgroup,此 cgroup 在创建层级时自动创建,后面在该层级中创建的 cgroup 都是此 cgroup 的后代)的初始成员;

  • 一个子系统最多只能附加到一个层级;

  • 一个层级可以附加多个子系统;

  • 一个任务可以是多个 cgroup 的成员,但是这些 cgroup 必须在不同的层级;

  • 系统中的进程(任务)创建子进程(任务)时,该子任务自动成为其父进程所在 cgroup 的成员。然后可根据需要将该子任务移动到不同的 cgroup 中,但开始时它总是继承其父任务的 cgroup。

    CGroup特点

  • 在 cgroups 中,任务就是系统的一个进程。

  • 控制族群(control group)。控制族群就是一组按照某种标准划分的进程。Cgroups 中的资源控制都是以控制族群为单位实现。一个进程可以加入到某个控制族群,也从一个进程组迁移到另一个控制族群。一个进程组的进程可以使用 cgroups 以控制族群为单位分配的资源,同时受到 cgroups 以控制族群为单位设定的限制。

  • 层级(hierarchy)。控制族群可以组织成 hierarchical 的形式,既一颗控制族群树。控制族群树上的子节点控制族群是父节点控制族群的孩子,继承父控制族群的特定的属性。

  • 子系统(subsytem)。一个子系统就是一个资源控制器,比如 cpu 子系统就是控制 cpu 时间分配的一个控制器。子系统必须附加(attach)到一个层级上才能起作用,一个子系统附加到某个层级以后,这个层级上的所有控制族群都受到这个子系统的控制。

漫谈服务化、微服务

漫谈服务化、微服务

这两个概念其实很早之前就听说了,近一年也一直在查找相关的资料。并且有意无意地读了一些相关的书籍。

前几天在《程序员的呐喊》里看到亚马逊在2002年的时候,是贝索斯突然向公司程序员们发出以下指令:

1.从今天起,所有的团队都要以服务接口的方式提供数据和各种功能。

2.团队之间必须通过接口来通信。

3.不允许任何其他形式的互操作:不允许直接链接,不允许直接读其他团队的数据,不允许共享内存,不允许任何形式的后门。唯一许可的通信方式就是通过网络调用服务。

4.至于具体的技术不做规定。HTTP、Corba、Pubsub、自定义协议都可以,贝索斯不关心这个。

5.所有的服务接口,必须从一开始就要以可以公开为设计导向,没有例外。这就是说,团队必须在设计的时候就计划好,接口要可以对外面的开发人员开放,没有讨价还价的余地。

6.不听话的人会被炒鱿鱼。 

老实说看到这段的时候我是震惊的,当然我和作者震惊的点不一样。因为之前听说巨头阿里也是在2009年才开始进行自己公司内部的服务化拆分(仔细想想,阿里还真是和亚马逊差不多啊哈哈),然后一对比,两者做这件事情的时间跨度差了7年之久。你可以想象,实际上技术上的理念,国内和国际大公司还是有很大的差距(当然了,时至今日阿里可以靠自己的业务量,比如每秒14w订单什么的来声称自己比亚马逊牛逼)。

当然,说归说,我们也可以看到,贝索斯做这事儿的目的并不单纯。不是从什么项目开发方便,责任分工明确之类的角度来考虑做网站的服务化拆分,而是考虑未来向平台公司进化而为之做一些前期准备。再想一想,SOA和微服务在近些年被吹起来好像最初的时候也并不是因为技术原因2333

之前在dc的时候还阅读过一本厚厚的《building microservices》,但其实那边书总结一下的话,只有这么几点:

做好服务拆分

不要直接去访问非本模块的数据,也就是不要直接去读其它服务的数据库,比如订单服务,不要去读商品或者用户的数据

直接用其它模块提供的api来获得上述数据

做好SSO(单点登陆)

处理好分布式事务

内容很少,但却扯了一大厚本。。老外就是厉害。

不小心扯远了。

说到服务发现这件事情,最早还是在dc的时候听同事提起,当时dc的内部站点基本都是php构成,但是因为我们的业务看起来貌似好像是稳定了技术团队没什么事做了(误)所以要做一些为未来着想的事情。所以就想到了电商网站做大了以后都考虑要做的几件事情:

1.服务拆分

2.服务发现

3.服务治理  

在调研了很久之后,发现这件事情实际要做起来还有很多方面之前没有考虑到。比如一个靠谱的api gateway,至于什么是api gateway,比如这篇:

https://www.nginx.com/blog/building-microservices-using-an-api-gateway/
所以看起来如果我们要切换成为现代化的架构,首先需要把服务给拆了。把服务拆分之后,还需要顺便再实现一个php或其它语言的网关,来调用我们拆分好的这些服务。其职责是把细粒度的服务组装成为一个大的服务,向终端用户提供最终的api。

当然有了整体的思路,继续调研你会发现,实现服务间调用的方法在不同语言里思路不太一样。

比如我们老态龙钟的java,虽然已经老了,但是这方面的框架却非常地完善。国内有阿里开源的一整套解决方案dubbo,只要方便地进行一些配置,那调用远程服务写起来就像调用本地代码。再去看一下dubbo所做的服务发布和订阅的原理,又是基于zookeeper。再看一下zookeeper,你发现在java里这些运行着的服务和zookeeper之间建立的是长连接!而基于这些长连接,简单的基于某个键值对的推拉操作简直是简单到暴啊。

反观php,要做到这件事情貌似并不容易。这要从php本身的运行机制讲起。

一般我们的php应用会将完整的项目代码部署在线上,以现在最流行或者说已经是事实标准的nginx+phpfpm的方式来讲。实际上一个http请求发过来,要经过像下面这样的链路:

GET /url  
|
|
|
Nginx  
|
| proxy_pass
|
php-fpm运行端口/unix socket  
|
|
php-fpm解析http请求  
|
| 将请求路径和内容传给web框架
|
php的web框架的index.php  
|
| 按照路由规则映射到controller层
|
具体的controller内的函数
|
| 返回请求,并释放资源
|

每一次新的http请求来到服务器端的时候,都会经历这个过程,不会节省任何一步(当然,可以开opcache来进行一些代码缓存)。如果了解其它语言的框架的话,会发现比较显著的不同之处,比如java(java不是特别了解,说错了请海涵)或者golang的Web框架,其资源被初始化后,有一些信息就已经在内存中了。所以当请求来到的时候,不需要从头开始重新构造一遍,而可以直接在内存中进行路由的字符串匹配,并映射到对应的函数/方法中去。所以对于这种能够长驻内存的web框架来说,可以实现很多php框架做不了的事情,比如数据库连接池、或者基于请求的程序内缓存(这样你甚至都不需要使用memcache,当然我只是说对于小网站而言)等等。

所以你可以理解了,因为我们每次的程序都是运行完成之后析构了全部的资源。你没法使用代码级别的全局变量来做什么事情,更别说和其它的组件去建立长连接了(新兴的swoole可能可以解决这种问题,但应用很少,不太愿意贸然踩坑)。这样的缺陷使得如果我们想要建立像java那样能够通过订阅动态、低延迟地对其它服务进行感知的系统非常的困难。

那么在php里,如果你想要调用其它资源,一般是怎么做的呢?拿我现在所在的滴滴举例,这些资源都会被OP们配置在nginx里,比如你要上线一个什么系统或者什么服务,那么这些服务都会强依赖于nginx。而这些配置又都是OP们人肉配置在多台机器上(当然,OP也不是傻子,因为nginx本身也是多台机器的集群,在配置管理的时候,OP也使用了gitlab+jenkins进行服务的修改/发布/版本控制)。但还存在一些恶心的问题,比如一台服务器down了怎么办?虽然我们可爱的op用nginx的upstream模块也似乎是解决了这个问题,这个模块可以动态地进行服务器的存活检测,如果某台机器的某个服务一小段时间没响应,那么会被upstream自动剔除出负载均衡的列表。实际试用的效果来看,这种方式存在一定时间的延迟,可能有几秒钟的时候服务器会给你返回502。如果你是一家电商,我觉得你大概可以估算出来高峰期的这几秒意味着什么了。但最麻烦的其实还是这些本来和OP没什么关系的东西需要OP去管理,如果你用java或者golang,OP只要配置好zookeeper或者etcd/consul集群就好了,剩下的跟OP也没什么关系了。只要机器坏了的时候报个警然后OP们去报修一下,嗯。看看现在nginx集群里一个大业务线的服务要几百台服务器,还都是op人肉录入进去,想想都觉得蛋疼。

所以php要做这件事情,其实并不是太合适。如果非要纠结于语言,想要在php里实现这件事情要怎么办呢?那么需要寻找一个支持发布/订阅的类似于zk的分布式kv,并且这个kv需要支持以客户端client的形式部署在所有的业务代码机器上,最好是直接预装,省得部署的时候麻烦。但这样依然没法解决节点挂了以后的自动剃除问题。

为了别给自己添堵还是别这么做了。。

既然php原生并不支持这些东西,那换一门语言就好了。

参考

http://xargin.com/about-microservice-1/

用Java代码制作简单注册码

用Java代码制作简单注册码

简单注册码一般都基于主机服务器CPU,网卡,及磁盘等信息进行加密转码。本文基于此给出一个简单的示例:


import java.net.InetAddress;   
import java.net.NetworkInterface;   
import java.net.UnknownHostException;   
  
public class testjacoxu {   
    public static void SmsBaseLoadConfig(){   
        String registerStr = config.getValue(“registerCode”);   
        long registerCode = Long.parseLong(extractNumberCharacter(registerStr));   
        if (!register()) {   
            log.error(“======= Register failure, please contact with developer! ========”);   
            log.error(“============ Author: Jacob Xu, Email:[email protected]! ============”);   
            long tmpCode = 1987;   
            try {   
                tmpCode = Long.parseLong(getMACAddress(InetAddress.getLocalHost()), 16);   
            } catch (NumberFormatException e) {   
                tmpCode = 1987;   
            } catch (Exception e) {   
                tmpCode = 1987;   
            }   
            log.error(“===Sent the number:”+tmpCode+“ to E-mail===”);   
            Thread.sleep(60000);   
            System.exit(1);   
        }   
    }   
    public static String extractNumberCharacter(String ss){   
        Boolean lastCharTag = true;   
        StringBuffer str = new StringBuffer();   
        char[] ch = ss.toCharArray();   
        for (int i = 0; i < ch.length; i++) {   
            if(isNumber(ch[i])){   
                if(lastCharTag){   
                    str.append(ch[i]);   
                }   
                else{   
                    str.append(ch[i]);   
                    lastCharTag = true;   
                }   
            }   
            else{   
                lastCharTag = false;   
            }   
        }   
        if(str.toString().length() == 0){   
            return null;   
        }   
        return str.toString();   
    }   
       
    public static final boolean isNumber(char c){   
        return ((c<=’9‘)&&(c>=’0‘))?true:false;   
    }   
       
    public static String getMACAddress(InetAddress ia)throws Exception{   
        //获得网络接口对象(即网卡),并得到mac地址,mac地址存在于一个byte数组中。   
        byte[] mac = NetworkInterface.getByInetAddress(ia).getHardwareAddress();   
           
        //下面代码是把mac地址拼装成String   
        StringBuffer sb = new StringBuffer();       
        for(int i=0;i

文本基本想法就是获取主机IP,转成10进制long型数值,然后进行简单运算(数值+1987)/428。然后与过滤掉字符的注册码进行直接匹配,若匹配成功则注册成功,如匹配失败,则留下作者邮箱,并退出程序。,当然这个计算非常简单也很容易破解,一个好的转码应该是无法直接逆转的。

参考

http://jacoxu.com/?p=834

数据结构之栈的定义及实现

栈的定义及实现

这里的栈的实现采用的是复用顺序表和单向链表的方式。

栈的基本定义

  1. 栈是一种特殊的线性表,只能从固定的方向进出,而且栈进出的基本原则是:先进栈的元素后出栈。
  2. 对栈顶栈底的定义:

    栈顶:允许操作的一端;栈底:不允许操作的一端。
    ###栈的基本实现方式
    ####1.顺序栈的实现
    1.首先定义的顺序栈中的数据结点的结构,主要包括两个部分,一部分是数据元素,另一部分是顺序栈的长度。

具体代码如下:

typedef struct _tag_stack_  
{  
    int a[20];  
    int top;  
}Sqstack; 

2.使用顺序栈之前要先初始化顺序栈。

主要是为顺序栈结点分配一个空间,然后将顺序栈的长度初始化为0.

Sqstack* InitStack ()  
{  
    Sqstack *ret = NULL;  
    ret = (Sqstack*)malloc (sizeof(Sqstack));  
    if (ret)  
    {  
        /*将栈的长度初始化为0*/   
        ret -> top = 0;  
    }  
    return ret;  
}  

3.将元素压入栈,这里采用复用方式。

int Push(Sqstack *stack, int data)  
{  
    /*这里有一个复用方式,也就是顺序栈的长度和数组的下标进行复用s*/  
    stack -> a[stack -> top] = data;  
    stack -> top++;  
    return 1;  
}

4.将已经在栈中的元素进行打印,因为栈不是只是一种存储数据的结构,所以我们不经过弹出栈中的元素也是可以访问到栈中的元素的。

void Play (Sqstack *stack)  
{  
    int i = 0;  
    if (stack -> top == 0)  
    {  
        printf ("It is empty\n");  
    }  
    /*stack -> top,栈的长度*/  
    else  
    {  
        for (i = 0; i < stack -> top; i++)  
        {  
            printf ("栈中的数据为:%d\n", stack -> a[i]);  
        }  
    }  
}

5.数据结点出栈

int Pop (Sqstack *stack, int *data)  
{  
    if (stack -> top == 0)  
    {  
        printf ("the stack is empty\n");  
        printf ("弹出已经被改变了的u的值");  
    }  
    else  
    {     
        stack -> top--;  
        *data = stack -> a[stack -> top];  
    }  
    return 1;     
}  

6.测试部分代码如下:

int main()  
{     
    int h = 4;  
    int p = 0;  
    int i = 0;  
    int u = 3;  
    Sqstack* qq;  
      
    qq = InitStack();  
              
    for (i = 0; i < 5; i++)  
    {  
        Push (qq, i);  
    }  
    Play (qq);  
      
    /*弹出操作*/  
    Pop (qq, &u);  
    printf ("弹出的元素是:%d\n",u);  
    Pop (qq, &u);  
    printf ("弹出的元素是:%d\n",u);  
    Pop (qq, &u);  
    printf ("弹出的元素是:%d\n",u);  
    Pop (qq, &u);  
    printf ("弹出的元素是:%d\n",u);  
    Pop (qq, &u);  
    printf ("弹出的元素是:%d\n",u);  
    Pop (qq, &u);  
    printf ("%d\n",u);  
  
    return 1;  
}  

7.虽然顺序栈实现了栈的基本功能,毕竟是顺序存储结构,而且占用的内存空间也必须是连续的,所以还是有一定的局限性的。

综上所述,可以将顺序表实现栈的建立和进栈出栈的过程用下面的代码总结。

    #include   
    #include "1.h"  
    #include "SeqList.h"   
      
    /******************************************************************************* 
    *函数名: SeqStack_Create 
    *参数:capacity 栈中元素的个数  
    *返回值:SeqStack*类型,是一个void*类型,然后再由接收函数进行强制类型转换  
    *功能:创建顺序栈,调用顺序表创建函数  
    *******************************************************************************/   
    SeqStack* SeqStack_Create(int capacity)  
    {  
        return SeqList_Create(capacity);  
    }  
      
    /******************************************************************************* 
    *函数名: SeqStack_Destroy 
    *参数:SeqStack* stack 栈指针  
    *返回值:void  
    *功能:销毁顺序栈,调用顺序表销毁函数  
    *******************************************************************************/   
    void SeqStack_Destroy(SeqStack* stack)  
    {  
        SeqList_Destroy (stack);  
    }  
      
    /******************************************************************************* 
    *函数名: SeqStack_Clear 
    *参数:SeqStack* stack 栈指针  
    *返回值:void  
    *功能:清空顺序栈,调用顺序表清空函数  
    *******************************************************************************/   
    void SeqStack_Clear(SeqStack* stack)  
    {  
        SeqList_Clear (stack);   
    }  
      
    /******************************************************************************* 
    *函数名: SeqStack_Push 
    *参数:SeqStack* stack 栈指针  void* item要进栈的元素  
    *返回值:void  
    *功能:将一个item元素压入栈  
    *******************************************************************************/   
    int SeqStack_Push(SeqStack* stack, void* item)  
    {  
        return SeqList_Insert(stack, item, SeqList_Length(stack));  
    }  
      
    /******************************************************************************* 
    *函数名: SeqStack_Pop 
    *参数:SeqStack* stack 栈指针  
    *返回值:void  
    *功能:将元素弹出栈  
    *******************************************************************************/  
    void* SeqStack_Pop(SeqStack* stack)  
    {  
        return SeqList_Delete(stack, SeqList_Length(stack) - 1);   
    }  
      
    /******************************************************************************* 
    *函数名: SeqStack_Top 
    *参数:SeqStack* stack 栈指针  
    *返回值:void  
    *功能:获取栈顶元素  
    *******************************************************************************/  
    void* SeqStack_Top(SeqStack* stack)  
    {  
        return SeqList_Get(stack, SeqList_Length(stack) - 1);  
    }  
      
    /******************************************************************************* 
    *函数名: SeqStack_Size 
    *参数:SeqStack* stack 栈指针  
    *返回值:int 返回栈的长度  
    *功能:获取栈的长度  
    *******************************************************************************/  
    int SeqStack_Size(SeqStack* stack)  
    {  
        return SeqList_Length (stack);  
    }  
      
    /******************************************************************************* 
    *函数名: SeqStack_Capacity 
    *参数:SeqStack* stack 栈指针  
    *返回值:void  
    *功能:获取栈的容量  
    *******************************************************************************/  
    int SeqStack_Capacity(SeqStack* stack)  
    {  
        return SeqList_Capacity(stack);  
    }  

测试代码:

#include   
#include   
#include "1.h"  
  
/*int main(int argc, char *argv[]) 
{ 
    int i; 
    int a[10]; 
    int q = 20; 
    int temp; 
     
    SeqStack* stack = SeqStack_Create (20); 
    for (i = 1; i < 10; i++) 
    { 
        a[i] = i; 
        SeqStack_Push (stack, a + i); 
    }  
     
    printf ("栈的长度是: %d\n", SeqStack_Size (stack)); 
     
    /*这里必须加上强制类型转换,因为调用函数结束以后返回的也是void *类型,所以要转换*/  
    /*printf ("栈顶元素是: %d\n", *(int*)SeqStack_Top (stack)); 
     
    for (i = 1; i < 10; i++) 
    { 
        printf ("栈中的元素分别是:%d\n", *(int *)SeqStack_Pop (stack)); 
    }  
     
     
    temp =  (int) SeqStack_Capacity(stack); 
    printf ("栈的容量为:%d\n", temp); 
     
    temp = SeqStack_Size(stack);  
    printf ("栈的元素个数为:%d\n", temp); 
     
    return 0; 
}*/  
  
  
int main()  
{  
    int i = 0;  
    char a[10];  
    char temp;  
      
    SeqStack* stack = SeqStack_Create (20);  
    for (i = 0; i < 9; i++)  
    {  
        a[i] = 'a';  
        SeqStack_Push (stack, a + i);  
    }  
    a[9] = 'b';  
    SeqStack_Push (stack, a + 9);     
      
    for (i = 0; i < 10; i++)  
    {  
        temp = *(char*)SeqStack_Pop (stack);  
        printf ("%c\n", temp);  
    }  
    return 0;  
}  

2.链式栈的实现

1、定义数据结点

数据结点用结构体来封装,这个结构体中包含了每一个next元素的信息和进栈元素的地址,虽然我们在创建链表的时候已经进行了一个结构体的定义,但是我们的栈成员并不适用于那套链表,所以这里进行重新定义。

结构体定义如下:

typedef struct _tag_LinkStack_  
{  
    LinkListNode header;  
    void *item;  
}TLinkStackNode;

2、销毁栈的函数

void LinkStack_Destroy(LinkStack* stack)  
{  
    /*调用栈清空函数*/  
    LinkStack_Clear (stack);  
    /*调用链表销毁函数*/  
    LinkList_Destroy(stack);   
}  
//销毁栈的函数中主要调用了栈的清空函数和链表的销毁函数,销毁栈的的前提首先要销清空栈中的每一个成员,然后在销毁栈的头

3、栈的清空函数

void LinkStack_Clear(LinkStack* stack)  
{  
    while (LinkStack_Size (stack) > 0)  
    {  
        LinkStack_Pop (stack);  
    }   
}  
//栈的清空函数中和链表的清空函数是由区别的,在栈的清空函数中我们主要是对栈中是否还存在元素进行了判定,如果还有元素就对将栈中的元素弹出,而链表的清空只是将链表头指向NULL,将链表长度置为0

4、将元素压入栈的操作

int LinkStack_Push(LinkStack* stack, void* item)  
{  
    TLinkStackNode* node = (TLinkStackNode*)malloc (sizeof (TLinkStackNode));  
  
    int ret = (node != NULL) && (item != NULL);   
      
    if (ret)  
    {  
        node->item = item;  
          
        ret = LinkList_Insert (stack, (LinkListNode*)node, 0);  
    }  
  
    if (!ret)  
    {  
        free(node);  
    }  
      
    return ret;   
}  
/*
在将元素压入栈的过程中,我们首先要为我们即将压入栈的元素开辟一块空间,因为是链式栈,所以我们的空间不一定非要是连续的,这是采用malloc的方式。

然后进行安全性的检测,我们要判断开辟的空间是否成功,然后还要判断我们要插入的元素的地址是不是空。如果条件都成立那我们进行元素的进栈操作。

元素的进栈操作,我们将要插入的数据结点的地址赋给栈结构体的item,然后调用链表的插入函数操作,将这个栈的数据结点插入栈中,而且由于我们是把链表的头作为栈顶,所以我们插入栈元素的位置为0.

如果我们的安全性检测没有通过,那么我们就释放为了插入一个栈元素而释放的空间。
*/

5、元素的出栈操作

void* LinkStack_Pop(LinkStack* stack)  
{  
    TLinkStackNode* node = (TLinkStackNode*)LinkList_Delete(stack, 0);  
    void * ret = NULL;  
    if (node != NULL)  
    {  
        ret = node->item;  
        free(node);  
    }   
    return ret;  
}  
/*
元素的出栈操作中,首先通过调用链表的元素删除函数来删除我们要弹出栈的元素,因为栈永远都是从栈顶弹出元素,而我们进栈的方向也是从链表的0位置方向进栈的,所以我们只要删除链表中的第0个元素即可(所谓的链表第0个元素哈)。

然后我们判定我们要删除的元素是否为空,如果不为空,那么我们将返回我们要弹出栈的元素。

在将元素弹出栈以后我们就要释放为这个链表中的元素开辟的空间。而我们的链表的操作的空间是在操作具体数据元素的时候才开辟空间,我们不使用链表的时候它是不占用空间的
*/

6、获取栈顶元素操作

void* LinkStack_Top(LinkStack* stack)  
{  
    TLinkStackNode* node = (TLinkStackNode*)LinkList_Get(stack, 0);  
    void *ret = NULL;  
    if (node != NULL)  
    {  
        ret = node->item;  
    }   
    return ret;  
}  
//通过LinkList_Get函数可以获得固定位置的元素,由于我们进栈的时候就是从0位置 所以在我们获取栈顶元素的时候也是从0位置开始获取

综上所述,可以将顺序表实现栈的建立和进栈出栈的过程用下面的代码总结。

#include   
#include   
#include "1.h"  
#include "LinkList.h"  
  
typedef struct _tag_LinkStack_  
{  
    LinkListNode header;  
    void *item;  
}TLinkStackNode;  
  
/******************************************************************************* 
*函数名: LinkStack_Create 
*参数:void 
*返回值:LinkStack* 栈的指针  
*功能:创建一个链栈,并返回创建成功后的指针  
*******************************************************************************/  
LinkStack* LinkStack_Create()  
{  
    return LinkList_Create();   
}  
  
/******************************************************************************* 
*函数名:LinkStack_Destroy 
*参数:void 
*返回值:LinkStack* 栈的指针  
*功能:销毁栈 调用栈清除函数和链表销毁函数  
*******************************************************************************/  
void LinkStack_Destroy(LinkStack* stack)  
{  
    /*调用栈清空函数*/  
    LinkStack_Clear (stack);  
    /*调用链表销毁函数*/  
    LinkList_Destroy(stack);   
}  
  
/******************************************************************************* 
*函数名:LinkStack_Clear 
*参数:void 
*返回值:LinkStack* 栈的指针  
*功能:栈清空函数   
*******************************************************************************/  
void LinkStack_Clear(LinkStack* stack)  
{  
    while (LinkStack_Size (stack) > 0)  
    {  
        LinkStack_Pop (stack);  
    }   
}  
  
/******************************************************************************* 
*函数名:LinkStack_Push 
*参数:LinkStack* stack  栈指针  void* item  要压入栈的元素  
*返回值:int 判断压栈操作是否成功  
*功能:将数据元素压入栈  
*******************************************************************************/  
int LinkStack_Push(LinkStack* stack, void* item)  
{  
    TLinkStackNode* node = (TLinkStackNode*)malloc (sizeof (TLinkStackNode));  
  
    int ret = (node != NULL) && (item != NULL);   
      
    if (ret)  
    {  
        node->item = item;  
          
        ret = LinkList_Insert (stack, (LinkListNode*)node, 0);  
    }  
  
    if (!ret)  
    {  
        free(node);  
    }  
      
    return ret;   
}  
  
/******************************************************************************* 
*函数名:LinkStack_Pop 
*参数:LinkStack* stack  栈指针  
*返回值:void* 返回的是出栈的元素  
*功能:将数据元素弹出栈  
*******************************************************************************/  
void* LinkStack_Pop(LinkStack* stack)  
{  
    TLinkStackNode* node = (TLinkStackNode*)LinkList_Delete(stack, 0);  
    void * ret = NULL;  
    if (node != NULL)  
    {  
        ret = node->item;  
        free(node);  
    }   
    return ret;  
}  
  
/******************************************************************************* 
*函数名:LinkStack_Top 
*参数:LinkStack* stack  栈指针  
*返回值:void* 返回的是栈顶的元素  
*功能:返回栈顶元素  
*******************************************************************************/  
void* LinkStack_Top(LinkStack* stack)  
{  
    TLinkStackNode* node = (TLinkStackNode*)LinkList_Get(stack, 0);  
    void *ret = NULL;  
    if (node != NULL)  
    {  
        ret = node->item;  
    }   
    return ret;  
}  
  
/******************************************************************************* 
*函数名:LinkStack_Size 
*参数:LinkStack* stack  栈指针  
*返回值:int 失败返回-1,成功返回栈的大小  
*功能:返回链栈的大小  
*******************************************************************************/  
int LinkStack_Size(LinkStack* stack)  
{  
    int ret = -1;  
    ret = LinkList_Length(stack);   
    return ret;  
}  

头文件部分

#ifndef _LINKSTACK_H_  
#define _LINKSTACK_H_  
  
typedef void LinkStack;  
  
LinkStack* LinkStack_Create();  
  
void LinkStack_Destroy(LinkStack* stack);  
  
void LinkStack_Clear(LinkStack* stack);  
  
int LinkStack_Push(LinkStack* stack, void* item);  
  
void* LinkStack_Pop(LinkStack* stack);  
  
void* LinkStack_Top(LinkStack* stack);  
  
int LinkStack_Size(LinkStack* stack);  
  
#endif 

测试代码

#include   
#include   
#include "1.h"  
   
int main(int argc, char *argv[])  
{  
    int i = 0;  
    int a[10];  
    int temp;  
      
    LinkStack * stack = LinkStack_Create();  
    for (i = 0; i < 10; i++)  
    {  
        a[i] = i;  
        LinkStack_Push(stack, a + i);  
    }  
      
    temp = LinkStack_Size(stack);  
    printf ("栈的大小为:%d\n", temp);  
    for (i = 0; i < 10; i++)  
    {  
        printf ("出栈元素为:%d\n", *(int*)LinkStack_Pop(stack));  
    }  
      
    return 0;  
} 

基于Mesos和Docker的分布式计算平台【转】

基于Mesos和Docker的分布式计算平台

针对“互联网+”时代的业务增长、变化速度及大规模计算的需求,廉价的、高可扩展的分布式x86集群已成为标准解决方案,如Google已经在几千万台服务器上部署分布式系统。Docker及其相关技术的出现和发展,又给大规模集群管理带来了新的想象空间。如何将二者进行有效地结合?本文将介绍数人科技基于Mesos和Docker的分布式计算平台的实践。

分布式系统设计准则

可伸缩性

首先分布式系统一定是大规模的系统,有很好的Scalability。出于成本的考虑,很多大规模的分布式系统一般采用廉价的PC服务器,而不是大型的高性能服务器。
没有单点失效

廉价的PC服务器在大规模使用中经常会遇到各种各样的问题,PC服务器的硬件不可能是高可靠的,比如Google的数据中心每天都会有大量的硬盘失效,所以分布式系统一定要对硬件容错,保证没有任何的单点失效。在这种很不稳定、很不可靠的硬件计算环境下,搭建一个分布式系统提供高可靠服务,必须要通过软件来容错。分布式系统针对不允许有单点失效的要求有两方面的设计考虑,一种是服务类的企业级应用,每个服务后台实例都要有多个副本,一两台硬件故障不至于影响所有服务实例;另外一种数据存储的应用,每份数据也必须要有多个备份,保证即使某几个硬件坏掉了数据也不会丢失。

高可靠性

除了单点失效,还要保证高可靠性。在分布式环境下,针对企业级服务应用,要做负载均衡和服务发现来保证高可靠性;针对数据服务,为了做到高可靠性,首先要按照某种算法来把整体数据分片(因为一台服务器装不下),然后按照同样的算法来进行分片查找。

数据本地性

再一个分布式设计理念是数据本地性,因为网络通信开销是分布式系统的瓶颈,要减少网络开销,应当让计算任务去找数据,而不是让数据去找计算。

分布式系统与Linux操作系统的比较

由于纵向拓展可优化空间太小(单台服务器的性能上限很明显),分布式系统强调横向扩展、横向优化,当分布式集群计算资源不足时,就要往集群里面添加服务器,来不停地提升分布式集群的计算能力。分布式系统要做到统一管理集群的所有服务器,屏蔽底层管理细节,诸如容错、调度、通信等,让开发人员觉得分布式集群在逻辑上是一台服务器。

和单机Linux操作系统相比,虽然分布式系统还没有成熟到成为“分布式操作系统”,但它和单机Linux一样要解决五大类操作系统必需的功能,即资源分配、进程管理、任务调度、进程间通信(IPC)和文件系统,可分别由Mesos、Docker、Marathon/Chronos、RabbitMQ和HDFS/Ceph来解决,对应于Linux下的Linux Kernel、Linux Kernel、init.d/cron、Pipe/Socket和ext4.

基于Mesos的分布式计算平台

Mesos资源分配原理

目前我们的Mesos集群部署在公有云服务上,用100多台虚拟机组成Mesos集群。Mesos不要求计算节点是物理服务器还是虚拟服务器,只要是Linux操作系统就可以。Mesos可以理解成一个分布式的Kernel,只分配集群计算资源,不负责任务调度。基于Mesos之上可以运行不同的分布式计算平台,如Spark、Storm、Hadoop、Marathon和Chronos等。Spark、Storm和Hadoop这样的计算平台有任务调度功能,可以直接使用Mesos SDK跟Mesos请求资源,然后自行调度计算任务,并对硬件容错。Marathon针对服务型分布式应用提供任务调度,比如企业网站等这类需要长时间运行的服务。通常网站应用程序没有任务调度和容错能力,因为网站程序不太会处理某个后台实例挂掉以后要在哪台机器上重新恢复等这类复杂问题。这类没有任务调度能力的服务型分布式应用,可以由Marathon来负责调度。比如,Marathon调度执行了网站服务的一百个后台实例,如果某个实例挂掉了,Marathon会在其他服务器上把这个实例恢复起来。Chronos是针对分布式批处理应用提供任务调度,比如定期处理日志或者定期调Hadoop等离线任务。

Mesos最大的好处是能够对分布式集群做细粒度资源分配。

基于Docker的分布式计算平台

Docker工作流

我们主要用Docker来做分布式环境下的进程管理。Docker工作流如图7所示,我们不仅把Docker应用到生产阶段,也应用到开发阶段,所以我们每天编辑Dockerfile,提升Docker Images,测试上线,发Docker镜像,在我们内部私有Docker regis里面,再调到我们Docker集群生产环境里面,这和其他的Docker工作流没有什么区别.

在Mesos提交Docker任务

因为Mesos和Docker已经是无缝结合起来。通过Marathon和Chronos提交服务型应用和批处理型应用。Marathon和Chronos通过RESTful的方式提交任务,用JSON脚本设定应用的后台实例个数、应用的参数、以及Docker Images的路径等等。

分布式环境下的进程通信

在分布式环境下应用服务之间通信,是用分布式消息队列来做,我们用的是RabbitMQ。RabbitMQ也是一个分布式系统,它也要保证高可靠性、解决容错的问题。首先RabbitMQ也有集群,如图8所示,六个节点组成了一个RabbitMQ的集群,每个节点之间是互为备份的关系,任何一个坏掉,其他五个还可以提供服务,通过冗余来保证RabbitMQ的高可靠性。

其次,RabbitMQ也有数据分片机制。因为消息队列有可能很长,长到所有的消息不可能都放到一个节点上,这时就要用分片,把很长的消息队列分为几段,分别放到不同的节点上。如图9所示是RabbitMQ的联盟机制,把一个消息队列打成两段,一段放在上游一段放在下游,假定下游消息队列的消息被消费完了就自动把上游消息队列里的消息移到下游,这样一个消息队列变成非常长的时候也不怕,分片到多个节点上即可。

分布式文件系统

最后讲一下分布式文件系统HDFS和Ceph。Hadoop文件系统HDFS,如图10所示,每个数据块有三个备份,必须放在不同的服务器上,而且三个备份里面每个机架最多放两份,这么做也是为了容错。Ceph是另一种流行的开源分布式文件系统。Ceph把网络存储设备抽象成一张逻辑硬盘,然后“挂载”到分布式集群的每台服务器上,原理上非常像是Linux操作系统Mount一块物理硬盘。这样一来,用户程序访问Ceph的文件系统就跟访问Linux本地路径一样,非常方便。

分布式环境下的监控

分布式环境下,程序不是运行在本地,而是在集群上面,没有监控就等于程序运行在黑盒子下,无法调优,必须要有监控。分布式环境下的监控分为两个部分,一是性能监控,另一个是报警。性能监控要知道每个应用程序运行状态是什么样,即每一个应用程序占了多少CPU内存、服务的请求处理延迟等。我们是用Graphite来做应用程序性能监控;还有其他系统,比如MongoDB、Hadoop等开源系统,我们用Ganglia来做性能监控,比如CPU内存硬盘的使用情况等。报警是要在关键服务出现故障时,通知开发运维人员及时排解故障,我们用Zabbix来做报警。

分布式系统架构的基本原则和实践

分布式系统架构的基本原则和实践

采用分布式系统架构是由于业务需求决定的,若系统要求具备如下特性,便可考虑采用分布式架构来实现:

  1. 数据存储的分区容错,冗余
  2. 应用的大访问、高性能要求
  3. 应用的高可用要求,故障转移

分布式系统遵循几个基本原则

1. CAP原理

CAP Theorem,CAP原理中,有三个要素:

  • 一致性(Consistency)
  • 可用性(Availability)
  • 分区容忍性(Partition tolerance)

CAP原理指的是,在分布式系统中这三个要素最多只能同时实现两点,不可能三者兼顾。因此在进行分布式架构设计时,必须做出取舍。而对于分布式数据系统,分区容忍性是基本要求,否则就失去了价值。因此设计分布式数据系统,就是在一致性和可用性之间取一个平衡。对于大多数web应用,其实并不需要强一致性,因此牺牲一致性而换取高可用性,是目前多数分布式数据库产品的方向。

从客户端角度,多进程并发访问时,更新过的数据在不同进程如何获取的不同策略,决定了不同的一致性。对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性。如果能容忍后续的部分或者全部访问不到,则是弱一致性。如果经过一段时间后要求能访问到更新后的数据,则是最终一致性。
但web应用也有例外,比如支付宝系统,就要求数据(银行账户)的强一致性,而且面对大量淘宝用户,可用性要求很高,因此只能牺牲数据的分区冗余。这一点也曾在和支付宝工程师交流时,得到验证。

  1. C10K问题
    分布式系统另一个理论是C10K问题,即系统的并发用户增加1万(customer ten thousand,过去一台服务器承载假设为1万用户,现在平均3~5万),是否意味着增加一台机器就能解决问题?答案通常是否定
    因为这涉及到系统的应用架构问题----串行系统和并行系统的架构和性能提升的关系:
    串行系统一般设备越多,性能成一条向下弯曲的曲线,最差情况,可能性能不增反降;而并行分布式系统设备越多,性能是正比例线性增长的直线.
  2. 串行系统和并行系统的可靠性问题
    一个大系统一般都有超过 30 个环节(串行):如果每个环节都做到 99% 的准确率,最终系统的准确率是 74%; 如果每个环节都做到98%的准确率,最终系统的准确率 54%。一个 74% 的系统是可用的(有商业价值的),一个 54% 的系统仅比随机稍好一点,不可用。这就是做大系统的魅力和挑战!
    而以上描述只是各模块串行系统所遇到的问题

如果是并行系统,准确率=1-(1-A)^B ,其中A是单个模块准确率,B是并行模块个数
如系统中每个模块的准确率是70%,那么3个模块并行,整体准确率=1-0.3^3=97.3%,如果是4个并行,准确率=1-0.3^4=99.19%,我在想这就是负载均衡靠谱的数学原理
5个9或6个9的QoS一定是指数思维的结果,线性思维等于送死

而对系统单一模块优化,准确性和可用性提升一个百分点,越接近100%,难度越大,投入成本越不可控(系统熵永不为零)
因此可靠性系统必然选择并行分布式作为架构的基本方法。

从数据的存储角度,多份冗余也是可靠性保障的一个方法。分布式存储的冗余备份常规是3份(aws就这么干的),古埃及的罗塞塔rosetta石碑用古埃及象形文字、埃及拼音和古希腊文三种文字记录一段历史,就算象形文字缺了一部分,没人能看懂,也能破译补全,这大概也是raid5的**起源吧.

分布式系统架构的实践

1.分布式存储架构

分布式存储架构现阶段有3种模式

1.1一种是物理存储采用集中式,存储节点采用多实例的方式,如NFS挂载SAN、NAS等等
1.2第二种是带有**控制器的分布式存储,如luster、moosefs、googlefs等等,一般特征是具备2个角色metadata server和storage node,将文件的元数据(描述数据的数据,如文件位置、大小等等)和数据块文件分开存储
其中metadata server除保存文件的元数据外,还维护存储节点的ip、状态等信息
luster的典型架构
MDS--meatadata server
MDT--metadata target
OSS--obj storage server
OST--obj starage target
其中MDT和OST是可以挂在NAS等**存储上的;可见,luster借鉴了上面**存储的模式,无论元数据服务还是节点服务都将服务实例和存储分离,但进化了一步,将元数据和数据块分离

luster系统很好解决了数据分布式存储,,在超级计算领域Lustre应用广泛,如美国LLNL国家实验室计算机系统、我国的天河超级计算机系统均采用Lustre搭建分布式存储系统。Lustre在全球排名前30个超级计算机系统中有15个在使用。

但有一个问题,就是metadata server的SPoF(single point of failure)问题,即单点故障;一旦metadata server挂了,整个集群也就挂了。实际应用中,是有解决方案的,如dell的官网有个pdf,就是采用heart beat和drbd网络raid的方式,启动2个实例,再如和keepalived一起组成故障转移的方案等等,可以自己试试.
再来看moosefs架构
moosefs架构和luster很相似,但进化了一步,mater(也就是metadata server)可以有从机备份了,而且可以多个
而且服务实例和存储放在一起,没有像luster,自此服务和数据不离不弃了;其实luster也可以简化成不离不弃模式,moosefs也可以学他搞个后端存储,但随着云计算、追求低成本的趋势,采用SAN这样存储设备就太贵了.
1.3第三种分布式存储是去中心化、全对称的架构(non-center or symmetric)
其设计**是采用一致性哈希consistent hash算法(DHT的一种实现,关于一致性hash具体参考后面的链接)来定位文件在存储节点中的位置,从而取消了metadata server的角色.
整个系统只有storage node一个角色,不区分元数据和数据块;
典型系统如sheepdog,但sheepdog是为满足kvm镜像和类EBS块存储而设计的,不是常规的分布式文件系统.
为了维护存储节点的信息,一般采用P2P技术的totem single ring算法(corosync是一种实现)来维护和更新node路由信息
对称架构有一个问题,采用totem single ring算法的存储节点数量有限,因为node数量超过1000,集群内的通信风暴就会产生(此处更正,应该是环太大,令牌传递效率下降,不会产生通信风暴),效率下降,sheepdog提出了一个解决方案,就是在一致性hash环上做嵌套处理.
1.4半对称结构
其实介于1.2metadata server**控制和1.3全对称的架构之间还有一种,就是把metadata也做成对称结构,我们可以称半对称结构,典型应用如fastdfs,淘宝一大牛fishman写的,主要用作图片存储,可以实现排重存储,国内几个大的网站都使用了fastdfs,在实际使用中,发现storage server之间同步数据较慢.
2.分布式数据库
分布式数据库一般都基于分布式文件系统实现数据的分片sharding,每中数据库都有自己的应用特性,就不做介绍,列出几个典型的应用,供参考
Google的big table,实现数据的追加存储append,顺序写入快速,不适合随机读的场景
hadoop的HBase,mongodb,hypertable .2010年以前,百度在用,今年infoq的**qcon,百度的杨栋也讲了百度用hypertable的血泪史.
3.分布式应用架构
分布式应用架构涉及具体应用场景,设计上除考虑上面的CAP和C10K等等经典分布式理论,还应根据业务进行权衡。基本的思路如下:
3.1在做完需求和模块设计后,要对各模块进行解藕Decoupling;
3.2在进行分布式设计时,先将各模块解藕,通过异步消息通知的方式将各模块链接;
3.3最后,要考虑这个应用的压力承载点在哪,根据用户规模估算各模块的并行数量.

总结

以上是分布式系统构建的基本原则和实践步骤,在实际应用中,仍有很多细节要考虑。但有一点要再强调,就是要根据业务来选择各层、各模块的技术,做好业务适用、成本和难度之间的权衡。

Golang可变参数函数

Golang 可变参数函数

可变参数函数。可以用任意数量的参数调用。例如,fmt.Println 是一个常见的变参函数。

这个函数使用任意数目的 int 作为参数。

变参函数使用常规的调用方式,除了参数比较特殊。

如果你的 slice 已经有了多个值,想把它们作为变参使用,你要这样调用 func(slice...)。

package main

import (
    "fmt"
)
func sum(nums ...int){
    fmt.Println(nums," ")
    total := 0
    for _,num := range nums{
        total +=num
    }
    fmt.Println(total)
}
func main(){
    sum(4,5,6,7,8,5)
    nums := []int{3,6,5,44,5,3}
    sum(nums...)
}
output==>
[4 5 6 7 8 5]  
35
[3 6 5 44 5 3]  
66

参考

www.yushuangqi.com

记[常州]-[上海]两日游

记[常州]-[上海]两日游

今天是2016-5-6,刚刚从上海回到黄山。

在两天之前,我与几个朋友一起去了周边的常州与上海玩了一圈。在学校里面是在太压抑了,趁着不是周末索性来了一场说走就走的旅行。我们几个花了两天玩完了常州中华恐龙园里面所有的项目,去了上海豫园外滩陆家嘴等几个比较有名气的地方,勉强算是一个毕业旅行吧,以此纪念一下马上就要结束的学生生涯。

在即将离开学校的时候,心中有点不舍,不过更多的是对美好明天的向往。加油吧,小伙伴们!

Golang语言内存模型【转】

Golang语言内存模型

名词定义

执行体 - Go里的Goroutine或Java中的Thread

背景介绍

内存模型的目的是为了定义清楚变量的读写在不同执行体里的可见性。理解内存模型在并发编程中非常重要,因为代码的执行顺序和书写的逻辑顺序并不会完全一致,甚至在编译期间编译器也有可能重排代码以最优化CPU执行, 另外还因为有CPU缓存的存在,内存的数据不一定会及时更新,这样对内存中的同一个变量读和写也不一定和期望一样。

和Java的内存模型规范类似,Go语言也有一个内存模型,相对JMM来说,Go的内存模型比较简单,Go的并发模型是基于CSP(Communicating Sequential Process)的,不同的Goroutine通过一种叫Channel的数据结构来通信;Java的并发模型则基于多线程和共享内存,有较多的概念(violatie, lock, final, construct, thread, atomic等)和场景,当然java.util.concurrent并发工具包大大简化了Java并发编程。

Go内存模型规范了在什么条件下一个Goroutine对某个变量的修改一定对其它Goroutine可见。

Happens Before

在一个单独的Goroutine里,对变量的读写和代码的书写顺序一致。比如以下的代码:

package main
import (
    "log"
)
var a, b, c int
func main() {
    a = 1
    b = 2
    c = a + 2
    log.Println(a, b, c)
}

尽管在编译期和执行期,编译器和CPU都有可能重排代码,比如,先执行b=2,再执行a=1,但c=a+2是保证在a=1后执行的。这样最后的执行结果一定是1 2 3,不会是1 2 2。但下面的代码则可能会输出0 0 0,1 2 2, 0 2 3 (b=2比a=1先执行), 1 2 3等各种可能。

package main
import (
    "log"
)
var a, b, c int
func main() {
    go func() {
        a = 1
        b = 2
    }()
    go func() {
        c = a + 2
    }()
    log.Println(a, b, c)
}

Happens-before 定义

Happens-before用来指明Go程序里的内存操作的局部顺序。如果一个内存操作事件e1 happens-before e2,则e2 happens-after e1也成立;如果e1不是happens-before e2,也不是happens-after e2,则e1和e2是并发的。

在这个定义之下,如果以下情况满足,则对变量(v)的内存写操作(w)对一个内存读操作(r)来说允许可见的:

r不在w开始之前发生(可以是之后或并发);
w和r之间没有另一个写操作(w’)发生;
为了保证对变量(v)的一个特定写操作(w)对一个读操作(r)可见,就需要确保w是r唯一允许的写操作,于是如果以下情况满足,则对变量(v)的内存写操作(w)对一个内存读操作(r)来说保证可见的:

w在r开始之前发生;
所有其它对v的写操作只在w之前或r之后发生;
可以看出后一种约定情况比前一种更严格,这种情况要求没有w或r没有其他的并发写操作。

在单个Goroutine里,因为肯定没有并发,上面两种情况是等价的。对变量v的读操作可以读到最近一次写操作的值(这个应该很容易理解)。但在多个Goroutine里如果要访问一个共享变量,我们就必须使用同步工具来建立happens-before条件,来保证对该变量的读操作能读到期望的修改值。

要保证并行执行体对共享变量的顺序访问方法就是用锁。Java和Go在这点上是一致的。

以下是具体的可被利用的Go语言的happens-before规则,从本质上来讲,happens-before规则确定了CPU缓冲和主存的同步时间点(通过内存屏障等指令),从而使得对变量的读写顺序可被确定–也就是我们通常说的“同步”。

同步方法

初始化

  • 如果package p 引用了package q,q的init()方法 happens-before p (Java工程师可以对比一下final变量的happens-before规则)
  • main.main()方法 happens-after所有package的init()方法结束。

创建Goroutine

go语句创建新的goroutine happens-before 该goroutine执行(这个应该很容易理解)

package main
import (
    "log"
    "time"
)
var a, b, c int
func main() {
    a = 1
    b = 2
    go func() {
        c = a + 2
        log.Println(a, b, c)
    }()
    time.Sleep(1 * time.Second)
}

利用这条happens-before,我们可以确定c=a+2是happens-aftera=1和b=2,所以结果输出是可以确定的1 2 3,但如果是下面这样的代码,输出就不确定了,有可能是1 2 3或0 0 2

func main() {
    go func() {
        c = a + 2
        log.Println(a, b, c)
    }()
    a = 1
    b = 2
    time.Sleep(1 * time.Second)
}

销毁Goroutine

Goroutine的退出并不保证happens-before任何事件。

var a string
func hello() {
    go func() { a = "hello" }()
    print(a)
}

上面代码因为a="hello" 没有使用同步事件,并不能保证这个赋值被主goroutine可见。事实上,极度优化的Go编译器甚至可以完全删除这行代码go func() { a = "hello" }()。

Goroutine对变量的修改需要让对其它Goroutine可见,除了使用锁来同步外还可以用Channel。

Channel通信

在Go编程中,Channel是被推荐的执行体间通信的方法,Go的编译器和运行态都会尽力对其优化。

  • 对一个Channel的发送操作(send) happens-before 相应Channel的接收操作完成
  • 关闭一个Channel happens-before 从该Channel接收到最后的返回值0
  • 不带缓冲的Channel的接收操作(receive) happens-before 相应Channel的发送操作完成
    var c = make(chan int, 10)
    var a string
    func f() {
    a = "hello, world"
    c <- 0
    }
    func main() {
    go f()
    <-c
    print(a)
    }
    
    上述代码可以确保输出hello, world,因为a = "hello, world" happens-before c <- 0,print(a) happens-after <-c, 根据上面的规则1)以及happens-before的可传递性,a = "hello, world" happens-beforeprint(a)。

根据规则2)把c<-0替换成close(c)也能保证输出hello,world,因为关闭操作在<-c接收到0之前发送。

var c = make(chan int)
var a string
func f() {
    a = "hello, world"
    <-c
}
func main() {
    go f()
    c <- 0
    print(a)
}

根据规则3),因为c是不带缓冲的Channel,a = "hello, world" happens-before <-c happens-before c <- 0 happens-before print(a), 但如果c是缓冲队列,如定义c = make(chan int, 1), 那结果就不确定了。

sync 包实现了两种锁数据结构:

  • sync.Mutex -> java.util.concurrent.ReentrantLock

  • sync.RWMutex -> java.util.concurrent.locks.ReadWriteLock
    其happens-before规则和Java的也类似:

  • 任何sync.Mutex或sync.RWMutex 变量(l),定义 n < m, 第n次 l.Unlock() happens-before 第m次l.lock()调用返回.

    var l sync.Mutex
    var a string
    func f() {
    a = "hello, world"
    l.Unlock()
    }
    func main() {
    l.Lock()
    go f()
    l.Lock()
    print(a)
    }
    

    a = "hello, world" happens-before l.Unlock() happens-before 第二个 l.Lock() happens-before print(a)

    Once

    sync包还提供了一个安全的初始化工具Once。还记得Java的Singleton设计模式,double-check,甚至triple-check的各种单例初始化方法吗?Go则提供了一个标准的方法。

  • once.Do(f)中的f() happens-before 任何多个once.Do(f)调用的返回,且f()有且只有一次调用。

    var a string
    var once sync.Once
    func setup() {
    a = "hello, world"
    }
    func doprint() {
    once.Do(setup)
    print(a)
    }
    func twoprint() {
    go doprint()
    go doprint()
    }
    

    上面的代码虽然调用两次doprint(),但实际上setup只会执行一次,并且并发的once.Do(setup)都会等待setup返回后再继续执行。

Golang数据序列化工具GOB

Golang数据序列化工具GOB

gob是go官方提供的数据序列化的工具。如果需要在不同的go程序中传输数据,或者希望将某个结构体数据保存到硬盘上,可以考虑使用gob。如果数据传输双方有一方不是使用go语言实现,则需要考虑其它RPC框架,如thrift grpc等。

package main

import (
"bytes"
"encoding/gob"
"fmt"
"log"
)

type P struct {
   X, Y, Z int
   Name    string
}

type Q struct {
   X, Y *int32
   Name string
}

func main() {
   var network bytes.Buffer        // Stand-in for a network connection
   enc := gob.NewEncoder(&network) // 用于写入数据
   dec := gob.NewDecoder(&network) // 用于读取数据

   err := enc.Encode(P{3, 4, 5, "Pythagoras"})
   if err != nil {
      log.Fatal("encode error:", err)
   }

   var q Q
   err = dec.Decode(&q)
   if err != nil {
      log.Fatal("decode error:", err)
   }
   fmt.Printf("%q: {%d,%d}\n", q.Name, *q.X, *q.Y)
}
output==>
"Pythagoras": {3,4}

可以看到enc写入的数据,通过dec读取后,完整的还原了数据。
如果将序列化的数据通过网络传输,就可能实现远程过程调用了。

参考

http://www.baiyuxiong.com/?p=1181

Golang定时器应用

ticker定时器

为了判断连接是否可用,通常我们会用timer机制来定时检测,这里要用ticker:

ticker := time.NewTicker(60 * time.Second)
/*
使用一个60s的ticker,定时去ping,如果ping失败了,证明连接已经断开了,这时候就需要close了*/
for {
    select {
        case <-ticker.C:
            if err := ping(); err != nil {
                close()
            }
    }
}

这套机制比较简单,也运行的很好,直到我们的服务器连上了10w+的连接。因为每一个连接都有一个ticker,所以同时会有大量的ticker运行,cpu一直在30%左右徘徊,性能不能让人接受。

其实,我们只需要的是一套高效的超时通知机制。

在go里面,channel是一个很不错的东西,我们可以通过close channel来进行broadcast。

ch := make(bool)
/*
启动了10个goroutine,它们都会因为等待ch的数据而block,10s之后close这个channel,那么所有等待该channel的goroutine就会继续往下执行
*/
for i := 0; i < 10; i++ {
    go func() {
        println("begin")
        <-ch
        println("end")
    }
}

time.Sleep(10 * time.Second)

close(ch)

通过channel这种close broadcast机制,我们可以非常方便的实现一个timer,timer有一个channel ch,所有需要在某一个时间 “T” 收到通知的goroutine都可以尝试读该ch,当T到达时候,close该ch,那么所有的goroutine都能收到该事件了。
时间轮算法:

package timingwheel
//性能很好,转载自siddontang
import (
    "sync"
    "time"
)

type TimingWheel struct {
    sync.Mutex

    interval time.Duration

    ticker *time.Ticker
    quit   chan struct{}

    maxTimeout time.Duration

    cs []chan struct{}

    pos int
}

func NewTimingWheel(interval time.Duration, buckets int) *TimingWheel {
    w := new(TimingWheel)

    w.interval = interval

    w.quit = make(chan struct{})
    w.pos = 0

    w.maxTimeout = time.Duration(interval * (time.Duration(buckets)))

    w.cs = make([]chan struct{}, buckets)

    for i := range w.cs {
        w.cs[i] = make(chan struct{})
    }

    w.ticker = time.NewTicker(interval)
    go w.run()

    return w
}

func (w *TimingWheel) Stop() {
    close(w.quit)
}

func (w *TimingWheel) After(timeout time.Duration) <-chan struct{} {
    if timeout >= w.maxTimeout {
        panic("timeout too much, over maxtimeout")
    }

    w.Lock()

    index := (w.pos + int(timeout/w.interval)) % len(w.cs)

    b := w.cs[index]

    w.Unlock()

    return b
}

func (w *TimingWheel) run() {
    for {
        select {
        case <-w.ticker.C:
            w.onTicker()
        case <-w.quit:
            w.ticker.Stop()
            return
        }
    }
}

func (w *TimingWheel) onTicker() {
    w.Lock()

    lastC := w.cs[w.pos]
    w.cs[w.pos] = make(chan struct{})

    w.pos = (w.pos + 1) % len(w.cs)

    w.Unlock()

    close(lastC)
}

探讨基于LBS功能的两种实现方案

探讨基于LBS功能的两种实现方案

概述

随着移动终端的普及,很多应用都基于LBS功能,附近的某某(餐馆、银行、妹纸等等)。
基础数据中,一般保存了目标位置的经纬度;利用用户提供的经纬度,进行对比,从而获得是否在附近。

目标

查找附近的XXX,由近到远返回结果,且结果中有与目标点的距离。
针对查找附近的XXX,提出两个方案.

方案1

抽象为球面两点距离的计算,即已知道球面上两点的经纬度:
点(纬度,经度),1(radLat1,radLng1)、2(radLat2,radLng2);

  • 优点:通俗易懂,部署简单便捷

  • 缺点:每次都会查询数据库,性能堪忧
    推导
    通过余弦定理以及弧度计算方法,最终推导出来的算式1为:

    $s = acos(cos($radLat1)*cos($radLat2)*cos($radLng1-$radLng2)+sin($radLat1)*sin($radLat2))*$R;
    

    目前网上大多使用Google公开的距离计算公司,推导算式2为:

    $s = 2*asin(sqrt(pow(sin(($radLat1-$radLat2)/2),2)+cos($radLat1)*cos($radLat2)*pow(sin(($radLng1-$radLng2)/2),2)))*$R;
    

    其中 : radLat1、radLng1,radLat2,radLng2 为弧度,$R 为地球半径.

    结果测试

    通过测试两种算法,结果相同且都正确,但通过PHP代码测试,两点间距离,10W次性能对比,自行推导版本计算时长算式2较优,如下:
    //算式1
    0.56368780136108float(431)
    0.57460689544678float(431)
    0.59051203727722float(431)
    //算式2
    0.47404885292053float(431)
    0.47808718681335float(431)
    0.47946381568909float(431)

    得出计算公式

    所以采用数学方法推导出的公式:

    
    

    实际应用

    在实际应用中,需要从数据库中遍历取出符合条件,以及排序等操作。
    将所有数据取出,然后通过PHP循环对比,筛选符合条件结果,显然性能低下;所以我们利用下Mysql存储函数来解决这个问题吧。

    创建存储函数

    创建Mysql存储函数,并对经纬度字段建立索引:

    DELIMITER $$
    
    

CREATE DEFINER=root@% FUNCTION GETDISTANCE(lat1 DOUBLE, lng1 DOUBLE, lat2 DOUBLE, lng2 DOUBLE) RETURNS double

READS SQL DATA

DETERMINISTIC

BEGIN

DECLARE RAD DOUBLE;

DECLARE EARTH_RADIUS DOUBLE DEFAULT 6378137;

DECLARE radLat1 DOUBLE;

DECLARE radLat2 DOUBLE;

DECLARE radLng1 DOUBLE;

DECLARE radLng2 DOUBLE;

DECLARE s DOUBLE;

SET RAD = PI() / 180.0;

SET radLat1 = lat1 * RAD;

SET radLat2 = lat2 * RAD;

SET radLng1 = lng1 * RAD;

SET radLng2 = lng2 * RAD;

SET s = ACOS(COS(radLat1)_COS(radLat2)_COS(radLng1-radLng2)+SIN(radLat1)_SIN(radLat2))_EARTH_RADIUS;

SET s = ROUND(s * 10000) / 10000;

RETURN s;

END$$

DELIMITER ;

查询SQL

通过SQL,可设置距离以及排序;可搜索出符合条件的信息,以及有一个较好的排序。

SELECT *,latitude,longitude,GETDISTANCE(latitude,longitude,30.663262,104.071619) AS distance FROM  mb_shop_ext where 1 HAVING distance<1000 ORDER BY distance ASC LIMIT 0,10

方案2

Geohash算法;geohash是一种地址编码,它能把二维的经纬度编码成一维的字符串。比如,成都永丰立交的编码是wm3yr31d2524。
优点:

  • 利用一个字段,即可存储经纬度;搜索时,只需一条索引,效率较高

  • 编码的前缀可以表示更大的区域,查找附近的,非常方便。 SQL中,LIKE ‘wm3yr3%’,即可查询附近的所有地点

  • 通过编码精度可模糊坐标、隐私保护等
    缺点:

  • 距离和排序需二次运算(筛选结果中运行,其实挺快)

    geohash的编码算法

    如:成都永丰立交经纬度(30.63578,104.031601)
    1.纬度范围(-90, 90)平分成两个区间(-90, 0)、(0, 90), 如果目标纬度位于前一个区间,则编码为0,否则编码为1。
    由于30.625265属于(0, 90),所以取编码为1。
    然后再将(0, 90)分成 (0, 45), (45, 90)两个区间,而39.92324位于(0, 45),所以编码为0。
    然后再将(0, 45)分成 (0, 22.5), (22.5, 45)两个区间,而39.92324位于(22.5, 45),所以编码为1。
    依次类推可得永丰立交纬度编码为101010111001001000100101101010。

2.经度也用同样的算法,对(-180, 180)依次细分,(-180,0)、(0,180) 得出编码110010011111101001100000000000

3.合并经纬度编码,从高到低,先取一位经度,再取一位纬度;得出结果 111001001100011111101011100011000010110000010001010001000100

4.用0-9、b-z(去掉a, i, l, o)这32个字母进行base32编码,得到(30.63578,104.031601)的编码为wm3yr31d2524。

11100 10011 00011 11110 10111 00011 00001 01100 00010 00101 00010 00100 => wm3yr31d2524
 
十进制  0   1   2   3   4   5   6   7   8   9   10  11  12  13  14  15
base32   0   1   2   3   4   5   6   7   8   9   b   c   d   e   f   g
十进制  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31
base32   h   j   k   m   n   p   q   r   s   t   u   v   w   x   y   z

策略

1.在纬度和经度入库时,数据库新加一字段geohash,记录此点的geohash值

2.查找附近,利用 在SQL中 LIKE ‘wm3yr3%’;且此结果可缓存;在小区域内,不会因为改变经纬度,而重新数据库查询

3.查找出的有限结果,如需要求距离或者排序,可利用距离公式和二维数据排序;此时也是少量数据,会很快的。

得出geohash基类

引自:https://github.com/tudouya/CodeLib/blob/master/PHP/class/Geohash.class.php

codingMap[substr($this->coding,$i,1)]=str_pad(decbin($i), 5, "0", STR_PAD_LEFT);
        }
 
    }
 
    public function decode($hash)
    {
        $binary="";
        $hl=strlen($hash);
        for($i=0; $i<$hl; $i++)
        {
            $binary.=$this->codingMap[substr($hash,$i,1)];
        }
 
        $bl=strlen($binary);
        $blat="";
        $blong="";
        for ($i=0; $i<$bl; $i++)
        {
            if ($i%2)
                $blat=$blat.substr($binary,$i,1);
            else
                $blong=$blong.substr($binary,$i,1);
 
        }
 
        $lat=$this->binDecode($blat,-90,90);
        $long=$this->binDecode($blong,-180,180);
 
        $latErr=$this->calcError(strlen($blat),-90,90);
        $longErr=$this->calcError(strlen($blong),-180,180);
 
        $latPlaces=max(1, -round(log10($latErr))) - 1;
        $longPlaces=max(1, -round(log10($longErr))) - 1;
 
        $lat=round($lat, $latPlaces);
        $long=round($long, $longPlaces);
 
        return array($lat,$long);
    }
 
    public function encode($lat,$long)
    {
        $plat=$this->precision($lat);
        $latbits=1;
        $err=45;
        while($err>$plat)
        {
            $latbits++;
            $err/=2;
        }
 
        $plong=$this->precision($long);
        $longbits=1;
        $err=90;
        while($err>$plong)
        {
            $longbits++;
            $err/=2;
        }
 
        $bits=max($latbits,$longbits);
 
        $longbits=$bits;
        $latbits=$bits;
        $addlong=1;
        while (($longbits+$latbits)%5 != 0)
        {
            $longbits+=$addlong;
            $latbits+=!$addlong;
            $addlong=!$addlong;
        }
 
 
        $blat=$this->binEncode($lat,-90,90, $latbits);
 
        $blong=$this->binEncode($long,-180,180,$longbits);
 
        $binary="";
        $uselong=1;
        while (strlen($blat)+strlen($blong))
        {
            if ($uselong)
            {
                $binary=$binary.substr($blong,0,1);
                $blong=substr($blong,1);
            }
            else
            {
                $binary=$binary.substr($blat,0,1);
                $blat=substr($blat,1);
            }
            $uselong=!$uselong;
        }
 
        $hash="";
        for ($i=0; $icoding[$n];
        }
 
 
        return $hash;
    }
 
    private function calcError($bits,$min,$max)
    {
        $err=($max-$min)/2;
        while ($bits--)
            $err/=2;
        return $err;
    }
 
    private function precision($number)
    {
        $precision=0;
        $pt=strpos($number,'.');
        if ($pt!==false)
        {
            $precision=-(strlen($number)-$pt-1);
        }
 
        return pow(10,$precision)/2;
    }
 
    private function binEncode($number, $min, $max, $bitcount)
    {
        if ($bitcount==0)
            return "";
        $mid=($min+$max)/2;
        if ($number>$mid)
            return "1".$this->binEncode($number, $mid, $max,$bitcount-1);
        else
            return "0".$this->binEncode($number, $min, $mid,$bitcount-1);
    }
 
    private function binDecode($binary, $min, $max)
    {
        $mid=($min+$max)/2;
 
        if (strlen($binary)==0)
            return $mid;
 
        $bit=substr($binary,0,1);
        $binary=substr($binary,1);
 
        if ($bit==1)
            return $this->binDecode($binary, $mid, $max);
        else
            return $this->binDecode($binary, $min, $mid);
    }
}
?>

1、2方案测试

 '127.0.0.1',
    'port' => 3306,
    'user' => 'root',
    'password' => '123456',
    'database' => 'mocube',
    'charset' => 'utf8',
    'persistent' => false
);
 
$mysql = new Db_Mysql($conf);
$geohash=new Geohash;
 
 
//经纬度转换成Geohash
/*
 
$sql = 'select shop_id,latitude,longitude from mb_shop_ext';
 
 
$data = $mysql->queryAll($sql);
 
 
foreach($data as $val)
{
 
  $geohash_val = $geohash->encode($val['latitude'],$val['longitude']);
 
  $sql = 'update mb_shop_ext set geohash= "'.$geohash_val.'" where shop_id = '.$val['shop_id'];
 
  echo $sql;
 
  $re = $mysql->query($sql);
 
  var_dump($re);
 
}
*/
 
 
//获取附近的信息
$n_latitude = $_GET['la'];
$n_longitude = $_GET['lo'];
 
//开始
$b_time = microtime(true);
 
 
//方案1,直接利用数据库存储函数,遍历排序
/*
$sql = 'SELECT *,latitude,longitude,GETDISTANCE(latitude,longitude,'.$n_latitude.','.$n_longitude.') AS distance FROM  mb_shop_ext where 1 HAVING distance<1000 ORDER BY distance ASC';
 
$data = $mysql->queryAll($sql);
 
//结束
$e_time = microtime(true);
 
echo $e_time - $b_time;
 
var_dump($data);
exit;
*/
 
//方案2 geohash求出附近,然后排序
 
//当前 geohash值
$n_geohash = $geohash->encode($n_latitude,$n_longitude);
 
//附近
$n = $_GET['n'];
$like_geohash = substr($n_geohash, 0, $n);
 
$sql = 'select * from mb_shop_ext where geohash like "'.$like_geohash.'%"';
 
echo $sql;
 
$data = $mysql->queryAll($sql);
 
//算出实际距离
foreach($data as $key=>$val)
{
    $distance = getDistance($n_latitude,$n_longitude,$val['latitude'],$val['longitude']);
 
    $data[$key]['distance'] = $distance;
 
    //排序列
    $sortdistance[$key] = $distance;
}
 
//距离排序
array_multisort($sortdistance,SORT_ASC,$data);
 
//结束
$e_time = microtime(true);
 
echo $e_time - $b_time;
 
var_dump($data);
 
 
 
//根据经纬度计算距离 其中1($lat1,$lng1)、2($lat2,$lng2)
function getDistance($lat1,$lng1,$lat2,$lng2)
{
    //地球半径
    $R = 6378137;
 
    //将角度转为狐度
    $radLat1 = deg2rad($lat1);
    $radLat2 = deg2rad($lat2);
    $radLng1 = deg2rad($lng1);
    $radLng2 = deg2rad($lng2);
 
    //结果
    $s = acos(cos($radLat1)*cos($radLat2)*cos($radLng1-$radLng2)+sin($radLat1)*sin($radLat2))*$R;
 
    //精度
    $s = round($s* 10000)/10000;
 
    return  round($s);
}
 
?>

方案对比

方案2的亮点在于:

搜索结果可缓存,重复使用,不会因为用户有小范围的移动,直接穿透数据库查询。
先缩小结果范围,再运算、排序,可提升性能。

254条记录,性能对比,在实际应用场景中,方案2数据库搜索可内存缓存;且如数据量更大,方案2结果会更优。

方案1:
0.016560077667236
0.032402992248535
0.040318012237549

方案2
0.0079810619354248
0.0079669952392578
0.0064868927001953

两种方案,根据应用场景以及负载情况合理选择,当然推荐方案2;
不管哪种方案,都记得,给列加上索引,利于数据库检索。

Nginx下流量拦截算法

Nginx下流量拦截算法

电商平台营销时候,经常会碰到的大流量问题,除了做流量分流处理,可能还要做用户黑白名单、信誉分析,进而根据用户ip信誉权重做相应的流量拦截、限制流量。
Nginx自身有的请求限制模块ngx_http_limit_req_module、流量限制模块ngx_stream_limit_conn_module基于令牌桶算法,可以方便的控制令牌速率,自定义调节限流,就能很好的限制请求数量,然而,nginx.conf问题还是在于无法热加载。
当然还有其他方法,比如使用流量限制方案,原理是动态的基于ip,实现简单的漏桶算法,限制访问频率。
这里的话,就简单分析下流量限制算法:漏桶算法、令牌桶算法、滑动窗口等在Nginx中如何动态绑定uri,动态设定rate实现。

漏桶算法

漏桶算法可以很好地限制容量池的大小,从而防止流量暴增。如果针对uri+ip作为监测的key,就可以实现定向的设定指定ip对指定uri容量大小,超出的请求做队列处理(队列处理要引入消息机制)或者丢弃处理。这也是v2ex对流量拦截的算法,针对uri+ip做流量监测。
漏桶算法实现上来说,就是建立一个队列,在Redis中以uri:ip作为key,队列上实现FIFO,在请求的前奏实现插入,请求完成后实现删除。
实现方法是在Nginx发送http数据给用户后,通过ngx.eof()关闭TCP协议,做其他操作。

local _M = { _VERSION = "2015.10.19", OK = 1, BUSY = 2, FORBIDDEN = 3 }

function _M.do_list(red, uri, key, size, rate)
    local ok, err = red:expire(uri .. ":" .. key, size)
    if not ok then
        ngx.log(ngx.WARN, "redis set expire error: ", err)
        return nil
    end
    local ok, err = red:rpush(uri .. ":" .. key, ngx.time())
    if not ok then
        ngx.log(ngx.WARN, "redis rpush error: ", err)
        return nil
    end
    local res, err = red:lrange(uri .. ":" .. key, -(size * rate), -1)
    if not ok then
        ngx.log(ngx.WARN, "redis lrange error: ", err)
        return nil
    end
    if #res < (size * rate) or res[#res] - res[1] < size then
        return _M.OK
    end
    return nil
end

漏桶算法优点很明显,简单、高效,能恰当拦截容量外的暴力流量。

但缺点也明显,无法对流量做频率处理,比如桶size大小设置范围内,进行并发攻击依然能大流量并发效果,桶容量不可以过小,否则容易卡死正常用户。

令牌算法

令牌桶算法通过发放令牌,根据令牌的rate频率做请求频率限制,容量限制等。

  • 系统根据rate(r/s)频率参数向指定桶中添加token,满则保持,不添加
  • 当用户请求Nginx时候,分析uri是否需要限制流量,限制则执行令牌桶算法
  • 如果桶满了,则请求通过,消耗令牌一枚;如果请求Redis发现key不存在,则通过size装满令牌桶;如果桶内令牌空,则废弃或等待流量。
    使用Nginx实现必然不能跑一个程序添加令牌了,这个时候需要在分析令牌时候,通过计算时间间隔一次性添加完令牌桶内令牌。具体算法是:rate * time_distance = token_count令牌数量, if token_count > size 桶容量, token_count = size。
    实现的存储结构是用Hash哈希存储 uri:ip -> token_count,字段通过EXPIRE设定过期时间,达到长时间不访问清除桶数据效果。
    桶的大小、请求的频率限制用Redis哈希表存储,不存在则默认不做流量拦截。
    用户黑白名单通过Order SET设定信誉权重,权重越大,代表危险性越大,进而通过百分比改变接口限定rate频率。
    令牌桶算法优势在于能针对uri做定向rate、size等,不仅限制总请求大小,还限制平均频率大小。缺点是,还是容易导致误判等问题,并切用户的信誉无法完全准确。

    参考

    http://homeway.me/2015/10/21/nginx-lua-traffic-limit-algorithm

数据结构之堆

数据结构之堆

堆的定义

堆:堆常用来实现优先队列,在这种队列中,待删除的元素为优先级最高(最低)的那个。在任何时候,任意优先元素都是可以插入到队列中去的,是计算机科学中一类特殊的数据结构的统称。
最大(最小)堆是一棵每一个节点的键值都不小于(大于)其孩子(如果存在)的键值的树。大顶堆是一棵完全二叉树,同时也是一棵最大树。小顶堆是一棵完全完全二叉树,同时也是一棵最小树。
注意点:

  • 堆中任一子树亦是堆;

  • 以上讨论的堆实际上是二叉堆(Binary Heap),类似地可定义k叉堆。

    支持的基本操作

    堆支持以下的基本操作:

  • build: 建立一个空堆;

  • insert: 向堆中插入一个新元素;

  • update:将新元素提升使其符合堆的性质;

  • get:获取当前堆顶元素的值;

  • delete:删除堆顶元素;

  • heapify:使删除堆顶元素的堆再次成为堆。
    某些堆实现还支持其他的一些操作,如斐波那契堆支持检查一个堆中是否存在某个元素。

    堆的应用

    1.堆排序

    堆排序(HeapSort)是一树形选择排序。
     堆排序的特点是:在排序过程中,将R[l..n]看成是一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系,在当前无序区中选择关键字最大(或最小)的记录。
    直接选择排序中,为了从R[1..n]中选出关键字最小的记录,必须进行n-1次比较,然后在R[2..n]中选出关键字最小的记录,又需要做n-2次比较。事实上,后面的n-2次比较中,有许多比较可能在前面的n-1次比较中已经做过,但由于前一趟排序时未保留这些比较结果,所以后一趟排序时又重复执行了这些比较操作。
    堆排序可通过树形结构保存部分比较结果,可减少比较次数。
    堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得在当前无序区中选取最大(或最小)关键字的记录变得简单。

    (1)用大根堆排序的基本**

  • 先将初始文件R[1..n]建成一个大根堆,此堆为初始的无序区

  • 再将关键字最大的记录R1和无序区的最后一个记录R[n]交换,由此得到新的无序区R[1..n-1]和有序区R[n],且满足R[1..n-1].keys≤R[n].key

  • 由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1..n-1]调整为堆。然后再次将R[1..n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[1..n-2]和有序区R[n-1..n],且仍满足关系R[1..n-2].keys≤R[n-1..n].keys,同样要将R[1..n-2]调整为堆。直到无序区只有一个元素为止。

    (2)大根堆排序算法的基本操作

  • 初始化操作:将R[1..n]构造为初始堆;

  • 每一趟排序的基本操作:将当前无序区的堆顶记录R[1]和该区间的最后一个记录交换,然后将新的无序区调整为堆(亦称重建堆)。

    注意:

  • 只需做n-1趟排序,选出较大的n-1个关键字即可以使得文件递增有序。

  • 用小根堆排序与利用大根堆类似,只不过其排序结果是递减有序的。堆排序和直接选择排序相反:在任何时刻,堆排序中无序区总是在有序区之前,且有序区是在原向量的尾部由后往前逐步扩大至整个向量为止。

    (3)算法实现

    template   
    void Sort::HeapSort(T arr[], int len){  
    int i;   
    
    //建立子堆   
    for(i = len / 2; i >= 1; i--){  
        CreateHeap(arr, i, len);  
    }  
    
    for(i = len - 1; i >= 1; i--){  
        buff = arr[1];  
        arr[1] = arr[i + 1];  
        arr[i + 1] = buff;   
    
        CreateHeap(arr, 1, i);   
    }  
    }   
    //建立堆   
    template   
    void Sort::CreateHeap(T arr[], int root, int len){  
    int j = 2 * root;                   //root's left child, right (2 * root + 1)   
    T temp = arr[root];  
    bool flags = false;   
    
    while(j <= len && !flags){  
        if(j < len){  
    
            if(arr[j] < arr[j + 1]){     // Left child is less then right child   
                ++j;                // Move the index to the right child   
            }     
        }  
    
        if(temp < arr[j]){  
            arr[j / 2] = arr[j];  
            j *= 2;   
        }else{  
            flags = true;   
        }   
    }   
    arr[j / 2]  = temp;   
    }   
    

    2.选择前k个最大(最小)的数

    **:在一个很大的无序数组里面选择前k个最大(最小)的数据,最直观的做法是把数组里面的数据全部排好序,然后输出前面最大(最小)的k个数据。但是,排序最好需要O(nlogn)的时间,而且我们不需要前k个最大(最小)的元素是有序的。这个时候我们可以建立k个元素的最小堆(得出前k个最大值)或者最大堆(得到前k个最小值),我们只需要遍历一遍数组,在把元素插入到堆中去只需要logk的时间,这个速度是很乐观的。利用堆得出前k个最大(最小)元素特别适合海量数据的处理。

    typedef multiset >            intSet;  
    typedef multiset >::iterator  setIterator;  
    
    void GetLeastNumbers(const vector& data, intSet& leastNumbers, int k)  
    {  
        leastNumbers.clear();  
    
        if(k < 1 || data.size() < k)  
            return;  
    
        vector::const_iterator iter = data.begin();  
        for(; iter != data.end(); ++ iter)  
        {  
            if((leastNumbers.size()) < k)  
                leastNumbers.insert(*iter);  
    
            else  
            {  
                setIterator iterGreatest = leastNumbers.begin();  
    
                if(*iter < *(leastNumbers.begin()))  
                {  
                    leastNumbers.erase(iterGreatest);  
                    leastNumbers.insert(*iter);  
                }  
            }  
        }  
    }  
    

Golang之关于死锁

Golang之关于死锁

什么是死锁:

何谓死锁? 操作系统有讲过的,所有的线程或进程都在等待资源的释放。如上的程序中, 只有一个goroutine, 所以当你向里面加数据或者存数据的话,都会锁死信道, 并且阻塞当前 goroutine, 也就是所有的goroutine(其实就main线一个)都在等待信道的开放(没人拿走数据信道是不会开放的),也就是死锁咯。

会发生死锁的几种情况:

  • 只在单一的goroutine里操作无缓冲信道,一定死锁。比如你只在main函数里操作信道
    package main
    import "fmt"
    func main(){
    ch := make(chan int)
    ch <- 1
    fmt.Println("This is Great")
    }
    
  • 主线等ch1中的数据流出,ch1等ch2的数据流出,但是ch2等待数据流入,两个goroutine都在等,也就是死锁
    package main
    import "fmt"
    var ch1 chan int = make(chan int)
    var ch2 chan int = make(chan int)
    func say(s string) {
    fmt.Println(s)
    ch1 <- <- ch2 // ch1 等待 ch2流出的数据
    }
    func main() {
    go say("hello")
    <- ch1  // 堵塞主线
    }
    
  • 非缓冲信道上如果发生流入无流出,或者流出无流入,导致发生死锁(除了channel操作没有执行的情况)
    package main
    func main(){
    c, quit := make(chan int), make(chan int)
    go func() {
      c <- 1  // c通道的数据没有被其他goroutine读取走,堵塞当前goroutine
      quit <- 0 // quit始终没有办法写入数据
    }()
    <- quit // quit 等待数据的写
    }
    
    对于上面的情况,有一个反例,可以补充说明一下,在只有channel单向操作的情况下,程序依然可以正常执行。
    package main
    func main(){
    c := make(chan int)
    go func(){
        c<- 1
    }()
    }
    
    解析:main又没等待其它goroutine,自己先跑完了, 所以没有数据流入c信道,一共执行了一个main, 并且没有发生阻塞,所以没有死锁错误。

死锁解决方法:

  • 很简单,把没有取走的数据取走,没放入的数据放入,因为无缓冲channel不能存储数据;
  • 将无缓冲channel变成缓冲channel,保证cap值大于等于channel里面将要处理的数据量。缓冲信道是先进先出的,我们可以把缓冲信道看作为一个线程安全的队列。类似于Python中的队列Queue。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.