liuhao163.github.io

杂七杂八


  • Home

  • Categories

  • Tags

  • Archives

  • Sitemap

Untitled

Posted on 2022-09-21

MyBatisy源码-概述

Posted on 2022-06-15 | Edited on 2022-09-21 | In java , mybatis

背景

mybatis在国内是是java-web开发里非常重要的框架,通过xml配置的mappr对数据库进行了orm映射,降低程序对数据库访问的开发难度。

希望通过阅读mybatis源码可以了解orm框架都有哪些角色,每个角色的边界是什么

角色

配置

作用:加载配置文件活着注解。

关键类:Configuration:该对象加载了配置里的数据源、mappr的映射即“MappedStatement”,并且缓存到内存中

会话

数据绑定

作用:

关键类:MappedStatement

执行器

作用:

关键类:Executor

处理器

作用:

关键类:
  1. StatementHandler
  2. ParamenterHandler
  3. ResultSetHandler

状态机

Posted on 2022-05-22 | Edited on 2022-09-21 | In 数据结构与算法 , 算法

最近刷题lc8写一个string–>int的函数,结题思路是用限自动机的方案

  1. 穷举有几种数字状态“ ”,”+/-“,”0-9”,”其他字符”,对应的 start、signin_number、end
  2. 穷举每种状态之间转化关系
  3. 遇到每种字符后,处理每种状态的逻辑

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class Solution {

public static void main(String[] args) {
System.out.println(Integer.MAX_VALUE);
System.out.println(Integer.MIN_VALUE);
System.out.println(new Solution().myAtoi("123213213213213211234"));
}

public int myAtoi(String s) {
Automation automation=new Automation();
return automation.cal(s);
}

private class Automation {
private final Map<String, String[]> map = new HashMap<>();
private String curState = "start";
private long res = 0;
private int sign = 1;


public Automation() {
initStatus();
}

public int cal(String s) {
for (char c : s.toCharArray()) {
parseChar(c);
}
return sign * (int) res;
}

//穷举当前状态和下一个状态的对应关系
private void initStatus() {
//' '、+/-、0-9、other"
map.put("start", new String[]{"start", "sign", "innumber", "end"});
map.put("sign", new String[]{"end", "end", "innumber", "end"});
map.put("innumber", new String[]{"end", "end", "innumber", "end"});
map.put("end", new String[]{"end", "end", "end", "end"});
}

//处理每种对应关系
public void parseChar(char c) {
String state = map.get(curState)[getNextState(c)];
if (state.equals("innumber")) {
res = res * 10 + (c - '0');
res = sign == 1 ? Math.min(res, (long) Integer.MAX_VALUE) : Math.min(res, -(long) Integer.MIN_VALUE);
} else if (state.equals("sign")) {
sign = c == '-' ? -1 : 1;
}
curState = state;
}

private int getNextState(char c) {
if (c == ' ') {
return 0;
} else if (c == '+' || c == '-') {
return 1;
} else if (Character.isDigit(c)) {
return 2;
} else {
return 3;
}
}
}
}

应用场景

当我们的系统复杂度到一定程度的时候,对象的状态分支会很多,我们可以选择用if,else来解决,但是最后往往会写出一堆几乎无法维护的屎山。那么我们可以将对象抽象成一个个状态,特殊情况实际上就是状态之间的转换。

比如上题,就是有限的状态之间的转换。
avator

ebpf学习-初探ebpf

Posted on 2022-03-11 | Edited on 2022-09-21 | In 经验积累 , 工具

ebpf是什么

Linux内核一直是实现监视/可观察性,网络和安全性的理想场所。不幸的是,这通常是不切实际的,因为它需要更改内核源代码或加载内核模块,并导致彼此堆叠的抽象层。 eBPF是一项革命性的技术,可以在Linux内核中运行沙盒程序,而无需更改内核源代码或加载内核模块。通过使Linux内核可编程,基础架构软件可以利用现有的层,从而使它们更加智能和功能丰富,而无需继续为系统增加额外的复杂性层。

eBPF导致了网络,安全性,应用程序配置/跟踪和性能故障排除等领域的新一代工具的开发,这些工具不再依赖现有的内核功能,而是在不影响执行效率或安全性的情况下主动重新编程运行时行为。

如果直接解释eBPF,有点不明所以。那我们就看看有哪些基于eBPF的工程,这些工程或许你已经知道,或是已经经常使用,也许你会明白eBPF距离我们并不遥远。

基于eBPF的项目

  1. bcc
    BCC是用于创建基于eBPF的高效内核跟踪和操作程序的工具包,其中包括一些有用的命令行工具和示例。 BCC简化了用C进行内核检测的eBPF程序的编写,包括LLVM的包装器以及Python和Lua的前端。它还提供了用于直接集成到应用程序中的高级库。

  2. bpftrace
    bpftrace是Linux eBPF的高级跟踪语言。它的语言受awk和C以及DTrace和SystemTap等以前的跟踪程序的启发。 bpftrace使用LLVM作为后端将脚本编译为eBPF字节码,并利用BCC作为与Linux eBPF子系统以及现有Linux跟踪功能和连接点进行交互的库。

  3. Cilium
    Cilium是一个开源项目,提供基于eBPF的联网,安全性和可观察性。它是从头开始专门设计的,旨在将eBPF的优势带入Kubernetes的世界,并满足容器工作负载的新可伸缩性,安全性和可见性要求。

  4. Falco
    Falco是一种行为活动监视器,旨在检测应用程序中的异常活动。 Falco在eBPF的帮助下审核Linux内核层的系统。它使用其他输入流(例如容器运行时度量标准和Kubernetes度量标准)丰富了收集的数据,并允许连续监视和检测容器,应用程序,主机和网络活动。

  5. Katran
    Katran是一个C ++库和eBPF程序,用于构建高性能的第4层负载平衡转发平面。 Katran利用Linux内核中的XDP基础结构来提供用于快速数据包处理的内核功能。它的性能与NIC接收队列的数量成线性比例,并且使用RSS友好的封装转发到L7负载平衡器。

  6. Sysdig

    Sysdig是提供深层系统可见性的简单工具,并具有对容器的原生支持。
    其他基于eBPF技术的项目还有很多,比如kubectl-trace ,ply 等,这里不再赘述。

一个简单的DEMO

下面我们写一个简单的demo来展示下开发一个ebpf程序需要哪些步骤,这里我们采用Cilium/ebpf库进行开发,因为ebpf的用户态代码可以用golang开发。:P

程序是通过ebpf的tracepoint监听syscalls/sys_enter_execve,在执行该方法时候获取pid和commd。通过ebpf的map将pid和commd传到用户态。

step.1 环境搭建

系统配置: Ubuntu 20.04.4
系统依赖: llvm,clang-10.0.0-4ubuntu1,还有linux的kernel这里源码在路径(/kernel-src),golang环境

step.2 编写ebpf的kernel侧的程序-c代码

  1. 编写代码并且用下面命令将代码变成bpf_program.o
    clang -O2 -target bpf -c bpf_program.c -I/kernel-src/tools/testing/selftests/bpf -o bpf_program.o
  2. 将bpf_program.o文件copy到golang程序下

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <linux/bpf.h>
#include <bpf_helpers.h>
#include <stdio.h>
#include <linux/types.h>

#define SEC(NAME) __attribute__((section(NAME), used))

//定义c的结构,将pid和comm封装到该结构体,将该结构体通过tracker_map传递给用户态
struct event_data_t {
__u32 pid;
char comm[20];
};

//定义ebpf的map
struct bpf_map_def SEC("maps") tracker_map = {
.type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(int),
.value_size = sizeof(struct event_data_t),
.max_entries = 2048,
};

//ebpf程序挂载点,
SEC("tracepoint/syscalls/sys_enter_execve")
int bpf_prog(void *ctx) {
int index = 0;

struct event_data_t evt = {};


__u64 id = bpf_get_current_pid_tgid();
evt.pid = id >> 32;

bpf_get_current_comm(&evt.comm, sizeof(evt.comm));
bpf_map_update_elem(&tracker_map, &index, &evt, BPF_ANY);

return 0;
}

//协议
char _license[] SEC("license") = "GPL";

step3用户态侧代码-golang的代码

  • 编写go代码
  • 执行go build ./
  • 执行go程序,这里是go-tools
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
var mapKey uint32 = 0

// ebpf标签的说明
// BpfProg对应的是c程序中的bSEC(tracepoinst/...)的方法名
// TrackerMap对应的c程序中的SEC(map)
type bpfPrograms struct {
BpfProg *ebpf.Program `ebpf:"bpf_prog"`
TrackerMap *ebpf.Map `ebpf:"tracker_map"`
}

func (p *bpfPrograms) Close() error {
err := p.BpfProg.Close()
err = p.TrackerMap.Close()
return err
}

// 字段类型,对应c程序中的event_data_t类型,注意Comm是uint8
type EventData struct {
Pid uint32
Comm [20]uint8
}

func (e EventData) CommHex() string {
ba := []byte{}
for _, b := range e.Comm {
ba = append(ba, b)
}
return string(ba)
}

//程序
func LoadTestEbpf() {

//
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal(err)
}

//加载c编译出来的.o文件的相对路径,这个地址一定不能出错
spec, err := ebpf.LoadCollectionSpec("ebpf/bpf_program.o")
if err != nil {
panic(err)
}

//赋值obj
obj := bpfPrograms{}
if err := spec.LoadAndAssign(&obj, nil); err != nil {
panic(err)
}
defer obj.Close()

tp, err := link.Tracepoint("syscalls", "sys_enter_execve", obj.BpfProg)
if err != nil {
panic(err)
}
defer tp.Close()

// Read loop reporting the total amount of times the kernel
// function was entered, once per second.
ticker := time.NewTicker(1 * time.Second)
log.Println("Waiting for events..")
for range ticker.C {

var value []byte
//c的结构体序列化成bytes(value)
if err := obj.TrackerMap.Lookup(&mapKey, &value); err != nil {
log.Printf("reading map: %v", err)
} else {
var event EventData
//c的结构体序列化成golang的喜爱那个并且打印出来
if err := binary.Read(bytes.NewBuffer(value), binary.LittleEndian, &event); err != nil {
log.Printf("parsing perf event: %s", err)
continue
}
log.Printf("pid:%v,comm:%v", event.Pid, event.CommHex())
}
}
}

程序入口

1
2
3
func main() {
ebpf.LoadTestEbpf()
}

step3,运行查看效果

  • 在终端1上运行:./go-tools
  • 在中断2上输入任何指令
  • 查看终端1的输出
1
2
2022/03/11 17:01:26 reading map: lookup: key does not exist
2022/03/11 17:01:30 pid:60184,comm:sshd

用antlr开发一个简单的规则引擎

Posted on 2022-03-11 | Edited on 2022-09-21

golang源码学习-sync.Map

Posted on 2021-12-15 | Edited on 2022-09-21 | In golang , 源码学习

sync.Map

位于sync包中,主要解决map的线程安全的问题,适用于读多写少的场景。

sync.Map的原理

内部持有俩个map,一个是read,类型是atomic.Value实际类型是【map[interface{}]entry】,一个是dirty类型是map【[interface{}]entry】。

其中read主要解决无数据竞争的情况下数据的快速访问,它通过cas进行快速的读写操作;一旦出现数据的竞争,就会用到dirty,dirty里面保存read里所有非nil的值【通过状态来表示unexpunged】,当出现竞争后数据会写到dirty而不是read中。

数据的访问路径大致是:

  • 读,先从read里找,read里没有,去dirty里找,如果miss过一定的阈值【dirty的长度】时候,将dirty和read交换,交换后dirty置为nil;
  • 写,先判断key是否存在,如果存在且不为expunaged,先通过cas写快速返回,否则有如下分支:

    • 如果key是expunaged,说明key之前被删除了,但是dirty没有,unexpunaged之后同步修改read和dirty
    • 如果key不存在于read,但是存在于dirty修改dirty
    • 如果key不存在与read和dirty,初始化dirty【如果需要】,数据写入dirty

    因为数据在并发写的时候一旦发生竞争还是会用到锁,并发写的时候的锁是不可避免的。所以sync.Map适用于读多写少数据冲突不那么复杂的场景

sync.Map的源码分析

结构体分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
type Map struct {
mu Mutex //锁 用来保护read、dirty的并发控制

// read包含部分map的内容,他是atomic.Value类型的用来解决并发访问的安全的问题,在读的场景不需要加锁,而在写的场景需要mu的控制
read atomic.Value // readOnly

// dirty访问要在mu的保护下进行,他包含所有的readOnly里non-expunged的数据
// 在read中标记为Expunged的entries不会被存储在dirt中,如果一个在read中存在的key被标记为expunged,他需要先unexpunged在保存在dirty中
// 如果dirty是nil,当下一次修改map的时候需要初始化dirty,初始化的方式是将read的不为exunped的value都copy到dirty中
dirty map[interface{}]*entry

// 计数器,当从read读取数据时候miss回增加该值,打到一定的阈值,即miss>=len(dirty)的时候回触发dirty和read的互换
misses int
}


// read中的值
type readOnly struct {
m map[interface{}]*entry
amended bool // 一个标记为,默认是false,true说明readOnly的m和dirty已经同步过了,即dirty不为nil了,在Store操作中,dirtyLocked函数调用后会置为true
}


type entry struct {
// 封装了指向value值的指针
//
// If p == nil, the entry has been deleted and m.dirty == nil.
//
// If p == expunged, the entry has been deleted, m.dirty != nil, and the entry
// is missing from m.dirty.
//
// Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty
// != nil, in m.dirty[key].
//
// An entry can be deleted by atomic replacement with nil: when m.dirty is
// next created, it will atomically replace nil with expunged and leave
// m.dirty[key] unset.
//
// An entry's associated value can be updated by atomic replacement, provided
// p != expunged. If p == expunged, an entry's associated value can be updated
// only after first setting m.dirty[key] = e so that lookups using the dirty
// map find the entry.
p unsafe.Pointer // *interface{}
}

//标记位当entry.p=expunged说明,该对象在read中被删除,标记了expunged的key不会出现在dirty中。
var expunged = unsafe.Pointer(new(interface{}))

Store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
func (m *Map) Store(key, value interface{}) {
//从read中获取readOnly,如果key存在,通过cas修改不为expunged的值,成功后快速返回
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}

//修改失败通过锁进行鬓发保护
m.mu.Lock()
read, _ = m.read.Load().(readOnly)//双重检查
if e, ok := read.m[key]; ok {
//key存在于read但是状态是expunged,cas修改成nil,同步dirty和readOnly的值,
if e.unexpungeLocked() {
//走到这里说明dirty已经被初始化了,见m.dirtyLocked()里e.tryExpungeLocked()这一步
m.dirty[key] = e
}
//cas保存value
e.storeLocked(&value)
} else if e, ok := m.dirty[key]; ok {
//key存在于dirty
e.storeLocked(&value)
} else {
//!read.amended说明dirty为空初始化dirty,将read中不为expunged值写入dirty,见下面
if !read.amended {
m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true})
}
//修改dirty的值
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
}

func (m *Map) dirtyLocked() {
if m.dirty != nil {
return
}

read, _ := m.read.Load().(readOnly)
m.dirty = make(map[interface{}]*entry, len(read.m))
for k, e := range read.m {
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}

func (e *entry) tryExpungeLocked() (isExpunged bool) {
p := atomic.LoadPointer(&e.p)
for p == nil {
if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
return true
}
p = atomic.LoadPointer(&e.p)
}
return p == expunged
}

Load

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
read, _ := m.read.Load().(readOnly)
//优先读取read
e, ok := read.m[key]
if !ok && read.amended {
//key不存在但是dirty已经初始化,这里需要锁来保护并发冲突
m.mu.Lock()
read, _ = m.read.Load().(readOnly)//双重检查
e, ok = read.m[key]
if !ok && read.amended {
//从dirty里取
e, ok = m.dirty[key]
//超过阈值交换dirty和read,并且dirty置为nil,见下面
m.missLocked()
}
m.mu.Unlock()
}
if !ok {
return nil, false
}
//加载value
return e.load()
}

func (e *entry) load() (value interface{}, ok bool) {
p := atomic.LoadPointer(&e.p)
if p == nil || p == expunged {
return nil, false
}
return *(*interface{})(p), true
}

func (m *Map) missLocked() {
m.misses++
if m.misses < len(m.dirty) {
return
}
m.read.Store(readOnly{m: m.dirty})
m.dirty = nil
m.misses = 0
}

Delete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func (m *Map) Delete(key interface{}) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
delete(m.dirty, key)
}
m.mu.Unlock()
}
if ok {
e.delete()
}
}

func (e *entry) delete() (hadValue bool) {
for {
p := atomic.LoadPointer(&e.p)
if p == nil || p == expunged {
return false
}
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return true
}
}
}
`

ThreadLocal如何在子线程传递

Posted on 2021-11-01 | Edited on 2022-09-21 | In 经验积累 , 工具

主线程如何向自己创建的子线程传递ThreadLocal值,只需要创建InheritableThreadLocal值,实现原理如下:

InheritableThreadLocal覆写了它三个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class InheritableThreadLocal<T> extends ThreadLocal<T> {

//创建线程时候,会调用该方法,见下面init
protected T childValue(T parentValue) {
return parentValue;
}

//获取Map时候,返回的是t.inheritableThreadLocals,见ThreadLocal操作值
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}

//createMap时候,返回的是t.inheritableThreadLocals,见ThreadLocal操作值
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}

见子线程初始化过程,初始化时候如果当前线程inheritableThreadLocals不为空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
//......
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
//这里初始的是this.inheritableThreadLocals
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
//......
}

// ThreadLocal.createInheritedMap会调用ThreadLocalMap(ThreadLocalMap parentMap)方法,
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];

for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
//这里调用InheritableThreadLocal覆写的childValue,返回的是parent的value,将parent的值copy到子线程中
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}

那么inheritableThreadLocals它什么时候不为空呢,见下面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//在set值时候判断ThreadLocalMap为空,调用InheritableThreadLocal.getMap方法返回当前线程的t.inheritableThreadLocals,否则初始化t.inheritableThreadLocals
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

//获取的是t.inheritableThreadLocals的值
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

//删除值实际上删除的t.inheritableThreadLocals
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

坑-下载文件,丢失文件格式和后缀

Posted on 2021-10-11 | Edited on 2022-09-21 | In 经验积累 , 工具

原因

将记录导出成excel供下载,生成xlsl以后下载文件会丢失文件格式,curl链接情况如下

1
2
3
4
5
6

bogon:~ liuhaoeric$ curl -vvv https://xxxxxx.xlsx -output xxxxx.xlsx
.....
< Content-Disposition: inline; filename="xxxxx" //上传时候需制定filename这就是下载的文件名,如果filename未制定文件后缀名下载就会丢失文件格式

......

解决方案

上传时候指定filename时候xxxxx.xlsx

在curl发现代码已经变为如下所示,问题解决

1
< Content-Disposition: inline; filename="xxxxx.xlsx"

SSL的请求过程

Posted on 2021-08-13 | Edited on 2022-09-21 | In 经验积累 , 工具

名词解释

公钥即证书:由服务端传递给客户端,用于客户端的CA证书校验合法性;非对称加密clientKey;
CA证书:内置于系统中用于校验公钥的合法性
私钥:服务端保存,用于非对称解密clientKey
clientKey:公钥验证通过后由浏览器生成clientKey通过对称加密和服务端进行交互

发送请求的过程

  1. 浏览器发送请求给服务端,这时候端口是443;
  2. 服务端返回公钥【证书】给客户端;
  3. 浏览器通过系统内置的CA证书验证证书的合法性,如果不合法请求终止;
  4. 浏览器生成clientKey明文,并且用公钥加密clientKey,发送密文的clientKey给服务端;
  5. 服务端得到加密过的公钥,用私钥解密,获取明文clientKey;
  6. 浏览器用clientKey通过对称加密方式通信:
    1. 浏览器通过clientKey加密请求,发送给服务端;
    2. 服务端通过clientKey解密请求,并且将响应用clientKey加密发给浏览器;
    3. 浏览器接到密文响应,用clientKey解密;

图解

avator

如何保证缓存和数据库的数据一致

Posted on 2021-06-25 | Edited on 2022-09-21 | In 经验积累 , 项目积累

本篇文章主要讨论常见的集中缓存的使用方案,以及如何解决数据和缓存的一致性

常见的缓存模式

Cache-Aside Pattern

我们常见的缓存方案,可以说我们大部分都采用这种方案,见下面:

avator

Read-Through/Write through(读写穿透)

本质上和Cache-Aside Pattern类似,只是在程序和缓存中增加了一层Cache Provider,读写如果未命中由Cache Proxy来负责和缓存交互。减少程序开发的复杂性,需要中间件支持

Read-Through

avator

Write through

avator

Write behind

和Read-Through/Write through类似,区别是Write behind模式下,数据都是写到Cache Provider,由Cache Provider异步的刷到数据库中,听着是不是很熟悉,对Mysql的WAL异步刷盘就是这种模式

avator

如何保证缓存和数据库的数据一致

这里是以常见的Cache-Aside Pattern来讨论,正确操作缓存的姿势是

avator

这里要注意俩点

不是更新缓存而是要删除缓存

如果更新缓存不是删除缓存在并发场景下会出现脏数据里有如下:

avator

  1. 线程A先发起一个写操作,第一步先更新数据库
  2. 线程B再发起一个写操作,第二步更新了数据库
  3. 由于网络等原因,线程B先更新了缓存
  4. 线程A更新缓存。

这时DB是B操作后的数据,缓存是A操作的数据

要先操作数据库而不是先操作缓存

如果先操作缓存不是先操作数据库会在并发场景下会出现脏数据里有如下:

avator

  1. 线程A发起一个写操作,第一步del cache
  2. 此时线程B发起一个读操作,cache miss
  3. 线程B继续读DB,读出来一个老数据
  4. 然后线程B把老数据设置入cache
  5. 线程A写入DB最新的数据

这时DB是A操作后的数据,缓存是B操作后的数据。

123…23

Liu hao

励志当好厨子的程序员

229 posts
54 categories
81 tags
RSS
GitHub E-Mail
© 2018 – 2023 Liu hao
Powered by Hexo v3.9.0
|
Theme – NexT.Pisces v7.0.0