作者: ethan

  • Golang BugFix: fatal error: concurrent map writes

    问题

    今天原本运行正常的程序,突然爆以下错误:

    fatal error: concurrent map writes
    fatal error: concurrent map writes
    
    goroutine 8 [running]:
    runtime.throw(0x1147862, 0x15)
            /usr/local/go/src/runtime/panic.go:1116 +0x72 fp=0xc00003f740 sp=0xc00003f710 pc=0x1034f12
    runtime.mapassign_fast64(0x1121720, 0xc000010300, 0x1, 0x0)
            /usr/local/go/src/runtime/map_fast64.go:176 +0x325 fp=0xc00003f780 sp=0xc00003f740 pc=0x1011c65
    command-line-arguments.RunGo.func1(0xc00001a110, 0xc000010300, 0x1)
            /Users/ethanxu/Ethan/Learn/go_learning/src/other/other_test.go:16 +0x6d fp=0xc00003f7c8 sp=0xc00003f780 pc=0x110b36d
    runtime.goexit()
            /usr/local/go/src/runtime/asm_amd64.s:1374 +0x1 fp=0xc00003f7d0 sp=0xc00003f7c8 pc=0x1069861
    created by command-line-arguments.RunGo
            /Users/ethanxu/Ethan/Learn/go_learning/src/other/other_test.go:14 +0x9b
    

    原因

    经过排查,原因是因为在go中map是非线程安全的,并发写会产生竞争,多个协程同时写就会出现fatal error: concurrent map writes的错误,如以下程序, 执行就会报该错误:

    func RunGo() {
    	emap := make(map[int]int)
    	i := 0
    	wg := sync.WaitGroup{}
    	for i < 10 {
    		wg.Add(1)
    		go func(i int) {
    			defer wg.Done()
    			emap[i] = i
    		}(i)
    		i++
    	}
    	wg.Wait()
    	return
    }
    
    func TestMap(t *testing.T) {
    	RunGo()
    }

    因为map为引用类型,所以即使函数传值调用,参数副本依然指向映射m, 所以多个goroutine并发写同一个映射m, 写过多线程程序的同学都知道,对于共享变量,资源,并发读写会产生竞争的, 故共享资源遭到破坏。

    而本次报错的线上服务还比较隐晦,是协程中调用redis服务,而redis服务本身会写日志,而写日志的方法用到了map,所以导致线上爆出了该问题。

    解决

    改用并发安全的sync.map, 如:

    func RunGoV2() {
    	emap := sync.Map{}
    	i := 0
    	wg := sync.WaitGroup{}
    	for i < 10 {
    		wg.Add(1)
    		go func(i int) {
    			defer wg.Done()
    			emap.Store(i, i)
    		}(i)
    		i++
    	}
    	wg.Wait()
    }
    
    func TestMap(t *testing.T) {
    	RunGoV2()
    }
    

    参考文档

    https://golang.org/pkg/sync/#Map

  • docker部署Elasticsearch-head

    下载镜像

    docker pull mobz/elasticsearch-head:5

    启动镜像

    docker run -d -p 9100:9100 -d --name elasticsearch-head mobz/elasticsearch-head:5

    配置

    docker exec -it elasticsearch-head bash
    
    # 安装vim
    apt-get update
    apt-get install vim
    
    # 修改vendor.js
    vim _site/vendor.js
    
    application/x-www-form-urlencoded 改为 application/json;charset=UTF-8
    
    # 退出
    exit
    
    # 重启
    docker restart elasticsearch-head

    浏览器访问elasticsearch-head

    http://127.0.0.1:9100/

  • 读《月亮与六便士》有感

    读完《月亮与六便士》后,心中感慨莫名。

    理想与现实永远都是矛盾的,斯特里克兰这人,你可以恨他,骂他,任何恶毒的词汇用在他身上好像都不为过,但唯有一点,他对于心中美的追求与执着或者更准确些是偏执,确实让人不得不惊叹,甚至是羡慕。

    如果说,在斯特里克兰到达他最终的归属之地-塔希提之前,可以说他是暴躁的,压抑的,给人一种一股股如火山喷发般的热情却被死死的压制住的感觉,如同一只妄想奔腾草原但却只能在方寸之地四处碰壁的狂狮,那么,塔希提就是他无数次渴望的草原。

    在塔希提,一切好像都是为他量身定制的一般,有了支持他创作的各种完美素材,有着妻子的照顾,虽然他并不在意她们,但至少无需为生计担忧,无需被生活琐事烦扰。每天只要朝着他梦中的画作前进即可,其他的一切他无需理会。

    也许是上帝,哦,他也许并不信仰上帝,嗯,也许是他的执着与偏执感动了命运吧,赐予他这一处完美之地,让他的灵魂能够得到解放,让他那积攒了多年的热情能够尽情的喷发,汹涌,直至将他自己燃烧殆尽。

    斯特里克兰的结局是好,是坏?也许对于旁观人来讲,完全就是一个彻头彻尾的悲剧,但对于斯特里克兰来说,我想应该并不是,毕竟求而有得,也算是死得其所了。

    除了斯特里克兰之外,书中给我印象最深刻的莫过于德克·斯特洛夫了,一个彻头彻尾的悲剧性人物,甚至在我看来,他比斯克里克兰更加的可悲。

    也许在一些官方正式的文章里面,我可能会写说,斯特洛夫是一位十分热爱艺术,甚至不乏些许狂热的人等等正面的评价,但那也许只是我不愿意表露自己那也许并不是十分高尚的想法罢了。

    在我眼中,斯特洛夫可以说活的非常的卑微,甚至卑微的有些可笑。他本身并没有什么错,错就错在他认为任何的一切都比他更为重要,甚至是斯特里克兰这个夺走并害死他深爱的妻子的人,他也不忍毁掉斯特里克兰给他妻子画的画像,甚至于,出于他所认为的斯特里克兰所具有的超凡的绘画天赋,他甚至并没有很恨斯特里克兰,这是我最接受不了的,一个人究竟为何能够卑微到这种地步?也许是我的思想境界还有待提高吧,呵!

    最后,也许,我们每个人心中都有一个斯特里克兰,不过他都被亲情,爱情,友情等等人与人之间的纽带,相互纠缠,纹丝不漏的牢牢的锁死在了我们的心底,也许偶尔会从心底传来几声微不可闻的咆哮,但却注定徒劳。

  • 判断字符串是否为base64

    业务场景中,有时我们需要判断一个字符串是否为base64来决定是否需要进行base64解码。

    实际上,并没有什么非常完美的方法能够完全判断一个字符串是否为base64, 都存在一定的误差。

    解决方案1

    最佳的解决方案,就是从产品层面进行解决,明确规定该字符串必须是base64或非base64, 这样我们就能够清楚的知道是否需要进行base64解码

    解决方案2

    如果产品层面不好动,或者已经存在混淆不清的数据的话,那就只能尽可能的进行判断,通过下面的方法能够准确判断出绝大部分的字符串

    public static function isBase64($str) {
                if(preg_match('/^[0-9]*$/',$str) || preg_match('/^[a-zA-Z]*$/',$str)) {
                    return false;
                }
                $decode = base64_decode($str, true);
                return $decode && $str == base64_encode(base64_decode($str, true));
            }

    解决方案2-扩展

    如果你的业务场景,能够确定某一批数据格式是一致的,也就是都是base64或都是非base64, 那就可以应用方案2的代码,判断一部分数据,如果判断的结果,大部分都是base64格式的,那该批数据应该就都是base64格式的,否则相反,这样精确度会更高一些

  • 本地同步服务器代码

    开发过程中,大家都知道,很多公司都是使用跳板机来登录开发机,甚至开发机还禁用了ftp服务,这种情况下,本地同步开发机上的代码就成了一个问题。

    下面讲解一个解决方案,就是通过脚本,借助nc命令来解决。

    服务器运行脚本(back.sh)

    #!/bin/bash
    #!/bin/bash
    read -p "please enter your code dir path:" path
    if [ ! -d "/tmp/.code_bak" ];then
        mkdir  /tmp/.code_bak
    fi
    ori=$(pwd)
    cd $path/../
    target=$(basename $path)
    cp -r $target /tmp/.code_bak/$target
    cd $ori
    tar -zcvf code.tar.gz  /tmp/.code_bak/$target
    # 172.21.23.13 本地机IP,8222 指定未使用的本地机端口
    nc 172.21.23.13 8222 < code.tar.gz
    rm -rf code.tar.gz
    rm -rf /tmp/.code_bak
    echo 'ok'

    本地运行命令

    nc -l 8222> code.tar.gz // 将服务器代码压缩文件下载下来

    本地运行脚本(sync.sh)

    #!/bin/bash
    
    # 需要同步的本地代码根路径
    read -p "please enter your path:" path
    tar zxvf code.tar.gz
    if [ ! -d "$path" ];then
        mkdir -p $path
    fi
    cp -r tmp/.code_bak/* $path
    rm -rf code.tar.gz
    rm -rf tmp
    echo 'ok'

  • ssh避免重复输入密码

    工作中经常用到跳板机,每次打开一个新的标签,都需要输入一遍密码,特别麻烦,通过以下方法,可以只需输入一次密码,就可以无限的打开标签,无需再次输入密码。

    vim ~/.ssh/config
    host *
    ControlMaster auto
    ControlPath ~/.ssh/master-%r@%h:%p
  • shell pwd -P

    shell pwd -P 显示当前路径的物理路径,与pwd命令的结果,在以下这种情况是不同的:

    Ethan@Ethan-mac > ~/self_work/learn/shell > ln -s ~/env/data ./tmp
    
    Ethan@Ethan-mac > ~/self_work/learn/shell > ll
    total 0
    lrwxr-xr-x 1 Ethan staff 21B 9 22 17:53 tmp -> /Users/Ethan/env/data
    Ethan@Ethan-mac > ~/self_work/learn/shell > cd tmp
    
    Ethan@Ethan-mac > ~/self_work/learn/shell/tmp > pwd
    /Users/Ethan/self_work/learn/shell/tmp
    
    Ethan@Ethan-mac > ~/self_work/learn/shell/tmp > pwd -P
    /Users/Ethan/env/data
    
  • shell set -e

    shell脚本,set -e方法,可以是脚本遇到异常立即退出,不会继续向下执行。

    例如:

    #!/bin/sh
    
    ls /data/ethan
    
    echo r444

    执行结果为:

    ls: /data/ethan: No such file or directory
    r444

    而,加上 set -e 命令后:

    #!/bin/sh
    
    set -e
    
    ls /data/ethan
    
    echo r444%

    执行结果为:

    ls: /data/ethan: No such file or directory

    可以看出,执行出现异常后,后续代码不会再继续执行

  • leetcode 2: 两数相加

    题目

    给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。

    如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。

    您可以假设除了数字 0 之外,这两个数都不会以 0 开头。

    示例

    输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
    输出:7 -> 0 -> 8
    原因:342 + 465 = 807

    解法

    // 时间复杂度 O(max(m,n)) 空间复杂度 O(max(m,n))
    // 说明: 从个位数(链表第一个节点)开始,依次求和 sum, 若sum>9, 则记录进位appendVal, 与下一次求和结果sum + appendVal
    func addTwoNumbers1(l1 *ListNode, l2 *ListNode) *ListNode {
    	var l3 ListNode
    	p := &l3
    	appendVal := 0
    	for l1 != nil || l2 != nil {
    		sum := 0
    		if l1 != nil && l2 != nil {
    			sum = l1.Val + l2.Val
    			l1 = l1.Next
    			l2 = l2.Next
    		} else if l1 != nil && l2 == nil {
    			sum = l1.Val
    			l1 = l1.Next
    		} else if l1 == nil && l2 != nil {
    			sum = l2.Val
    			l2 = l2.Next
    		}
    		sum = sum + appendVal
    		appendVal = sum / 10
    		p.Val = sum % 10
    		
    		// 两个链表都已遍历完,且最后一次同位求和无进位时,当前节点,即为结果链表最后一个节点,否则,需要追加一个 Val=appendVal 的节点
    		if l1 == nil && l2 == nil {
    			if appendVal > 0 {
    				p.Next = &ListNode{Val: appendVal, Next: nil}
    			} else {
    				p.Next = nil
    			}
    		} else {
    			p.Next = &ListNode{Val: 0, Next: nil}
    			p = p.Next
    		}
    	}
    	return &l3
    }

    代码优化

    // 时间复杂度 O(max(m,n)) 空间复杂度 O(max(m,n))
    // 说明: 从个位数(链表第一个节点)开始,依次求和 sum, 若sum>9, 则记录进位appendVal, 与下一次求和结果sum + appendVal
    func addTwoNumbers2(l1 *ListNode, l2 *ListNode) *ListNode {
    	var l3 ListNode
    	p := &l3
    	appendVal := 0
    	for l1 != nil || l2 != nil {
    		sum := 0
    		if l1 != nil {
    			sum += l1.Val
    			l1 = l1.Next
    		}
    		if l2 != nil {
    			sum += l2.Val
    			l2 = l2.Next
    		}
    
    		sum = sum + appendVal
    
    		appendVal = sum / 10
    
    		p.Val = sum % 10
    
    		// 两个链表都已遍历完,且最后一次同位求和无进位时,当前节点,即为结果链表最后一个节点,否则,需要追加一个 Val=appendVal 的节点
    		if l1 == nil && l2 == nil && appendVal == 0 {
    			p.Next = nil
    		} else {
    			p.Next = &ListNode{Val: appendVal, Next: nil}
    			p = p.Next
    		}
    	}
    
    	return &l3
    }
  • leetcode 1: 两数之和

    题目

    给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。

    你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。

    示例

    给定 nums = [2, 7, 11, 15], target = 9
    因为 nums[0] + nums[1] = 2 + 7 = 9
    所以返回 [0, 1]

    解法1: 暴力解法

    两数之和暴力解法图解

    纯暴力解法,多次遍历数组,将数组中每个元素依次与其他元素相加,进行计算,循环嵌套,时间复杂度为O(n^2), 但因其为原地计算,没有额外申请空间,所以空间复杂度为O(1)

    // 暴力解法, 时间复杂度O(n^2), 空间复杂度O(1)
    func twoSum1(nums []int, target int) []int {
    	length := len(nums)
    	for i := 0; i < length - 1; i++ {
    		for j := i + 1; j < length; j++ {
    			if nums[i] + nums[j] == target {
    				return []int{i, j}
    			}
    		}
    	}
    	return []int{}
    }

    解法2: hash空间换时间

    空间换时间,新增一个map, 存储数组的键值,以值为key, 键为value, 遍历数组,若map中存在target – nums[i]的数据,则找到了结果,[i, map[target – nums[i]]], 如果nums中,i=0, v=2, map中 7=>1, target=9, 则遍历nums时,map[9 – 2] = map[7] = 1, 存在,则得结果[i, map[target – nums[i]]] => [0, 1]

    // hash, 空间换时间, 时间复杂度O(n), 空间复杂度O(n) 
    func twoSum2(nums []int, target int) []int {
    	length := len(nums)
    	numsMap := make(map[int]int)
    	for k, v := range nums {
    		numsMap[v] = k
    	}
    
    	for i := 0; i < length; i++ {
    		tmpTarget, exist := numsMap[target - nums[i]]
    
    		if exist && tmpTarget != i {
    			return []int{i, tmpTarget}
    		}
    	}
    	return []int{}
    }

    解法2优化:

    在解法2的基础上,减少一次遍历

    // hash, 空间换时间, 时间复杂度O(n), 空间复杂度O(n)
    func twoSum3(nums []int, target int) []int {
    	var result []int
    	m := make(map[int]int)
    	for k, v := range nums {
    		if value, exist := m[target - v]; exist && k != value {
    			result = append(result, value)
    			result = append(result, k)
    			break
    		}
    		m[v] = k
    	}
    	return result
    }