merge conflicts
@@ -7,14 +7,21 @@ comments: true
|
|||||||
author: lemonchann
|
author: lemonchann
|
||||||
---
|
---
|
||||||
|
|
||||||
VsCode 强大地自定义功能,已经成为程序员最爱编辑器。
|
软件设计过程中,有好几种图需要画,比如流程图、类图、组件图等,我知道大部分人画流程图一般都会用微软的viso绘制,我之前也是这个习惯,viso画图有个不好的地方是需要时刻去调整线条和边框已达到简洁美观,今天我给大家介绍一款程序员画图神器PlantUML,一款你用了就爱上的画图软件!
|
||||||
Microsoft在2015年4月30日Build 开发者大会上正式宣布了 Visual Studio Code 项目:一个运行于 Mac OS X、Windows和Linux之上的,针对于编写现代 Web 和云应用的跨平台源代码编辑器。
|
|
||||||
|
|
||||||
<!-- more -->
|
VsCode以插件的形式支持了这款画图神器,还不知道VsCode?
|
||||||
|
|
||||||
该编辑器也集成了所有一款现代编辑器所应该具备的特性,包括语法高亮(syntax high lighting),可定制的热键绑定(customizable keyboard bindings),括号匹配(bracket matching)以及代码片段收集(snippets)。Somasegar 也告诉笔者这款编辑器也拥有对 Git 的开箱即用的支持。引用[360百科](https://baike.so.com/doc/24428308-25261478.html)
|
> VsCode 强大地自定义功能,已经成为程序员最爱编辑器。
|
||||||
|
> Microsoft在2015年4月30日Build 开发者大会上正式宣布了 Visual Studio Code 项目:一个运行于 Mac OS X、Windows和Linux之上的,针对于编写现代 Web 和云应用的跨平台源代码编辑器。
|
||||||
|
|
||||||
|
> 该编辑器也集成了所有一款现代编辑器所应该具备的特性,包括语法高亮(syntax high lighting),可定制的热键绑定(customizable keyboard bindings),括号匹配(bracket matching)以及代码片段收集(snippets)。Somasegar 也告诉笔者这款编辑器也拥有对 Git 的开箱即用的支持。引用[360百科](https://baike.so.com/doc/24428308-25261478.html)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 主角出场
|
||||||
|
|
||||||
### PlantUML
|
### PlantUML
|
||||||
|
|
||||||
**PlantUML是一个开源项目,支持快速绘制:**
|
**PlantUML是一个开源项目,支持快速绘制:**
|
||||||
|
|
||||||
>时序图
|
>时序图
|
||||||
@@ -82,3 +89,4 @@ Alice <-- Bob: another authentication Response
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 好了,这么好用工具赶紧用起来吧!
|
||||||
@@ -7,7 +7,7 @@ comments: true
|
|||||||
author: lemonchann
|
author: lemonchann
|
||||||
---
|
---
|
||||||
|
|
||||||
傻瓜式一站式教你用github pages 来搭建博客,详细记录全过程。
|
作为一个程序员怎么能没有自己的个人博客呢,这里详细记录和分享我的博客搭建经验,让你轻轻松松拥有自己的博客网站。 傻瓜式一站式教你用github pages 来搭建博客,详细记录全过程。
|
||||||
|
|
||||||
<!-- more -->
|
<!-- more -->
|
||||||
|
|
||||||
@@ -204,7 +204,6 @@ url: https://yourname.github.io
|
|||||||
1. netstat -ano|findstr "4000" 找到占用4000端口的进程ID
|
1. netstat -ano|findstr "4000" 找到占用4000端口的进程ID
|
||||||
|
|
||||||
2. 查看最后一列数字就是PID=312964
|
2. 查看最后一列数字就是PID=312964
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
3. 打开windows资源管理器,结束该进程.
|
3. 打开windows资源管理器,结束该进程.
|
||||||
|
|||||||
@@ -7,13 +7,15 @@ comments: true
|
|||||||
author: lemonchann
|
author: lemonchann
|
||||||
---
|
---
|
||||||
|
|
||||||
文章是由自己笔试面试腾讯的笔记整理而来,整理的时候又回顾了一遍,中间工作忙断断续续整理了半个月,才完成现在的样子。主要是针对面试的C++后台开发岗位,涵盖了大部分C++相关的可能会被问到的技术点,作为面试技术的参考回头查阅。
|
**文章是由我笔试面试腾讯笔记整理而来,主要是针对面试的C++后台开发岗位,涵盖了大部分C++后台开发相关的,可能会考察和被问到的技术点。**
|
||||||
|
|
||||||
这篇笔记是基础C++知识点总结,没有过多的阐述后台开发的系统架构和分布式后台服务设计相关,还有c++11新特性,这些笔试面试也会被问到但不在这篇讨论范围,可以关注专栏后面如果有机会再补上。
|
**自认为这篇笔记比较全面的涵盖了,后台开发C++笔试面试大部分知识点,不管你是已经工作准备参加社招,还是在校学生准备参加校招,笔记都可以作为技术面试准备阶段参考查阅,查缺补漏。**
|
||||||
|
|
||||||
### 为什么析构函数要是虚函数?
|
笔记是基础C++知识点总结,没有过多的阐述后台开发的系统架构,和分布式后台服务设计相关内容,以及C++11新特性,这些在笔试面试也会被问到但不在这篇讨论范围,可以关注我后面有机会补上。
|
||||||
|
|
||||||
基类指针可以指向派生类的对象(多态性),如果删除该指针delete []p;就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。所以,将析构函数声明为虚函数是十分必要的。
|
### 阅读提示
|
||||||
|
|
||||||
|
文章约12839字,阅读时长预计33分钟。建议关注收藏方便回头查阅。
|
||||||
|
|
||||||
### gdb调试命令
|
### gdb调试命令
|
||||||
|
|
||||||
@@ -23,59 +25,59 @@ author: lemonchann
|
|||||||
|
|
||||||
#### 查看内存
|
#### 查看内存
|
||||||
|
|
||||||
(gdb)p &a //打印变量地址
|
> (gdb)p &a //打印变量地址
|
||||||
|
|
||||||
gdb)x 0xbffff543 //查看内存单元内变量
|
> (gdb)x 0xbffff543 //查看内存单元内变量
|
||||||
|
|
||||||
0xbffff543: 0x12345678
|
> 0xbffff543: 0x12345678
|
||||||
|
|
||||||
(gdb) x /4xb 0xbffff543 //单字节查看4个内存单元变量的值
|
> (gdb) x /4xb 0xbffff543 //单字节查看4个内存单元变量的值
|
||||||
|
|
||||||
0xbffff543: 0x78 0x56 0x34 0x12
|
> 0xbffff543: 0x78 0x56 0x34 0x12
|
||||||
|
|
||||||
#### 多线程调试
|
#### 多线程调试
|
||||||
|
|
||||||
(gdb) info threads:查看GDB当前调试的程序的各个线程的相关信息
|
> (gdb) info threads:查看GDB当前调试的程序的各个线程的相关信息
|
||||||
|
|
||||||
(gdb) thread threadno:切换当前线程到由threadno指定的线程
|
> (gdb) thread threadno:切换当前线程到由threadno指定的线程
|
||||||
|
|
||||||
break filename:linenum thread all 在所有线程相应行设置断点,注意如果主线程不会执行到该行,并且启动all-stop模式,主线程执行n或s会切换过去
|
> break filename:linenum thread all 在所有线程相应行设置断点,注意如果主线程不会执行到该行,并且启动all-stop模式,主线程执行n或s会切换过去
|
||||||
|
|
||||||
set scheduler-locking off|on\step 默认off,执行s或c其它线程也同步执行。on,只有当前相称执行。step,只有当前线程执行
|
> set scheduler-locking off|on\step 默认off,执行s或c其它线程也同步执行。on,只有当前相称执行。step,只有当前线程执行
|
||||||
|
|
||||||
show scheduler-locking 显示当前模式
|
> show scheduler-locking 显示当前模式
|
||||||
|
|
||||||
thread apply all command 每个线程执行同意命令,如bt。或者thread apply 1 3 bt,即线程1,3执行bt。
|
> thread apply all command 每个线程执行同意命令,如bt。或者thread apply 1 3 bt,即线程1,3执行bt。
|
||||||
|
|
||||||
#### 查看调用堆栈
|
#### 查看调用堆栈
|
||||||
|
|
||||||
(gdb)bt
|
> (gdb)bt
|
||||||
|
|
||||||
(gdb)f 1 帧简略信息
|
> (gdb)f 1 //帧简略信息
|
||||||
|
|
||||||
(gdb)info f 1 帧详细信息
|
> (gdb)info f 1 //帧详细信息
|
||||||
|
|
||||||
#### 断点
|
#### 断点
|
||||||
|
|
||||||
b test.cpp:11
|
> b test.cpp:11
|
||||||
|
|
||||||
b test.cpp:main
|
> b test.cpp:main
|
||||||
|
|
||||||
gdb attach 调试方法:
|
gdb attach 调试方法:
|
||||||
|
|
||||||
gdb->file xxxx->attach pid->这时候进程是停止的->c 继续运行
|
> gdb->file xxxx->attach pid->**这时候进程是停止的**->c 继续运行
|
||||||
|
|
||||||
#### 带参数调试
|
#### 带参数调试
|
||||||
|
|
||||||
输入参数命令set args 后面加上程序所要用的参数,注意,不再带有程序名,直接加参数,如:
|
输入参数命令set args 后面加上程序所要用的参数,注意,不再带有程序名,直接加参数,如:
|
||||||
|
|
||||||
(gdb)set args -l a -C abc
|
> (gdb)set args -l a -C abc
|
||||||
|
|
||||||
#### list命令
|
#### list命令
|
||||||
|
|
||||||
list linenum 显示程序第linenum行的周围的程序
|
> list linenum //显示程序第linenum行的周围的程序
|
||||||
|
|
||||||
list function 显示程序名为function的函数的源程序
|
> list function //显示程序名为function的函数的源程序
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -87,23 +89,23 @@ list function 显示程序名为function的函数的源程序
|
|||||||
|
|
||||||
ln -s 源文件 目标文件, ln -s / /home/good/linkname链接根目录/到/home/good/linkname
|
ln -s 源文件 目标文件, ln -s / /home/good/linkname链接根目录/到/home/good/linkname
|
||||||
|
|
||||||
1、软链接就是:“ln –s 源文件 目标文件”,只会在选定的位置上生成一个文件的镜像,不会占用磁盘空间,类似与windows的快捷方式。
|
1. 软链接就是:“ln –s 源文件 目标文件”,只会在选定的位置上生成一个文件的镜像,不会占用磁盘空间,类似与windows的快捷方式。
|
||||||
|
|
||||||
2、硬链接ln源文件目标文件,没有参数-s, 会在选定的位置上生成一个和源文件大小相同的文件,无论是软链接还是硬链接,文件都保持同步变化。
|
2. 硬链接ln源文件目标文件,没有参数-s, 会在选定的位置上生成一个和源文件大小相同的文件,无论是软链接还是硬链接,文件都保持同步变化。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 函数指针
|
### 函数指针
|
||||||
|
|
||||||
函数指针 int (*func)(int, int)
|
int (*func)(int, int) //函数指针
|
||||||
|
|
||||||
函数指针数组 int (*funcArry[10])(int, int)
|
int (*funcArry[10])(int, int) //函数指针数组
|
||||||
|
|
||||||
const int* p; 指向const int的指针
|
const int* p; //指向const int的指针
|
||||||
|
|
||||||
int const* p; 同上
|
int const* p; //同上
|
||||||
|
|
||||||
int* const p; const指针
|
int* const p; //const指针
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -436,45 +438,34 @@ class LayerManager : public ILayerManager{};
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 为什么析构函数要是虚函数?
|
||||||
|
|
||||||
|
基类指针可以指向派生类的对象(多态性),如果删除该指针delete []p;就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。所以,将析构函数声明为虚函数是十分必要的。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 覆盖虚函数机制
|
#### 覆盖虚函数机制
|
||||||
|
|
||||||
在某些情况下,希望覆盖虚函数机制并强制函数调用使用虚函数的特定版
|
在某些情况下,希望覆盖虚函数机制并强制函数调用使用虚函数的特定版
|
||||||
|
|
||||||
本,这里可以使用作用域操作符:
|
本,这里可以使用作用域操作符:
|
||||||
|
|
||||||
|
```c++
|
||||||
Item_base *baseP = &derived;
|
Item_base *baseP = &derived;
|
||||||
|
|
||||||
// calls version from the base class regardless of the dynamic type
|
// calls version from the base class regardless of the dynamic type of baseP
|
||||||
|
|
||||||
of baseP
|
|
||||||
|
|
||||||
double d = baseP->Item_base::net_price(42);
|
double d = baseP->Item_base::net_price(42);
|
||||||
|
```
|
||||||
|
|
||||||
这段代码强制将 net_price 调用确定为 Item_base 中定义的版本,该调用
|
这段代码强制将 net_price 调用确定为 Item_base 中定义的版本,该调用
|
||||||
|
|
||||||
将在编译时确定。
|
将在编译时确定。**只有成员函数中的代码才应该使用作用域操作符覆盖虚函数机制。**
|
||||||
|
|
||||||
只有成员函数中的代码才应该使用作用域操作符覆盖虚函数机制。
|
**为什么会希望覆盖虚函数机制?最常见的理由是为了派生类虚函数调用基类中的版本。**在这种情况下,基类版本可以完成继承层次中所有类型的公共任务,而每个派生类型只添加自己的特殊工作。
|
||||||
|
|
||||||
为什么会希望覆盖虚函数机制?最常见的理由是为了派生类虚函数调用基
|
例如,可以定义一个具有虚操作的 Camera 类层次。Camera 类中的 display函数可以显示所有的公共信息,派生类(如 PerspectiveCamera)可能既需要显示公共信息又需要显示自己的独特信息。可以显式调用 Camera 版本以显示公共信息,而不是在 PerspectiveCamera 的 display 实现中复制 Camera 的操作。
|
||||||
|
|
||||||
类中的版本。在这种情况下,基类版本可以完成继承层次中所有类型的公共任务,
|
在这种情况下,已经确切知道调用哪个实例,因此,不需要通过虚函数机制。派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果派生类函数忽略了这样做,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归。
|
||||||
|
|
||||||
而每个派生类型只添加自己的特殊工作。例如,可以定义一个具有虚操作的 Camera 类层次。Camera 类中的 display
|
|
||||||
|
|
||||||
函数可以显示所有的公共信息,派生类(如 PerspectiveCamera)可能既需要显
|
|
||||||
|
|
||||||
示公共信息又需要显示自己的独特信息。可以显式调用 Camera 版本以显示公共
|
|
||||||
|
|
||||||
信息,而不是在 PerspectiveCamera 的 display 实现中复制 Camera 的操作。
|
|
||||||
|
|
||||||
在这种情况下,已经确切知道调用哪个实例,因此,不需要通过虚函数机制。
|
|
||||||
|
|
||||||
派生类虚函数调用基类版本时,必须显式使用作用域操作符。
|
|
||||||
|
|
||||||
如果派生类函数忽略了这样做,则函数调用会在运行时确定并
|
|
||||||
|
|
||||||
且将是一个自身调用,从而导致无穷递归。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -526,6 +517,8 @@ struct Derived : Base
|
|||||||
|
|
||||||
设计派生类时,只要可能,最好避免与基类数据成员的名字相同
|
设计派生类时,只要可能,最好避免与基类数据成员的名字相同
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 类成员函数的重载、覆盖和隐藏区别?
|
#### 类成员函数的重载、覆盖和隐藏区别?
|
||||||
|
|
||||||
a.成员函数被重载的特征:
|
a.成员函数被重载的特征:
|
||||||
|
|||||||
257
_posts/2020-1-28-redis_distributed_locks.md
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "redis分布式锁3种实现方式对比分析总结"
|
||||||
|
date: 2020-1-28
|
||||||
|
tags: [后台开发]
|
||||||
|
comments: true
|
||||||
|
author: lemonchann
|
||||||
|
---
|
||||||
|
|
||||||
|
大家春节在家抢红包玩的不亦乐乎,抢红包服务看起来非常简单,实际上要做好这个服务,特别是money相关服务是不允许出错的,想想看每个红包的数字都是真金白银,要求服务的鲁棒性非常高,背后包含着很多后台服务技术细节。
|
||||||
|
|
||||||
|
<!-- more -->
|
||||||
|
|
||||||
|
## 什么是锁
|
||||||
|
|
||||||
|
后台开发中锁的概念是「实现多个进程或线程互斥的访问共享资源的一种机制」,这里的计算机术语我举个栗子你就能理解:
|
||||||
|
|
||||||
|
> 小王家只有卧室一台电视机。小王他爸喜欢看篮球NBA,小王他妈喜欢追综艺,如果小王他爸妈一起抢着看就会打架谁都看不好,这就是「死锁」。
|
||||||
|
>
|
||||||
|
> 怎么办?小王他爸每次进入房间看电视第一件事就是把房门锁上,同样的小王他妈每次进房间看综艺第一件事也是把房门锁上,这就是「加锁」。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
在计算机中公共资源可以是一块公共的内存,或者是一个公共的文件,对于这类共享资源的访问都是需要「加锁」保证各个进程或线程的资源访问互相不干扰。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 什么是分布式锁
|
||||||
|
|
||||||
|
分布式锁是在分布式系统中提出的概念,所谓分布式是指由很多功能对等的节点,提供相同的服务,各个节点如果需要访问「共享资源」,为了保证数据一致性也需要「加锁」,这个锁可以放在「公共存储数据库」,访问共享资源之前先去公共存储数据库拿锁,拿到锁才能访问共享资源。
|
||||||
|
|
||||||
|
还是拿上面的小王来举例子:
|
||||||
|
|
||||||
|
> 现在小王的村里只有一个电视(小王村真穷),现在这个电视不是属于小王家,整个村的人都看这一个电视,并且要求一家在看的时候其他家不能看(这是看的啥电视),以前小王家的锁不能锁村里的电视,那怎么办呢?
|
||||||
|
>
|
||||||
|
> 村里每个家庭就是一个「分布式节点」,一个解决方案是把电视放在村长家「公共存储数据库」,各家轮流去村长家看电视,并且在进去看的时候让村长关门「加锁」,这就是分布式锁。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 分布式锁实现
|
||||||
|
|
||||||
|
今天就来说说其中一个技术细节,也是在我另一篇文章[Linux后台开发C++学习路线技能加点](https://zhuanlan.zhihu.com/p/102048769)中提到但没展开讲的,高并发服务编程中的**redis分布式锁**。
|
||||||
|
|
||||||
|
这里罗列出**3种redis实现的分布式锁**,并分别对比说明各自特点。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Redis单实例分布式锁
|
||||||
|
|
||||||
|
### 实现一: SETNX实现的分布式锁
|
||||||
|
|
||||||
|
setnx用法参考redis[官方文档](https://redis.io/commands/setnx)
|
||||||
|
|
||||||
|
#### 语法
|
||||||
|
|
||||||
|
`SETNX key value`
|
||||||
|
|
||||||
|
将`key`设置值为`value`,如果`key`不存在,这种情况下等同SET命令。 当`key`存在时,什么也不做。`SETNX`是”**SET** if **N**ot e**X**ists”的简写。
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
|
||||||
|
- 1 设置key成功
|
||||||
|
- 0 设置key失败
|
||||||
|
|
||||||
|
#### 加锁步骤
|
||||||
|
|
||||||
|
1. ```SETNX lock.foo <current Unix time + lock timeout + 1>```
|
||||||
|
|
||||||
|
如果客户端获得锁,`SETNX`返回`1`,加锁成功。
|
||||||
|
|
||||||
|
如果`SETNX`返回`0`,那么该键已经被其他的客户端锁定。
|
||||||
|
|
||||||
|
2. 接上一步,`SETNX`返回`0`加锁失败,此时,调用`GET lock.foo`获取时间戳检查该锁是否已经过期:
|
||||||
|
|
||||||
|
- 如果没有过期,则休眠一会重试。
|
||||||
|
|
||||||
|
- 如果已经过期,则可以获取该锁。具体的:调用`GETSET lock.foo <current Unix timestamp + lock timeout + 1>`基于当前时间设置新的过期时间。
|
||||||
|
|
||||||
|
**注意**: 这里设置的时候因为在`SETNX`与`GETSET`之间有个窗口期,在这期间锁可能已被其他客户端抢去,所以这里需要判断`GETSET`的返回值,他的返回值是SET之前旧的时间戳:
|
||||||
|
|
||||||
|
- 若旧的时间戳已过期,则表示加锁成功。
|
||||||
|
- 若旧的时间戳还未过期(说明被其他客户端抢去并设置了时间戳),代表加锁失败,需要等待重试。
|
||||||
|
|
||||||
|
#### 解锁步骤
|
||||||
|
|
||||||
|
解锁相对简单,只需`GET lock.foo`时间戳,判断是否过期,过期就调用删除`DEL lock.foo`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 实现二:SET实现的分布式锁
|
||||||
|
|
||||||
|
set用法参考[官方文档](https://redis.io/commands/set)
|
||||||
|
|
||||||
|
#### 语法
|
||||||
|
|
||||||
|
`SET key value [EX seconds|PX milliseconds] [NX|XX]`
|
||||||
|
|
||||||
|
将键`key`设定为指定的“字符串”值。如果 `key` 已经保存了一个值,那么这个操作会直接覆盖原来的值,并且忽略原始类型。当`set`命令执行成功之后,之前设置的过期时间都将失效。
|
||||||
|
|
||||||
|
从2.6.12版本开始,redis为`SET`命令增加了一系列选项:
|
||||||
|
|
||||||
|
- `EX` *seconds* – Set the specified expire time, in seconds.
|
||||||
|
- `PX` *milliseconds* – Set the specified expire time, in milliseconds.
|
||||||
|
- `NX` – Only set the key if it does not already exist.
|
||||||
|
- `XX` – Only set the key if it already exist.
|
||||||
|
- `EX` *seconds* – 设置键key的过期时间,单位时秒
|
||||||
|
- `PX` *milliseconds* – 设置键key的过期时间,单位是毫秒
|
||||||
|
- `NX` – 只有键key不存在的时候才会设置key的值
|
||||||
|
- `XX` – 只有键key存在的时候才会设置key的值
|
||||||
|
|
||||||
|
版本\>= 6.0
|
||||||
|
|
||||||
|
- `KEEPTTL` -- 保持 key 之前的有效时间TTL
|
||||||
|
|
||||||
|
#### 加锁步骤
|
||||||
|
|
||||||
|
一条命令即可加锁: `SET resource_name my_random_value NX PX 30000`
|
||||||
|
|
||||||
|
The command will set the key only if it does not already exist (NX option), with an expire of 30000 milliseconds (PX option). The key is set to a value “my*random*value”. This value must be unique across all clients and all lock requests.
|
||||||
|
|
||||||
|
这个命令只有当`key` 对应的键不存在resource_name时(NX选项的作用)才生效,同时设置30000毫秒的超时,成功设置其值为my_random_value,这是个在所有redis客户端加锁请求中全局唯一的随机值。
|
||||||
|
|
||||||
|
#### 解锁步骤
|
||||||
|
|
||||||
|
解锁时需要确保my_random_value和加锁的时候一致。下面的Lua脚本可以完成
|
||||||
|
|
||||||
|
```lau
|
||||||
|
if redis.call("get",KEYS[1]) == ARGV[1] then
|
||||||
|
return redis.call("del",KEYS[1])
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
这段Lua脚本在执行的时候要把前面的`my_random_value`作为`ARGV[1]`的值传进去,把`resource_name`作为`KEYS[1]`的值传进去。释放锁其实包含三步操作:’GET’、判断和’DEL’,用Lua脚本来实现能保证这三步的原子性。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Redis集群分布式锁
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 实现三:Redlock
|
||||||
|
|
||||||
|
前面两种分布式锁的实现都是针对单redis master实例,既不是有互为备份的slave节点也不是多master集群,如果是redis集群,每个redis master节点都是独立存储,这种场景用前面两种加锁策略有锁的安全性问题。
|
||||||
|
|
||||||
|
比如下面这种场景:
|
||||||
|
|
||||||
|
> 1. 客户端1从Master获取了锁。
|
||||||
|
> 2. Master宕机了,存储锁的key还没有来得及同步到Slave上。
|
||||||
|
> 3. Slave升级为Master。
|
||||||
|
> 4. 客户端2从新的Master获取到了对应同一个资源的锁。
|
||||||
|
>
|
||||||
|
> 于是,客户端1和客户端2同时持有了同一个资源的锁。锁的安全性被打破。
|
||||||
|
|
||||||
|
针对这种多redis服务实例的场景,redis作者antirez设计了**Redlock** (Distributed locks with Redis)算法,就是我们接下来介绍的。
|
||||||
|
|
||||||
|
### 加锁步骤
|
||||||
|
|
||||||
|
**集群加锁的总体思想是尝试锁住所有节点,当有一半以上节点被锁住就代表加锁成功。集群部署你的数据可能保存在任何一个redis服务节点上,一旦加锁必须确保集群内任意节点被锁住,否则也就失去了加锁的意义。**
|
||||||
|
|
||||||
|
具体的:
|
||||||
|
|
||||||
|
1. 获取当前时间(毫秒数)。
|
||||||
|
2. 按顺序依次向N个Redis节点执行**获取锁**的操作。这个获取操作跟前面基于单Redis节点的**获取锁**的过程相同,包含随机字符串`my_random_value`,也包含过期时间(比如`PX 30000`,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个**获取锁**的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。这里的失败,应该包含任何类型的失败,比如该Redis节点不可用,或者该Redis节点上的锁已经被其它客户端持有(注:Redlock原文中这里只提到了Redis节点不可用的情况,但也应该包含其它的失败情况)。
|
||||||
|
3. 计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。
|
||||||
|
4. 如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。
|
||||||
|
5. 如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起**释放锁**的操作(即前面介绍的Redis Lua脚本)。
|
||||||
|
|
||||||
|
### 解锁步骤
|
||||||
|
|
||||||
|
客户端向所有Redis节点发起释放锁的操作,不管这些节点当时在获取锁的时候成功与否。
|
||||||
|
|
||||||
|
### 算法实现
|
||||||
|
|
||||||
|
上面描述的算法已经有现成的实现,各种语言版本。
|
||||||
|
|
||||||
|
- [Redlock-rb](https://github.com/antirez/redlock-rb) (Ruby implementation). There is also a [fork of Redlock-rb](https://github.com/leandromoreira/redlock-rb) that adds a gem for easy distribution and perhaps more.
|
||||||
|
- [Redlock-py](https://github.com/SPSCommerce/redlock-py) (Python implementation).
|
||||||
|
- [Aioredlock](https://github.com/joanvila/aioredlock) (Asyncio Python implementation).
|
||||||
|
- [Redlock-php](https://github.com/ronnylt/redlock-php) (PHP implementation).
|
||||||
|
- [PHPRedisMutex](https://github.com/malkusch/lock#phpredismutex) (further PHP implementation)
|
||||||
|
- [cheprasov/php-redis-lock](https://github.com/cheprasov/php-redis-lock) (PHP library for locks)
|
||||||
|
- [Redsync](https://github.com/go-redsync/redsync) (Go implementation).
|
||||||
|
- [Redisson](https://github.com/mrniko/redisson) (Java implementation).
|
||||||
|
- [Redis::DistLock](https://github.com/sbertrang/redis-distlock) (Perl implementation).
|
||||||
|
- [Redlock-cpp](https://github.com/jacket-code/redlock-cpp) (C++ implementation).
|
||||||
|
- [Redlock-cs](https://github.com/kidfashion/redlock-cs) (C#/.NET implementation).
|
||||||
|
- [RedLock.net](https://github.com/samcook/RedLock.net) (C#/.NET implementation). Includes async and lock extension support.
|
||||||
|
- [ScarletLock](https://github.com/psibernetic/scarletlock) (C# .NET implementation with configurable datastore)
|
||||||
|
- [Redlock4Net](https://github.com/LiZhenNet/Redlock4Net) (C# .NET implementation)
|
||||||
|
- [node-redlock](https://github.com/mike-marcacci/node-redlock) (NodeJS implementation). Includes support for lock extension.
|
||||||
|
|
||||||
|
### 比如我用的C++实现
|
||||||
|
|
||||||
|
[源码在这](https://github.com/jacket-code/redlock-cpp)
|
||||||
|
|
||||||
|
#### 创建分布式锁管理类CRedLock
|
||||||
|
|
||||||
|
```c++
|
||||||
|
CRedLock * dlm = new CRedLock();
|
||||||
|
dlm->AddServerUrl("127.0.0.1", 5005);
|
||||||
|
dlm->AddServerUrl("127.0.0.1", 5006);
|
||||||
|
dlm->AddServerUrl("127.0.0.1", 5007);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 加锁并设置超时时间
|
||||||
|
|
||||||
|
```c++
|
||||||
|
CLock my_lock;
|
||||||
|
bool flag = dlm->Lock("my_resource_name", 1000, my_lock);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 加锁并保持直到释放
|
||||||
|
|
||||||
|
```c++
|
||||||
|
CLock my_lock;
|
||||||
|
bool flag = dlm->ContinueLock("my_resource_name", 1000, my_lock);
|
||||||
|
```
|
||||||
|
|
||||||
|
`my_resource_name`是加锁标识;`1000`是锁的有效期,单位毫秒。
|
||||||
|
|
||||||
|
#### 加锁失败返回false, 加锁成功返回`Lock`结构如下
|
||||||
|
|
||||||
|
```c++
|
||||||
|
class CLock {
|
||||||
|
public:
|
||||||
|
int m_validityTime; => 9897.3020019531 // 当前锁可以存活的时间, 毫秒
|
||||||
|
sds m_resource; => my_resource_name // 要锁住的资源名称
|
||||||
|
sds m_val; => 53771bfa1e775 // 锁住资源的进程随机名字
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 解锁
|
||||||
|
|
||||||
|
```c++
|
||||||
|
dlm->Unlock(my_lock);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
综上所述,三种实现方式。
|
||||||
|
|
||||||
|
- 单redis实例场景,分布式锁实现一和实现二都可以,实现二更简洁推荐用实现二,用实现三也可以,但是实现三有点复杂略显笨重。
|
||||||
|
- 多redis实例场景推荐用实现三最安全,不过实现三也不是完美无瑕,也有针对这种算法缺陷的讨论(节点宕机同步时延、时间同步假设),大家还需要根据自身业务场景灵活选择或定制自己的分布式锁。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 参考
|
||||||
|
|
||||||
|
[Distributed locks with Redis](https://redis.io/topics/distlock)
|
||||||
|
|
||||||
|
[How to do distributed locking](https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html)
|
||||||
|
|
||||||
|
[基于Redis的分布式锁到底安全吗](http://zhangtielei.com/posts/blog-redlock-reasoning.html)
|
||||||
92
_posts/2020-1-29-red_packet_thinking_lock.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "后台服务高并发编程-抢红包"
|
||||||
|
date: 2020-1-27
|
||||||
|
tags: [后台开发]
|
||||||
|
comments: true
|
||||||
|
author: lemonchann
|
||||||
|
---
|
||||||
|
|
||||||
|
今年春节响应国家号召在家宅着抵抗疫情,拜年也改用微信红包,春节发了很多也抢了很多微信红包,也算支持了公司业务,想到WXG的小伙伴丰厚的年终奖我柠檬了,微信支付融入生活,抢红包已经是非常平常的事情。
|
||||||
|
|
||||||
|
抢红包这一简单的动作,每一次都是对红包服务后台的一次请求,在春节期间海量的服务请求下,其实是一个很典型的高并发编程模型。后台开发程序员都有一个共识:**实现一个功能很容易,难的是大量请求下提高服务性能**。
|
||||||
|
|
||||||
|
在程序员眼里,大家抢的不是红包,是红包后台服务的**锁** !这里的**锁**不是我们日常生活中的锁,后台服务编程中锁的概念:
|
||||||
|
|
||||||
|
> 实现多个进程或线程互斥的访问共享资源的一种机制
|
||||||
|
|
||||||
|
### 今天和大家聊聊后台服务编程中的锁。
|
||||||
|
|
||||||
|
## 业务模型
|
||||||
|
|
||||||
|
为便于说明,我们简化模型,约定抢红包服务是多线程服务,抢红包操作包含以下3个步骤:
|
||||||
|
|
||||||
|
1. 查询数据库内红包余额
|
||||||
|
2. 扣除抢到的红包金额
|
||||||
|
3. 更新红包余额到数据库
|
||||||
|
|
||||||
|
假设发了100块钱红包,1000个人1秒内同时来抢(高并发),如果不加锁是这样的情况:
|
||||||
|
|
||||||
|
- 第一个人查余额得到100元,他在此基础上扣除抢到的假设2元,准备步骤3更新到数据库。
|
||||||
|
- 在第一个人更新进去之前,此时剩下的人查到的余额也是100,他们各自扣除抢到的金额,准备按步骤3更新。
|
||||||
|
- 导致最后的红包余额只记录了最后一次更新的数据。
|
||||||
|
- 很明显,这就可能出现1000个人都抢到红包,但是红包余额还没分完的情况,这就乱了。
|
||||||
|
|
||||||
|
怎么解决这个问题呢? 就用到我们上面说的**加锁**来解决。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 有哪些锁
|
||||||
|
|
||||||
|
实现锁的方式有很多,这里列举几种常见的分类
|
||||||
|
|
||||||
|
### 悲观锁
|
||||||
|
|
||||||
|
> 顾名思义就是悲观的做最坏打算的锁机制,占有锁期间独占资源。
|
||||||
|
|
||||||
|
悲观锁把抢红包这三个步骤打包成一个整体做成互斥操作,**“在我抢了没更新数据之前你别来查余额,查到也不准确”**。也可以类比数据库的**事务**来理解。
|
||||||
|
|
||||||
|
> **事务必须具备以下四个属性,简称ACID 属性:**
|
||||||
|
> `原子性(Atomicity)`:事务是一个完整的操作。事务的各步操作是不可分的(原子的);要么都执 行,要么都不执行
|
||||||
|
> `一致性(Consistency)`:当事务完成时,数据必须处于一致状态
|
||||||
|
> `隔离性(Isolation)`:对数据进行修改的所有并发事务是彼此隔离的,这表明事务必须是独立的,它不应以任何方式依赖于或影响其他事务
|
||||||
|
> `永久性(Durability)`:事务完成后,它对数据库的修改被永久保持,事务日志能够保持事务的永久性
|
||||||
|
|
||||||
|
它悲观的认为你每次去抢红包必然有其他人也同时在抢,所以你这条线程在抢的时候要独占资源,其他线程需要阻塞挂起等待你抢完才能进来抢,挂起的线程就干不了其他事了。
|
||||||
|
|
||||||
|
> 鲁迅先生说过,浪费CPU资源就是浪费生命!
|
||||||
|
|
||||||
|
而一旦你抢完红包释放了锁,其他在等待中的线程又要抢占资源、抢到了还要恢复线程上下文。
|
||||||
|
|
||||||
|
CPU不断的切换线程上下文非常浪费服务器资源,严重的会导致不能及时处理后续抢红包请求,需要想办法提高效率,于是有了**乐观锁**
|
||||||
|
|
||||||
|
### 乐观锁
|
||||||
|
|
||||||
|
> 乐观锁是对悲观锁的改进,乐观的认为加锁的时候没有竞争,乐观锁不阻塞线程。
|
||||||
|
|
||||||
|
一种实现乐观锁的方法是**数据库内红包余额增加版本号**,初始版本号是0,每次抢完红包版本号加1后再去更新余额,**只有更新的版本号大于数据库内的版本号才认为是合法的,予以更新;否则不予更新,线程不阻塞可以稍后重试,**避免频繁切换线程上下文。
|
||||||
|
|
||||||
|
乐观锁在抢红包的步骤1、2不做加锁判断,在步骤3的时候才做加锁判断版本号。
|
||||||
|
|
||||||
|
- 第一个人抢到版本号是0的红包,第二个人也抢到版本号是0的红包
|
||||||
|
- 第一个人更新红包余额并设置版本号为1
|
||||||
|
- 第二个人更新红包余额设置版本号为1的时候发现余额版本号已经为1,更新失败
|
||||||
|
- 第二个人更新失败后,**线程不阻塞,继续处理其他抢红包抢请求**,按**一定策略重试**(超时重试、有限次数重试)第二个人的更新操作
|
||||||
|
- 其他请求以此类推
|
||||||
|
|
||||||
|
可以看到,乐观锁在加锁失败的时候不挂起线程等待,避免了线程上下文频繁的切换,提高红包服务处理性能。
|
||||||
|
|
||||||
|
### 分布式锁
|
||||||
|
|
||||||
|
上面两种锁的形式都是基于对数据库的更新来做的,在大请求高并发的时候,频繁的存取数据库,尤其是乐观锁重试会对数据库产生很大的冲击,在实际生产环境要尽量减少对数据库的访问。
|
||||||
|
|
||||||
|
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。也可以用redis实现**分布式锁**,与数据库交互两次:第一次获取红包余额,第二次抢完更新红包状态。抢红包和中间过程更新操作都在内存中进行,这可比数据库操作快了几个数量级,显著改善服务并发性能。
|
||||||
|
|
||||||
|
redis分布式锁:
|
||||||
|
|
||||||
|
> 利用Redis的SET操作在内存中保存key-value键值对,加锁就是获取这个键值对的值,解锁就是删除这个键值对。
|
||||||
|
|
||||||
|
分布式锁也不阻塞线程,关于这种分布式锁的实现不在这里展开说明,参考我另一篇公众号文章,[redis分布式锁3种实现方式分析](1)
|
||||||
|
|
||||||
|
#### 更多原创技术干货分享在我的公众号:柠檬橙学编程 欢迎关注。
|
||||||
|
|
||||||
@@ -155,6 +155,24 @@ author: lemonchann
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## TCP/IP协议
|
||||||
|
|
||||||
|
目前网络通信中应用最广泛的协议就是IP TCP协议,后面Unix提供的TCP套接字也是基于协议实现,所以很有必要系统的学习 TCP/IP 协议。
|
||||||
|
|
||||||
|
#### 推荐书:
|
||||||
|
|
||||||
|
大学的计算机网络教程
|
||||||
|
|
||||||
|
[《TCP/IP详解 卷1:协议》](https://book.douban.com/subject/1088054/)
|
||||||
|
|
||||||
|
[《TCP/IP详解 卷2:实现》](https://book.douban.com/subject/1087767/)
|
||||||
|
|
||||||
|
[《TCP/IP详解 卷3:TCP事务协议、HTTP、NNTP和UNIX域协议》](https://book.douban.com/subject/1058634/)
|
||||||
|
|
||||||
|
这几本书很厚,可以先看卷1、卷3
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Linux网络编程套接字
|
## Linux网络编程套接字
|
||||||
|
|
||||||
在同一台机器上进程间的通信(IPC)有多种方式,可以是通过**消息队列、FIFO、共享内存**等方式。网络编程套接字是指:分布在不同机器上的程序通过系统提供的网络通信接口,跨越网络将不同机器上的进程连接起来,实现跨机器的网络通信。一般有**UDP套接字、TCP套接字、Unix Domain,当然,如果你是通信从业者对SCTP套接字肯定也不会陌生。**
|
在同一台机器上进程间的通信(IPC)有多种方式,可以是通过**消息队列、FIFO、共享内存**等方式。网络编程套接字是指:分布在不同机器上的程序通过系统提供的网络通信接口,跨越网络将不同机器上的进程连接起来,实现跨机器的网络通信。一般有**UDP套接字、TCP套接字、Unix Domain,当然,如果你是通信从业者对SCTP套接字肯定也不会陌生。**
|
||||||
@@ -228,7 +246,7 @@ author: lemonchann
|
|||||||
|
|
||||||
## 一个网站
|
## 一个网站
|
||||||
|
|
||||||
这个网站一定要告诉大家,网站就是个C++百科全书,类似Linux的man手册,平常开发查忘记了函数名或者容器用法直接搜索非常方便,我下载了离线版本。
|
这个网站一定要告诉大家,网站就是个C++百科全书,类似Linux的man手册,平常开发查忘记了函数名或者容器用法直接搜索非常方便。
|
||||||
|
|
||||||
网址:C++参考: [cppreference](https://en.cppreference.com/w/cpp)
|
网址:C++参考: [cppreference](https://en.cppreference.com/w/cpp)
|
||||||
|
|
||||||
@@ -236,7 +254,8 @@ author: lemonchann
|
|||||||
|
|
||||||
## 待续
|
## 待续
|
||||||
|
|
||||||
一口气写下来肯定还不够完善,文章会保持更新和修改,想到了再补充吧。感兴趣可以关注我和专栏接收更新提醒。
|
一口气写下来肯定还不够完善,文章会保持更新和修改,想到了再补充吧。感兴趣可以关注我的微信公众号 **后端技术学堂** 更多干货和有趣的技术分享。
|
||||||
|
|
||||||
|
**我整理了文中提到和推荐的电子书与视频教材**,都是好几年学习过程中收集的,关注微信公众号 **后端技术学堂** 回复 【**1024**】 免费分享给大家。
|
||||||
|
|
||||||
我整理了文中提到和推荐的电子书与视频教材,都是学习过程收集的,关注微信公众号 **柠檬橙学编程** 回复 **1024** 免费分享给大家。
|
|
||||||
|
|
||||||
|
|||||||
210
_posts/2020-2-12-linux_date.md
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "多面手linux date命令"
|
||||||
|
date: 2020-1-27
|
||||||
|
tags: [后台开发]
|
||||||
|
comments: true
|
||||||
|
author: lemonchann
|
||||||
|
---
|
||||||
|
|
||||||
|
今天给项目写了个脚本需要获取前一天的时间,本来先获取今天的然后减一下,如果是1号的话还要考虑大小月份挺复杂的,于是去查了一下手册`date`命令原生支持,喜出望外,今天就详细说说这个看起来不起眼的`date`命令。
|
||||||
|
|
||||||
|
使用Linux的同学应该对linux的`date`命令不会陌生,经常需要在命令行敲一下这个命令获取当前时间。然而这只是他的能力冰山一角。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[llchan@localhost ~]$ date
|
||||||
|
2020年 02月 12日 星期三 19:51:46 CST
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 常规操作
|
||||||
|
|
||||||
|
#### 获取时间戳,1970年1月1日0点0分0秒到现在历经的秒数
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[llchan@localhost ~]$ date +%s
|
||||||
|
1581508426
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 时间戳还原,把刚才的秒数还原成时间字符串
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[llchan@localhost ~]$ date -d "@1581508426"
|
||||||
|
2020年 02月 12日 星期三 19:53:46 CST
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 指定的时间字符串转换成时间戳
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[llchan@localhost ~]$ date -d '02/22/2222 07:21:22' +%s
|
||||||
|
7956832882
|
||||||
|
#或者
|
||||||
|
[llchan@localhost ~]$ date -d '2222-02-22 07:21:22' +"%s"
|
||||||
|
7956832882
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 格式化输出时间格式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[llchan@localhost ~]$ date "+%Y-%m-%d"
|
||||||
|
2020-02-12
|
||||||
|
[llchan@localhost ~]$ date "+%H:%M:%S"
|
||||||
|
20:01:53
|
||||||
|
[llchan@localhost ~]$ date "+%Y-%m-%d %H:%M:%S"
|
||||||
|
2020-02-12 20:02:06
|
||||||
|
```
|
||||||
|
|
||||||
|
具体的格式参考man手册:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
格式 FORMAT 控制着输出格式. 仅当选项指定为全球时间时本格式才有效。 分别解释如下:
|
||||||
|
|
||||||
|
%% 文本的 %
|
||||||
|
|
||||||
|
%a 当前区域的星期几的简写 (Sun..Sat)
|
||||||
|
|
||||||
|
%A 当前区域的星期几的全称 (不同长度) (Sunday..Saturday)
|
||||||
|
|
||||||
|
%b 当前区域的月份的简写 (Jan..Dec)
|
||||||
|
|
||||||
|
%B 当前区域的月份的全称(变长) (January..December)
|
||||||
|
|
||||||
|
%c 当前区域的日期和时间 (Sat Nov 04 12:02:33 EST 1989)
|
||||||
|
|
||||||
|
%d (月份中的)几号(用两位表示) (01..31)
|
||||||
|
|
||||||
|
%D 日期(按照 月/日期/年 格式显示) (mm/dd/yy)
|
||||||
|
|
||||||
|
%e (月份中的)几号(去零表示) ( 1..31)
|
||||||
|
|
||||||
|
%h 同 %b
|
||||||
|
|
||||||
|
%H 小时(按 24 小时制显示,用两位表示) (00..23)
|
||||||
|
|
||||||
|
%I 小时(按 12 小时制显示,用两位表示) (01..12)
|
||||||
|
|
||||||
|
%j (一年中的)第几天(用三位表示) (001..366)
|
||||||
|
|
||||||
|
%k 小时(按 24 小时制显示,去零显示) ( 0..23)
|
||||||
|
|
||||||
|
%l 小时(按 12 小时制显示,去零表示) ( 1..12)
|
||||||
|
|
||||||
|
%m 月份(用两位表示) (01..12)
|
||||||
|
|
||||||
|
%M 分钟数(用两位表示) (00..59)
|
||||||
|
|
||||||
|
%n 换行
|
||||||
|
|
||||||
|
%p 当前时间是上午 AM 还是下午 PM
|
||||||
|
|
||||||
|
%r 时间,按 12 小时制显示 (hh:mm:ss [A/P]M)
|
||||||
|
|
||||||
|
%s 从 1970年1月1日0点0分0秒到现在历经的秒数 (GNU扩充)
|
||||||
|
|
||||||
|
%S 秒数(用两位表示)(00..60)
|
||||||
|
|
||||||
|
%t 水平方向的 tab 制表符
|
||||||
|
|
||||||
|
%T 时间,按 24 小时制显示(hh:mm:ss)
|
||||||
|
|
||||||
|
%U (一年中的)第几个星期,以星期天作为一周的开始(用两位表示) (00..53)
|
||||||
|
|
||||||
|
%V (一年中的)第几个星期,以星期一作为一周的开始(用两位表示) (01..52)
|
||||||
|
|
||||||
|
%w 用数字表示星期几 (0..6); 0 代表星期天
|
||||||
|
|
||||||
|
%W (一年中的)第几个星期,以星期一作为一周的开始(用两位表示) (00..53)
|
||||||
|
|
||||||
|
%x 按照 (mm/dd/yy) 格式显示当前日期
|
||||||
|
|
||||||
|
%X 按照 (%H:%M:%S) 格式显示当前时间
|
||||||
|
|
||||||
|
%y 年的后两位数字 (00..99)
|
||||||
|
|
||||||
|
%Y 年(用 4 位表示) (1970...)
|
||||||
|
|
||||||
|
%z 按照 RFC-822 中指定的数字时区显示(如, -0500) (为非标准扩充)
|
||||||
|
|
||||||
|
%Z 时区(例如, EDT (美国东部时区)), 如果不能决定是哪个时区则为空
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 下面就是比较骚的操作,我今天用到了。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 获取相对当前时间的明天的时间
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[llchan@localhost ~]$ date -d next-day
|
||||||
|
2020年 02月 13日 星期四 20:08:35 CST
|
||||||
|
|
||||||
|
#你可以指定输出格式,比如
|
||||||
|
[llchan@localhost ~]$ date -d next-day +%Y%m%d
|
||||||
|
20200213
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 获取相对于当前时间的昨天的时间
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[llchan@localhost ~]$ date -d last-day
|
||||||
|
2020年 02月 11日 星期二 20:11:35 CST
|
||||||
|
|
||||||
|
#你也可以指定输出格式,比如
|
||||||
|
[llchan@localhost ~]$ date -d last-day +%Y%m%d
|
||||||
|
20200211
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 获取相对当前时间的上个月的时间
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[llchan@localhost ~]$ date -d last-month
|
||||||
|
2020年 01月 12日 星期日 20:13:20 CST
|
||||||
|
|
||||||
|
#同样的你也可以指定输出格式,比如
|
||||||
|
[llchan@localhost ~]$ date -d last-month +%Y-%m-%d
|
||||||
|
2020-01-12
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 获取相对当前时间的下个月的时间
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[llchan@localhost ~]$ date -d next-month
|
||||||
|
2020年 03月 12日 星期四 20:15:44 CST
|
||||||
|
|
||||||
|
[llchan@localhost ~]$ date -d next-month "+%Y-%m-%d %H:%M:%S"
|
||||||
|
2020-03-12 20:15:38
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 获取相对当前时间的明年的时间
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[llchan@localhost ~]$ date -d next-year
|
||||||
|
2021年 02月 12日 星期五 20:17:21 CST
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 获取相对当前时间的上一年的时间
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[llchan@localhost ~]$ date -d last-year
|
||||||
|
2019年 02月 12日 星期二 20:17:29 CST
|
||||||
|
```
|
||||||
|
|
||||||
68
_posts/2020-2-21-sourcegraph.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "推荐一款github代码在线浏览神器sourcegraph"
|
||||||
|
date: 2020-1-27
|
||||||
|
tags: [后台开发]
|
||||||
|
comments: true
|
||||||
|
author: lemonchann
|
||||||
|
---
|
||||||
|
|
||||||
|
程序员逛github已经是每日必须项目,看到感兴趣的项目都会点进去看一下,github全球最大的同性交友平台,这里有海量的开源代码库,作为开源代码管理平台github是非常专业的。
|
||||||
|
|
||||||
|
但是,你要在上面看代码就不是那么舒服了,特别是点进去每个文件夹浏览文件非常的不方便,大工程文件之间的切换有时候网页加载特别慢非常不方便。
|
||||||
|
|
||||||
|
推荐这款我用的这款Google浏览器插件,安装之后让在线浏览github项目源码,查找引用和定义如同在IDE看代码一样,体验如丝滑般舒爽。
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
进入[Google应用商店](https://chrome.google.com/webstore/category/extensions?utm_source=chrome-ntp-icon) 搜索sourcegraph下载安装插件,如下图:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
点击,**添加至Chrome**,即可在项目中使用。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
打开github上任意一个项目,点击项目上方的Sourcegraph图标,即可进入代码浏览界面。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
代码浏览界面的左侧是代码目录结构,就跟一般的IDE工程视图一样,你可以很轻松的在各个文件夹中查看文件,不用像在github那样来回前进后退,望着网页加载进度发呆。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
鼠标单击相应的函数,出现的选项框可以选择跳转到定义
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
也可选择查找所有引用
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 离线安装
|
||||||
|
|
||||||
|
鉴于有些同学由于众所周知的原因,不方便去Google应用商店下载,这里再说说离线安装的方法
|
||||||
|
|
||||||
|
- 进入Chrome插件中心,浏览器输入 [chrome://extensions/](chrome://extensions/)
|
||||||
|
- 打开开发者模式开关
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- 下载我提供的插件安装包 `20.2.5.1810_0.rar` ,安装包在公众号【柠檬的编程学堂】回复【插件】获取,解压放到插件文件夹路径,比如我的路径:
|
||||||
|
|
||||||
|
`C:\Users\替换成你的电脑用户名\AppData\Local\Google\Chrome\User Data\Default\Extensions`
|
||||||
|
|
||||||
|
- 打开浏览器插件中心,打开 **开发者模式**,选择 **加载已解压的扩展程序**,即可完成安装。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
以上,这款好用的插件分享给大家,愉快的在github玩耍吧!
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
160
_posts/2020-3-3-job_analyzes.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "我分析几个一线城市的近千份岗位招聘需求,得出应该这么准备找工作"
|
||||||
|
date: 2020-1-27
|
||||||
|
tags: [后台开发]
|
||||||
|
comments: true
|
||||||
|
author: lemonchann
|
||||||
|
---
|
||||||
|
|
||||||
|
每年的三四月份是招聘高峰,也常被大家称为金三银四黄金求职期,这时候上一年的总结做完了,奖金拿到了,职场人开始谋划着年初的找工作大戏。
|
||||||
|
|
||||||
|
作为IT人要发挥自己的专业特长,如何让伯乐和千里马更快相遇?我利用大数据分析了北京、广州、深圳三个一线城市的C++招聘岗位信息,篇幅限制文中只拿出北京和深圳的数据展示,让我们来看看岗位的招聘现状,以及如何科学提高应聘成功率。
|
||||||
|
|
||||||
|
文末可以获取本次分析的高清图表,需要的同学自取。同时分享完整源码用于学习交流,若对其他岗位感兴趣也可以自行运行源码分析。
|
||||||
|
|
||||||
|
### 需求分析
|
||||||
|
|
||||||
|
通过大数据分析招聘网站发布的招聘数据,得出岗位分布区域、薪资水平、岗位关键技能需求、匹配的人才具有哪些特点、学历要求。从而帮助应聘者提高自身能力,补齐短板,有的放矢的应对校招社招,达成终极目标获得心仪的offer。
|
||||||
|
|
||||||
|
### 软件设计
|
||||||
|
|
||||||
|
数据分析是Python的强项,项目用Python实现。软件分为两大模块:数据获取 和 数据分析
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 详细实现
|
||||||
|
|
||||||
|
#### 数据获取
|
||||||
|
|
||||||
|
request库构造请求获取数据
|
||||||
|
|
||||||
|
```py
|
||||||
|
cookie = s.cookies
|
||||||
|
req = requests.post(self.baseurl, headers=self.header, data={'first': True, 'pn': i, 'kd':self.keyword}, params={'px': 'default', 'city': self.city, 'needAddtionalResult': 'false'}, cookies=cookie, timeout=3)
|
||||||
|
text = req.json()
|
||||||
|
```
|
||||||
|
|
||||||
|
数据csv格式存储
|
||||||
|
|
||||||
|
```py
|
||||||
|
with open(os.path.join(self.path, '招聘_关键词_{}_城市_{}.csv'.format(self.keyword, self.city)), 'w',newline='', encoding='utf-8-sig') as f:
|
||||||
|
f_csv = csv.DictWriter(f, self.csv_header)
|
||||||
|
f_csv.writeheader()
|
||||||
|
f_csv.writerows(data_list)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 数据分析
|
||||||
|
|
||||||
|
字段预处理
|
||||||
|
|
||||||
|
```py
|
||||||
|
df_all.rename({'职位名称': 'position'}, axis=1, inplace=True) #axis=1代表index; axis=0代表column
|
||||||
|
df_all.rename({'详细链接': 'url'}, axis=1, inplace=True)
|
||||||
|
df_all.rename({'工作地点': 'region'}, axis=1, inplace=True)
|
||||||
|
df_all.rename({'薪资': 'salary'}, axis=1, inplace=True)
|
||||||
|
df_all.rename({'公司名称': 'company'}, axis=1, inplace=True)
|
||||||
|
df_all.rename({'经验要求': 'experience'}, axis=1, inplace=True)
|
||||||
|
df_all.rename({'学历': 'edu'}, axis=1, inplace=True)
|
||||||
|
df_all.rename({'福利': 'welfare'}, axis=1, inplace=True)
|
||||||
|
df_all.rename({'职位信息': 'detail'}, axis=1, inplace=True)
|
||||||
|
df_all.drop_duplicates(inplace=True)
|
||||||
|
df_all.index = range(df_all.shape[0])
|
||||||
|
```
|
||||||
|
|
||||||
|
数据图表展示
|
||||||
|
|
||||||
|
```py
|
||||||
|
from pyecharts.charts import Bar
|
||||||
|
regBar = Bar(init_opts=opts.InitOpts(width='1350px', height='750px'))
|
||||||
|
regBar.add_xaxis(region.index.tolist())
|
||||||
|
regBar.add_yaxis("区域", region.values.tolist())
|
||||||
|
regBar.set_global_opts(title_opts=opts.TitleOpts(title="工作区域分布"),
|
||||||
|
toolbox_opts=opts.ToolboxOpts(),
|
||||||
|
visualmap_opts=opts.VisualMapOpts())
|
||||||
|
|
||||||
|
from pyecharts.commons.utils import JsCode
|
||||||
|
shBar = Bar(init_opts=opts.InitOpts(width='1350px', height='750px'))
|
||||||
|
shBar.add_xaxis(sala_high.index.tolist())
|
||||||
|
shBar.add_yaxis("区域", sala_high.values.tolist())
|
||||||
|
shBar.set_series_opts(itemstyle_opts={
|
||||||
|
"normal": {
|
||||||
|
"color": JsCode("""new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
|
||||||
|
offset: 0,
|
||||||
|
color: 'rgba(0, 244, 255, 1)'
|
||||||
|
}, {
|
||||||
|
offset: 1,
|
||||||
|
color: 'rgba(0, 77, 167, 1)'
|
||||||
|
}], false)"""),
|
||||||
|
"barBorderRadius": [30, 30, 30, 30],
|
||||||
|
"shadowColor": 'rgb(0, 160, 221)',
|
||||||
|
}})
|
||||||
|
shBar.set_global_opts(title_opts=opts.TitleOpts(title="最高薪资范围分布"), toolbox_opts=opts.ToolboxOpts())
|
||||||
|
|
||||||
|
word.add("", [*zip(key_words.words, key_words.num)],
|
||||||
|
word_size_range=[20, 200], shape='diamond')
|
||||||
|
word.set_global_opts(title_opts=opts.TitleOpts(title="岗位技能关键词云图"),
|
||||||
|
toolbox_opts=opts.ToolboxOpts())
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 数据分析
|
||||||
|
|
||||||
|
#### 区域分布
|
||||||
|
|
||||||
|
C++岗位区域分布,北京 VS 深圳
|
||||||
|

|
||||||
|
|
||||||
|
北京的C++岗位数量比深圳更多,首都buff加持,并且集中分布在海淀区和朝阳区这两个区域,中关村位于海淀区,还有位于海淀区西北旺镇的后厂村,腾讯、滴滴、百度、新浪、网易这些互联网巨头扎堆,自然能提供更多的岗位。
|
||||||
|
|
||||||
|
深圳的岗位则集中在南山区,猜测鹅厂C++大厂在南山区贡献了重大份额,第二竟然在宝安区。
|
||||||
|
|
||||||
|
#### 学历分布
|
||||||
|
|
||||||
|
C++岗位学历分布,北京 VS 深圳
|
||||||
|

|
||||||
|
|
||||||
|
学历上两个城市的本科学历占比都是85%以上,北京岗位需求研究生占比和大专相当。可见大部分岗位本科学历即可胜任,或许能给即将毕业纠结考不考研的你一些参考。
|
||||||
|
|
||||||
|
如果你的学历是专科,那么需要加倍的努力,因为留给你的职位并不是很多。同时,从图表数据来看,深圳的岗位对大专生需求10%而对硕士仅占2%,或许专科生去深圳比去北京更加友好,emmm...仅供参考。
|
||||||
|
|
||||||
|
#### 薪资分布
|
||||||
|
|
||||||
|
C++岗位薪资分布,薪资单位K。
|
||||||
|
|
||||||
|
北京最高薪资 VS 最低薪资
|
||||||
|

|
||||||
|
|
||||||
|
深圳最高薪资 VS 最低薪资
|
||||||
|

|
||||||
|
|
||||||
|
薪资对比没啥好说的,大家看图说话,只想说帝都果然财大气粗。
|
||||||
|
|
||||||
|
#### 技能储备
|
||||||
|
|
||||||
|
C++岗位关键技能词云,北京 VS 深圳
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
首先在脱离开发走上管理岗位之前,编程解决问题能力是最重要,可以看到「编程」能力在技能词云中占比最大。
|
||||||
|
|
||||||
|
大部分岗位要求较高的「算法、数据结构、Linux、数据库(存储)、多线程(操作系统)」这些计算机基础素养,所以不管你是在校学生准备校招或者职场老人准备跳槽,都需要储备好这些计算机基础能力,无论哪种个方向,硬实力的储备都很重要。
|
||||||
|
|
||||||
|
值得一提的是除去硬核技术要求外,岗位对候选人的软实力也有要求,比如更加偏爱具备「团队、协作、学习、沟通」这些能力的候选人,大家在提高技术能力的同时,也要注重这些软实力的培养。
|
||||||
|
|
||||||
|
一个彩蛋。Linux和window下都有C++开发岗位需求,相对而言Linux下C++开发占比更多,词云更大,如果你对这两个平台没有特殊偏爱,那么学Linux下开发大概能加大应聘成功率,毕竟岗位需求更大。
|
||||||
|
|
||||||
|
关注公众号「柠檬的编程学堂」回复 「分析」获取本文程序完整源码以及高清分析图表。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
6
_posts/todolist.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
进程 线程 协程的对比python vs go
|
||||||
|
github c++项目推荐
|
||||||
|
|
||||||
|
谈谈序列化protobuf
|
||||||
|
|
||||||
|
说说RPC
|
||||||
BIN
images/2020-1-29-red_packet_thinking_lock/题图.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
images/2020-1-4-learn_cpp/资料截图.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
images/2020-2-21-sourcegraph/加载扩展程序.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
images/2020-2-21-sourcegraph/启动插件.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
images/2020-2-21-sourcegraph/工程文件浏览.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
images/2020-2-21-sourcegraph/应用商店.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
images/2020-2-21-sourcegraph/插件中心.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
images/2020-2-21-sourcegraph/查找定义.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
images/2020-2-21-sourcegraph/查找引用.png
Normal file
|
After Width: | Height: | Size: 76 KiB |