阅读本文大约需要 40 分钟。

上一篇文章 《大白话讲讲 Go 语言的 sync.Map(一)》 讲到 entry 数据结构,原因是 Go 语言标准库的 map 不是线程安全的,通过加一层抽象回避这个问题。

当一个 key 被删除的时候,比如李四销户了,以前要撕掉小账本,现在可以在大账本上写 expunged,

对,什么也不写也是 OK 的。也就是说,

entry.p 可能是真正的数据的地址,也可能是 nil,也可能是 expunged。

为什么无端端搞这个 expunged 干嘛?因为 sync.Map 实际上是有两个小账本,

一个叫 readOnly map(只读账本),一个叫 dirty map(可读、也可写账本):

type Map struct {
    mu sync.Mutex
    read atomic.Value // readOnly
    dirty map[interface{}]*entry
    misses int
}

type readOnly struct {
    m       map[interface{}]*entry
    amended bool // true if the dirty map contains some key not in m.
}

既然有账本一个变成两个,那肯定会有些时候出现两个 map 数据是不一致的情况。

readOnly 结构的 amended 字段,是一个标记,为 true 的时候代表 dirty map 包含了一些 key,这些 key 不会存在 readOnly map 中。

这个字段的作用,在于加速查找的过程。

假设 readOnly 账本上有 张三、李四、钱五,dirty 账本除了这三个人,后面又新增了 王六,查找逻辑就是这样的:

  1. 先在 readOnly 查找,王六不在
  2. 判断 amended ,发现两个账本数据是不一致的
  3. 再去 dirty 账本查找,终于找到王六

如果 2 的 amended 标记是两个账本数据一致,那就没有执行 3 的必要了。

我们可以看看源码是怎么实现的:

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
  read, _ := m.read.Load().(readOnly)
  // 1. 先在 readOnly 查找,王六不在
  e, ok := read.m[key]
  // 2. 判断 amended ,发现两个账本数据是不一致的
  if !ok && read.amended {
    // 加锁的原因是,前面步骤 1 的读取有可能被另一个协程的 missLocked 更改了
    // 导致读出来的值不符合预期,所以加锁再读取一次,老套路了。
    m.mu.Lock()
    read, _ = m.read.Load().(readOnly)
    e, ok = read.m[key]
    if !ok && read.amended {
      // 3. 再去 dirty 账本查找,终于找到王六
      e, ok = m.dirty[key]
      // missLocked 拥有一个计数器,
      // 它的作用在于 readOnly 如果一直查不到,经常退化到 dirty,
      // 那就把 dirty 作为 readOnly ,直接取代它。
      m.missLocked()
    }
    m.mu.Unlock()
  }
  if !ok {
    return nil, false
  }
  return e.load() // 还记得大账本吗?这里是拿到最终的值,针对 entry.p == expunged 做了特殊处理。
}

func (m *Map) missLocked() {
  // 查不到就递增 misses 计数器
  m.misses++
  // 这个判断条件不是常数,而是 dirty map 的记录数。
  // 这个判断条件很奇妙,
  // 它使得 dirty 取代 readOnly 的时机,和 dirty 的数据量正相关了。
  // 也就是说,dirty map 越大,对两个 map 不一致的容忍度越大,
  // 不会有频繁的取代操作。
  if m.misses < len(m.dirty) {
    // 如果不是经常查不到,说明 readOnly 还是可以用的,退出。
    return
  }
  // 如果 readOnly 已经没有存在价值,那就把 dirty 取代 readOnly。
  // 此时,dirty 置空,并把 misses 计数器置 0。
  // read 和 dirty 的数据类型都是 map[interface{}]*entry,
  // 可以直接替换,无需类型转换,这个设计简直完美。
  m.read.Store(readOnly{m: m.dirty})
  m.dirty = nil
  m.misses = 0
}

func (e *entry) load() (value interface{}, ok bool) {
  p := atomic.LoadPointer(&e.p)
  // entry.p 可能是真正的数据的地址,也可能是 nil,也可能是 expunged
  if p == nil || p == expunged {
    // nil 或者是 expunged 都是不存在的,返回空
    return nil, false
  }
  // 如果是真正的数据地址,那就返回真正的数据(就是拿到大账本的某一页纸上的内容)
  return *(*interface{})(p), true
}

到这里已经讲完数据读取这部分的代码了,接着再讲数据是怎么写入的。

上一篇文章我留了一个思考题,

为什么小账本不能做到同时修改?限于篇幅,我不会展开。

我现在解答我们有了大账本,是如何做到同时修改的!

答案在这里:

// tryStore 顾名思义,就是不断尝试的意思。
// 你可以看到有一个无条件的死循环,只有某些条件满足的时候才会退出
// 计算机术语:自旋(自己一直在旋转)
func (e *entry) tryStore(i *interface{}) bool {
  for {
    p := atomic.LoadPointer(&e.p)
    // readOnly map 存储的是 entry 结构,p 就是所谓的大账本,
    // p 指向大账本上某一页纸上的内容,
    // 当账本查不到的时候,返回查不到。
    if p == expunged {
      return false
    }
    // 当账本可以查到的时候,使用 CAS 把旧的值,替换为新的值。
    // 可以查到并替换成功,返回成功,函数退出
    // 查不到或者替换失败,自旋,重试,直到成功为止
    if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
      return true
    }
  }
}

问题来了, CAS(Compare and Swap,比较并交换)是什么东西?我们看这个加法函数:

func add(delta int32) {
  for {
    // 把原先的值取出来
    oldValue := atomic.LoadInt32(&addr)
    // 读取后,如果没有其他人对它修改(Compare)
    // 那就用 oldValue+delta 新值,替换掉原来的值(Swap)
    // 成功程序退出,失败了就自旋重试(可能被其他人改了导致 Compare 不成功)
    if atomic.CompareAndSwapInt32(&addr, oldValue, oldValue+delta) {
      return
    }
  }
}

越来越有趣了,atomic.CompareAndSwapInt32 到底是个啥子哟?

它的具体实现在 src/runtime/internal/atomic/asm_amd64.s 里(不同 CPU 架构,使用的文件不同,这里以最常见的 amd64 为例):

// bool Cas(int32 *val, int32 old, int32 new)
// Atomically:
//  if (*val == old) {
//    *val = new;
//    return 1;
//  } else
//    return 0;
TEXT runtime∕internal∕atomic·Cas(SB),NOSPLIT,$0-17
  MOVQ  ptr+0(FP), BX
  MOVL  old+8(FP), AX
  MOVL  new+12(FP), CX
  LOCK
  CMPXCHGL  CX, 0(BX)
  SETEQ  ret+16(FP)
  RET

FP(Frame pointer: arguments and locals):

函数的输入参数,格式 symbol+offset(FP),symbol 没有实际意义,只为了增强代码可读性,但没有 symbol 程序无法编译。

ptr+0(FP) 代表第一个参数,取出复制给 BX 寄存器。

由于 ptr 是一个指针,在 64 位的处理器中,一个指针的占 8 个字节,

所以第二个参数 old+8(FP),偏移量 offset 等于 8,

而第三个参数 new+12(FP),偏移量再加 4 的原因是 int32 占据 4 个字节。

LOCK 指令前缀会设置处理器的 LOCK# 信号,锁定总线,阻止其他处理器接管总线访问内存,

设置 LOCK# 信号能保证某个处理器对共享内存的独占使用。

CMPXCHGL CX, 0(BX) 是比较并交换的指令,将 AX 和 CX 比较,相同将 BX 指向的内容 放入 CX,

CMPXCHGL 暗中使用了 AX 寄存器。

兜了一大圈,终于明白大账本的数据是怎样被更新的了。

看看数据是怎么写入之前,我们要知道数据是怎么被删除的:

// 删除的逻辑是比较简单的。
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
  read, _ := m.read.Load().(readOnly)
  e, ok := read.m[key]
  // key 不存在的时候并且 readOnly map 和 dirty map 不一致时,
  // 把 dirty map 对应的记录删了。
  if !ok && read.amended {
    m.mu.Lock()
    read, _ = m.read.Load().(readOnly)
    e, ok = read.m[key]
    if !ok && read.amended {
      // 数据不一致的时候,最终读出来的值以 dirty map 为主,
      // 即使 readOnly map 是 !ok 的,但 dirty map 可能是 ok 的,
      // 既然值可能是存在的,那就读取出来。
      e, ok = m.dirty[key]
      // 删除操作
      delete(m.dirty, key)
      // 递增数据不一致的计数器。
      // 太多不一致会把 dirty map 提升为 readOnly map,前面讲过了。
      m.missLocked()
    }
    m.mu.Unlock()
  }
  // key 存在的时候,把 key 置为 nil,注意这里不是 expunged,
  // 这也是我为什么要先讲 Delete 的原因。
  if ok {
    return e.delete()
  }
  return nil, false
}

// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
  m.LoadAndDelete(key)
}

// delete 将对应的 key 置为 nil!而不是 expunged!
func (e *entry) delete() (value interface{}, ok bool) {
  for {
    p := atomic.LoadPointer(&e.p)
    if p == nil || p == expunged {
      return nil, false
    }
    if atomic.CompareAndSwapPointer(&e.p, p, nil) {
      return *(*interface{})(p), true
    }
  }
}

OK,我们看数据写入的逻辑,它是整个源码中最难理解的,隐含的逻辑关系非常多:

// unexpungeLocked 将 expunged 的标记变成 nil。
func (e *entry) unexpungeLocked() (wasExpunged bool) {
  return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}

// storeLocked 将 entry.p 指向具体的值
func (e *entry) storeLocked(i *interface{}) {
  atomic.StorePointer(&e.p, unsafe.Pointer(i))
}
// tryExpungeLocked 尝试 entry.p == nil 的 entry 标记为删除(expunged)
func (e *entry) tryExpungeLocked() (isExpunged bool) {
  p := atomic.LoadPointer(&e.p)
  // for 循环的作用,可以保证 p != nil,
  // 保证写时复制过程中,p == nil 的情况不会被写到 dirty map 中。
  for p == nil {
    if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
      return true
    }
    p = atomic.LoadPointer(&e.p)
  }
  return p == expunged
}

// dirtyLocked 写时复制,两个 map 都找不到新增的 key 的时候调用的。
func (m *Map) dirtyLocked() {
  // dirty 被置为 nil 的情景还记得吗?
  // 
  // 当 readOnly map 一直读不到,需要退化到 dirty map 读取的时候,
  // dirty map 会被提升为 readOnly map,
  // 此时,dirty map 就会被置空。
  //
  // 但是,dirtyLocked 被调用之前,
  // 都是判断 read.amended 是否为 false
  // if !read.amended {...}
  // 个人认为,可以直接判断 if m.dirty == nil {...},
  // 代码可读性更强!下面三行代码也可以不要了。
  if m.dirty != nil {
    return
  }
  // 遍历 readOnly map,把里面的内容都复制到新创建的 dirty map 中。
  read, _ := m.read.Load().(readOnly)
  m.dirty = make(map[interface{}]*entry, len(read.m))
  for k, e := range read.m {
    // tryExpungeLocked 将 entry.p == nil 设置为 expunged,
    // 遍历之后,所有的 nil 都变成 expunged 了。
    // 返回 false 说明 p 是有值的,要拷贝到 dirty 里。
    // Delete 操作会把有值的状态,转移为 nil,
    // 并不会把 expunged 状态转移为 nil,
    // 由于 for 循环的存在,p 也不会等于 nil,
    // 也就是说,tryExpungeLocked 的 p == expunged 是可以信任的。
    if !e.tryExpungeLocked() {
      // 如果没有被删除,拷贝到 dirty map 中。
      m.dirty[k] = e
    }
  }
}

func (m *Map) Store(key, value interface{}) {
  // 如果 readOnly map 有对应的 key,
  // 通过 e.tryStore 直接写入(就是上面更新大账本的整个过程),
  // 注意,tryStore 会在 entry.p == expunged 的情况下失败。
  read, _ := m.read.Load().(readOnly)
  if e, ok := read.m[key]; ok && e.tryStore(&value) {
    return
  }
  // readOnly map 找不到,或者 key 被删除了,
  // 那就写到 dirty map 里面。
  m.mu.Lock()
  read, _ = m.read.Load().(readOnly)
  if e, ok := read.m[key]; ok {
    // unexpungeLocked 将 expunged 的标记变成 nil。
    // 当 entry.p == expunged,并且成功替换为 nil,
    // 返回 true。
    // 
    // 这个分支的意义在于,写时复制 dirtyLocked 的时候,
    // 数据从 readOnly map 搬迁到 dirty map 中,
    // 如果 p 是被删除的,dirty 是不会有这个 key 的,
    // 所以要把它也写进 dirty 中,保证数据的一致性。
    // 
    // 为什么好端端的 expunged,要改成 nil?
    // unexpungeLocked 是一个原子操作,成功的话,
    // 说明 p == expunged,
    // 说明写时复制已经完成。
    // 
    // 为什么要写时复制完成之后,才可以去改 dirty?
    // 我理解是这样的:
    // 如果不这样做,dirty 会被你修改成 Store 传进来的参数,
    // 写时复制又把它修改成 readOnly map 的值,
    // 所以更新 readOnly map 就好了。
    // 
    // 这一块的细节真的非常多,每一块地方都要小心处理好。
    if e.unexpungeLocked() {
      m.dirty[key] = e
    }
    // 写入值。
    e.storeLocked(&value)
  } else if e, ok := m.dirty[key]; ok {
    // 如果 dirty map 存在就直接更新进去,这个很好理解,
    // 因为 readOnly map 找不到会来 dirty 查。
    e.storeLocked(&value)
  } else {
    // 两个 map 都找不到的时候,说明这是一个新的 key。
    // 
    // 1. 如果 dirty 之前被提升为 readOnly,那就导一份没有被删除的 key 进来。
    // 
    // 这个判断条件,我理解等价于 if m.dirty == nil {...}
    if !read.amended {
      // 初始化 m.dirty,并把值写进去(写时复制)
      m.dirtyLocked()
      // amended 设置为不一致。
      // amended 表示 dirty 是否包含了 readOnly 没有的记录,
      // 很明显,read.m[key] 是 !ok 的,
      // 下面把值存到 dirty map 里面了。
      m.read.Store(readOnly{m: read.m, amended: true})
    }
    // 2. 这里,把值存到 dirty map 中。
    m.dirty[key] = newEntry(value)
  }
  m.mu.Unlock()
}

精妙绝伦!整个写入的逻辑就讲完了,最后看看遍历吧,非常简单:

func (m *Map) Range(f func(key, value interface{}) bool) {
  read, _ := m.read.Load().(readOnly)
  // 如果不一致,就把 dirty 提升为 readOnly,
  // 同时 dirty 置空,
  // 因为 dirty map 也包含了 readOnly map 没有的 key。
  if read.amended {
    m.mu.Lock()
    read, _ = m.read.Load().(readOnly)
    if read.amended {
      read = readOnly{m: m.dirty}
      m.read.Store(read)
      m.dirty = nil
      m.misses = 0
    }
    m.mu.Unlock()
  }
  // 遍历 readOnly map 的数据,执行回调函数。
  for k, e := range read.m {
    v, ok := e.load()
    if !ok {
      continue
    }
    if !f(k, v) {
      break
    }
  }
}

好了,到这里整个 sync.Map 就讲完了,剩下的代码也没多少了,套路差不多,我们总结一下:

  1. 在读多写少的场景下,sync.Map 的性能非常高,因为访问 readOnly map 是无锁的;
  2. Load:先查找 readOnly map,找不到会去找 dirty map,如果经常没命中,dirty map 会被提升为 readOnly map,提升的时机跟 dirty 的大小相关,dirty 越大,容忍不命中的次数就越多,也就越难提升;
  3. Delete:当 readOnly map 的 key 不存在的时候,会去删除 dirty map 中的 key;如果 readOnly map 的 key 存在,entry.p 置为 nil;
  4. Store :

    1. readOnly map 的 key 存在时,entry.p != expunged 时直接更新,entry.p == expunged 就改成 nil,此时数据也同步写入 dirty map;
    2. readOnly map 的 key 不存在时,dirty map 有就更新进去,两个都没有,触发写时复制机制:搬迁 readOnly map 的没有被删除的 key 到 dirty map 中,新值写入 dirty map,并设置 amended 标记为 true。
  5. sync.Map 的缺陷在于读少写多的时候,dirty map 会被一直更新,misses 次数增加,dirty 置空后,数据又重新从 readOnly map 同步回去,使得 sync.Map 忙于数据搬迁工作,影响性能。

这篇文章近 5000 字(第一篇差不多 2000 字),从构思、成文到校对,真的需要花费不少时间,希望对你有帮助!

阅读本文大约需要 4.25 分钟。

程序是枯燥乏味的。

在讲 sync.Map 之前,我们先说说什么是 map(映射)。

我们每个人都有身份证号码,如果我需要从身份证号码查到对应的姓名,用 map 存储是非常合适的。

map[000...001] = 张三
map[000...002] = 李四
...
map[999...993] = 钱五

身份证号码有 18 位,如果要知道 111...002 这个人叫什么名字,没有 map 我只能从 000...001 一个一个往下查找,效率是非常低的。

咦,那 map 不就是在查字典嘛?根据拼音、笔画、部首,可以查到某个字的具体含义!

没错!Go 语言中的 map 在 Python 语言称之为 dict(字典),意思是完全一样的。

再设想另一个场景,

如果 map 存储的是每个人银行卡里的余额(同一所银行),那就是这样子的形式(账本):

map[张三] = 100.00
map[李四] = 600.00
map[钱五] = 800.00

某一天,李四要转账给张三和钱五,各 100 元,银行为了提高转账速度,安排了两名交易员同时处理。

交易员 A 和交易员 B 瞄了一眼账本,开始操作:

交易员 A:李四的余额是 600 元,张三的余额是 100 元,转账后李四的余额是 500 元,张三的余额是 200 元。
交易员 B:李四的余额是 600 元,钱五的余额是 800 元,转账后李四的余额是 500 元,钱五的余额是 900 元。

账本变成这个样子:

map[张三] = 200.00
map[李四] = 500.00
map[钱五] = 900.00

账本出问题了!银行凭空多出 100 元!

一个一个来不就完了?可是你别忘了,我们是为了提高转账速度,才这样做的。

在 Go 1.9 之前,大部分人还真的就是这么干的!

type Name        string
type Money       string
type AccountBook struct {
    lock sync.RWMutex
    m    map[Name]Money
}

sync.RWMutex 是一个读写锁,在写入数据的时候,阻止其他人写入、读取,让其他人处于等待的状态,直到操作完再释放锁。

本质上,上面的例子,就是读取到了脏数据,如果能等待交易员 A 把账本改完,交易员 B 再去操作,账本就不会乱了。

如果你不知道锁是什么,我再给你讲一个例子:

张三和李四两个人,需要打印不同的文档,

打印机只有一台,放在打印室里,打印室有钥匙,

钥匙只有一把,谁拿到打印室的钥匙,谁就能进去打印。

打印室的钥匙,就是锁。

张三拿了钥匙,进去打印室,打印完了,就出来后把钥匙给了李四,李四打印完了把钥匙还回打印室(真是有条不紊)。

我花费这么多笔墨说 map,也是真的希望,就算你不是程序员,不是 Go 语言后端工程师,也可以看懂我的文章。

不得不承认,把复杂琐碎的东西,讲通透、讲明白是一种本事。

教科书讲 if...else、switch、while (true) 、异常和捕获,

如果有下面的图片这么形象生动就好了:

看到图片的那一瞬间,真的把我逗乐了。

多么形象生动啊!

回头想想,大学的 C 语言课程是多少人的噩梦,老师都是照书念的,完全听不进去。

我也不感慨了,咱们还是回归正题。

刚刚讲了 map,接着往下讲 sync.Map,它用来解决什么问题?

我们知道 map + 锁的形式,还是有等待的现象出现,不符合我们提高转账速度的初衷。

而 sync.Map 有一个非常巧妙的抽象(entry 的 p 指向具体数据的位置):

var m map[key]*entry

type entry struct {
    p unsafe.Pointer
}

还是看回上面的例子,做个小修改——原先的 map 是一个小账本,我们又做了一个大账本,原先的账本变成:

map[张三] = 记录在大账本第 6 页(翻开第 6 页,内容是:100.00)
map[李四] = 记录在大账本第 7 页(翻开第 7 页,内容是:600.00)
map[钱五] = 记录在大账本第 8 页(翻开第 8 页,内容是:800.00)

假设小账本 map 的张三、李四只能一个一个排队改,没办法做到同时修改,

而我们有了大账本,可以直接同时修改张三、李四纸上的内容(两页纸互不影响了)。

(真实的计算机世界确实如此,具体是怎么样的,留一个思考题,下一篇文章细细解答)

更通俗的讲,sync.Map 通过 entry 这个中间层的抽象,

把最开始整个小账本的冲突(影响所有人),降低到大账本上的某一页纸(只影响某个人),

用计算机术语讲,就是降低锁的粒度,从而提升性能!

另一方面,假设李四销户了,

我可以选择在第 7 页的纸上写,已销户(expunged),

// expunged is an arbitrary pointer that marks
// entries which have been deleted from the 
// dirty map.
var expunged = unsafe.Pointer(new(interface{}))

如果是以前,只能把小账本,李四那一张纸撕掉,

而撕掉小账本的某一页,也会影响所有人使用小账本,

如果下次要把撕掉的那一页放回去,也是非常麻烦,

在计算机的世界里,这是资源的分配和回收的问题,会严重影响程序运行效率。

写了一千七百字,直到现在只是冰山一角,sync.Map 的巧妙之处,远远不止 entry 的抽象。

今天先消化这么多,下一篇文章会更深层次一些,敬请期待!

阅读本文大约需要 3.50 分钟。

趁五一有时间,做个小小的复盘。

4 月 10 日,我当时围绕光环新网写了挺多内容,先回顾一下:

风险:商誉减值、坏账损失风险高、股东人数增多、管理层减持、5G 发展、IDC 建设与上架不及预期、定增不及预期、中美关系影响亚马逊 AWS 业务、市场竞争;

仓位:当前技术面上依然是下跌趋势,等资金流入后,趋势反转再进场也不迟,忍不住的可以一成仓位;

亮点:营收能力强、估值低、机构给买入评级、北向资金持股 7.52%、国家严控新建机房,已建成机房大部分在一线城市、机房自建高利率高、代运营亚马逊云。

9 号收盘价 17.02,20 号站上 20 日均线,收盘价 17.51,4 月 30 日收盘价 14.10,亏损 (17.51 - 14.10) / 17.51 = 19.47%……

真是令人绝望。

有朋友说,还不如买浪潮信息。确实,都是同个行业,比较稳,价格也不贵。

佛曰:人生八苦——生、老、病、死、爱别离、怨憎会、求不得、五阴炽盛。

鲁迅曾经说过,人生有九苦——黑、矮、胖、丑、双下巴、香肠嘴、大粗腰、小短腿、没钱。

好家伙,股(韭)民(菜)来股市都是想赚钱的,幻想着今天买进去,接着一周五连板,现实总被啪啪啪打脸。

是求之不得,

是没钱,

是双重痛苦。

算了不说了,鲁迅他老人家的棺材板按不住鸟~

今天买进去,

明天涨停,

真的完美,赶紧卖出。

这是什么?有人调侃说,是一夜情。

有这么好的事?不可能,运气罢了。

搞明白当初为什么买入,这点很重要。

如果当前市场上一个西瓜一百块钱,没人会去买的。

如果降价到一块钱一个,一周后恢复原价一百块钱一个,大妈可能都不跳广场舞了。

她们会把家里的锅碗瓢盆全都拿出来装西瓜,家里的老头子可能把拖拉机都开出来。

换句话说,如果当初买入的价格足够便宜,那你还会考虑一夜情还是娶回家吗?话糙理不糙。

然而现实中,一只股票真的足够便宜,也不见得有人买,即便是买了也不一定拿得住。这又是为什么?

光环 -20% 如果你扛过去了,那再来 -20% 呢?人性很奇怪,能够接受 200% 的涨幅,却容忍不了 2% 的亏损,更别说亏 20% 了。

人性是贪婪的,欲望是无止境的。

我也能理解亏 2% 真的是割肉般的痛苦:)

光环本身也没看到有啥问题,但这个奇葩公司的财报居然点错小数点,搞得大家以为商誉出问题,散户机构用脚投票,一天砸了十多个点,真的服。

如果没有暴雷,说明它又便宜了。不过当前趋势不怎么好,仓位高可以出来点等资金流入再进去。

如果不幸暴雷了,那就割肉离场,认知有限,敬畏市场。

每一笔交易都提前想好为什么。

摸黑滚爬这么久,投资真的是活到老学到老。股市相比指数基金,风险更高,但也意味着更大的弹性。

每个人的想法、思维模式、经历不同,我个人偏向巴菲特的价值投资哲学。

巴菲特的老师格雷厄姆,非常擅于寻找市场上价格比企业当前价格低得多的企业,买下他们,然后寻求价值回归。

可以是等待,也可以是主动清算或者收购等方式。

频繁的短线交易,就是给券商打工。

赌王何鸿燊说十赌九输,你玩不过抽水的——手续费万分之二点五 + 印花税。

所以,控制仓位,放弃择时,拿着优秀的资产,趴在地上,好好享受这个折磨的过程。

特别是你看到身边的人都在涨,天天吃肉的时候,

特别是三天涨了一个点,第四天打回原形,第五天又跌一个点的时候,

特别是真的要坚持不下去了的时候,想象你娶回家的是一个大美女:

三千秀发,

根根入情,

一点朱唇,

半张半合,

说不尽的妩媚妖娆。

过程是痛苦的,

只要方向没错,

向下空间有限,

向上弹性无限。

凭借非常低的价格,

拿着优秀的资产,

剩下的就交给时间吧。

阅读本文大约需要 4.28 分钟。

直到今天,2021 年已经过去了四分之一,有多少人保持正收益?

不得不说,股市真是屠宰场。跌起来毫无人性,却又充满人性:大涨时喜笑颜开,大跌时哀嚎一片。

一哭:

二闹:

三上 (YOU)(YA)

2 月 21 日周日,我写了一篇文章 《 让我告诉你明天买什么股什么基 》提示当下的风险,并建议管住手控制仓位,现在回头看看数据~

先看看沪深 300 指数:

2 月 18 日周四,沪深 300 最高点 5930.91,是 2008 年以来的最高点;
2 月 22 日周一,沪深 300 最高点 5785.51,当日跌 -3.14%,收盘 5597.33;
3 月 25 日周四,沪深 300 跌到短期低点 4883.66,(4883.66 - 5597.33) / 5597.33 = -12.75%
4 月 9 日周五,沪深 300 收盘 5035.34,(5035.34 - 5597.33) / 5597.33 = -10.04%

再看看招商中证白酒指数:

2 月 22 日周一,招商中证白酒指数最高点 1.56,当日跌 -5.17%,收盘 1.49;
3 月 9 日周二,招商中证白酒指数跌到短期低点 1.11,(1.11 - 1.49) / 1.49 = -25.50%
4 月 9 日周五,招商中证白酒指数收盘 1.27,(1.27 - 1.49) / 1.49 = -14.77%

最后看看南方中证新能源 ETF:

2 月 22 日周一,南方中证新能源最高点 1.010,当日跌 -1.92%,收盘 0.971;
3 月 25 日周四,南方中证新能源跌到短期低点 0.770,(0.770 - 0.971) / 0.971 = -20.70%
4 月 9 日周五,南方中证新能源收盘是 0.821,(0.821 - 0.971) / 0.971 = -15.45%

如果你不看我的文章,周一继续冲进去,直到今天,你将亏掉 10% ~ 25% 的本金。

最惨的是 3 月 25 日,有多少人扛不住暴跌割肉离场了?

逃顶是一门艺术~

那当下存在什么机会?衣食住行,我们已经离不开互联网了。

网购:淘宝 or 拼多多 or 京东……
吃饭:饿了么 or 美团……
娱乐:网易云 or B 站 or 抖音 or 百度云……
学习:有道云笔记 or WPS Office……
出行:滴滴打车……

这么多 APP 服务于我们,肯定是离不开服务器的。然而,硬件资源是有限的,资源不够需要加,资源浪费需要减。无形之中,一个 APP 背后需要极高的维护成本、硬件成本等。

比如天猫双十一的时候,网购人数暴涨,按照传统模式需要提前购置大量的服务器,准备大促,节后又要撤下来,等下一轮大促重新上线……

这样反反复复真的累人。有没有解决办法?有,答案是 “云计算”。因为工作原因,对这个行业还算是有些了解:

  1. 可以按需购买;
  2. 点几下鼠标就可以快速启动一台机器;
  3. 不需要自行购置硬件;
  4. 不需要人力成本;
  5. 计算能力不够的时候可以快速扩容,不需要的时候可以点下鼠标销毁掉;
  6. 配合技术手段可以做到全自动。

云计算,能让一个企业拥有海量的计算能力。国内比较有名的有阿里云、腾讯云、华为云等。

前前后后我也看过不少资料,终于找到了一只质地不错的股票(光环)。

先说风险(数据来源于 2019 年资产负债表):

  1. 商誉减值风险高:商誉 / 净资产= 239790 / (1223321 - 388771) = 28.7%;
  2. 坏账损失风险高:应收账款 / 流动资产 = 192826 / 360408 = 53.5%;
  3. 股价走低,但股东人数增多,有机构抛售、散户接盘的嫌疑;
  4. 管理层减持股票;
  5. 5G 发展、IDC 建设与上架不及预期、定增不及预期;**
  6. 中美关系影响亚马逊 AWS 业务;
  7. 市场竞争加剧(阿里云、腾讯云等);
再说仓位管理:

当前技术面上依然是下跌趋势,等资金流入后,趋势反转再进场也不迟,忍不住的可以一成仓位。

抄底是一种能耐~

最后说下亮点:

  1. 营收能力强:近 3 年增速 26%;
  2. 估值低:市盈率 29.64(0.83%),最大值 400.54(100.00%),其中 20% 分位点 38.80,50% 分位点 56.10,80% 分位点 110.52;
  3. 近 3 月 13 家机构给出买入评级,目标均价 26.69,上涨空间 56.82%
  4. 北向资金持股 7.52%;
  5. 互联网或金融客户集中于一线城市,机房耗电量大,国家严控新建机房,已建成机柜超 3 万个,自身机房 84% 在北京,9% 在上海
  6. 自建 VS 租建代建(数据港,阿里重要合作厂商),自建拥有更高的利润
  7. 代运营全球最大的云计算服务 AWS(亚马逊云)。根据 Canalys 去年 10 月数据,AWS 全球市占率为 32%,排行第一,而微软Azure、谷歌云、阿里云市占率分别为 19%、7%、6%;

当下上证指数、深证成指、创业板指已经回到去年 7 月份的水平,成交量低迷,消磨市场信心,身边的人也几乎不谈论股票和基金了。

反正,人多的地方不要去,去了分不到羹。人少的地方呢?

算了算了,炒股犹如算命,话只能说半句,说多了就不灵了。

(投资有风险,入市需谨慎)

阅读本文大约需要 5.18 分钟。

本篇文章是命理知识系列的第二篇,第一篇见 命理案例剖析——什么样的人容易遇到渣男


01 释义
————

十二长生(长生、沐浴、冠带、临官、帝旺、衰、病、死、墓、绝、胎、养),是十天干(甲乙丙丁戊己庚辛壬癸)的十二种运势,可以表示人事物的生老病死或兴亡盛衰。

长生,表示新事物的产生。就像刚刚出生的小孩,虽然弱小,但有旺盛的生命力。
含义:出生、生长、起点、发生、苏醒、来源、源泉、哺育等。

沐浴,也称败地。就像小孩出生后要洗澡,需要大人照顾,这个阶段的小孩子也有很高的可塑造性,所以也有改变的意思。
含义:洗澡、裸体、桃花酒色、暴露、失败、改变等。

冠带,“冠而字之,成人之道也”。成年行冠笈之礼。男子 20 岁,女子 15 岁。穿衣打扮,越来越好看。
含义:穿衣、打扮、包装荣誉、表彰等。

临官,出仕做官,成家立业,挣钱养家,也称建禄(禄为钱财)。
含义:公务员事业单位、官府、地位、自力更生、壮实、步入佳境等。

帝旺,人或事物发展到一定阶段,达到的巅峰状态、最高点。但也意味着没有发展空间,稍有不慎就会退步,物极必反,喜中有忧。
含义:发达、巅峰、顶点、最高点、得意、辉煌、极限、高潮、终点、结局等。

,人开始走下坡路了。衰依然强大,是巅峰时期的衰减;新事物至此已成为旧事物,接受其他新事物的挑战;衰不是老,前者更多是精神上力不从心,注意力跟不上等,后者是生理上已经出现了退化。
含义:衰退、衰弱、无力、软弱、弱点、失去靠山、胆小、退缩等。

,百病丛生,旧事物已经千疮百孔。
含义:生病、病灶、毛病、缺点、把柄、要害、腐败、问题、吃药等。

,离开人世,或旧事物的灭亡。
含义:死亡、死路、死心、一条路走到黑、钻牛角尖、不能变通、想不开、呆板、无退路、滞留、终结、寂静、无生气等。

,坟墓,人死之后最后的归宿。或者是事物被收入仓库,也称库。
含义:埋葬、结束、收藏、存放、管理、控制、操纵、指挥、仓库、黑暗、指挥、包含、纪念等。

,表示消失断绝,转世了。
含义:危险、无退路、绝地、绝境、悬崖、分手、断绝、无情冷酷、消失、无影无踪等。

,人体受孕,或万物在大地中萌芽。
含义:怀胎、酝酿计划、先天的、本性、幼稚、弱小。

,胚胎在母体中成形,或万物在大地中成形。
含义:收养、培育、寄托、修养、养育、疗养、依靠、扶助操心等。


02 查法
————

1. 阳顺阴逆:
甲丙戊庚壬,干,地支顺着排
乙丁己辛癸,干,地支逆着排

2. 戊土寄居丙火,丙戊同宫,排列顺序是一样的;丁己同。

3. 熟悉地支三合局,可从中速记:
申子辰合水局壬水长生于申;
亥卯未合木局甲木长生于亥;
寅午戌合火局丙火长生于寅;
巳酉丑合金局庚金长生于巳。

4. 以下表格可以速查,结合上一节中的各个状态的含义、引申义,最好熟记并背诵:

十天干五行生旺死绝表(十二长生)

 长生沐浴冠带临官帝旺

题外话:我不赞成用的时候再去推算,最好是把整张表死记硬背啃下来。毕竟真正取象的时候,都是看一眼,参考多个信息之后知道个大概,很多时候没有那么多推算的时间。


03 例子
————

1. 庚,长生于巳,临官于申,绝于寅。

男命,日柱庚申,庚金正财为乙木,为妻子。乙胎于申,申是夫妻宫——也就是说,妻子(乙)嫁进来(申)的时候,有可能肚子里是有孩子的(胎)。

女命,日柱庚寅,或庚日,见丙寅年或其他柱,男朋友、情人、丈夫,多半有绝情老死不相往来的信息,寅藏甲木、丙火、戊土,丙火七杀可以代表情人、丈夫。

2. 丙,长生于寅,病在申。丙申,自坐病地。

①丙是申金的七杀,也有疾病的含义;
申金是丙的偏财,可以代表职业
申属金,肃杀,金属,手术刀;
申金悬针煞,针灸,吊针;
——可能是个医生。

②丙偏财为申,申为父亲
申是住宅,申中庚为父亲;
——父亲住进来,可能有身体不舒服的情况。
申藏庚金,还有壬水、戊土;
壬为七杀,为儿子
申是坤卦,是被子
——命主与其父亲、儿子常常睡在同一张床。

寅申庚甲,商路吏人——出自《玉照定真经》。
申是道路,是传送,是车辆,病于申,车辆常常出毛病;
寅年出生,寅是政府,申是驿马,是公家车
——命主可能是交通运输行业相关的公职人员。

④男命,阳历1953年11月21日,生于寅时。命盘如下:

癸癸丙庚
巳亥子寅 乾

6岁  16岁 26岁
壬    辛     庚
戌    酉     申

丙火坐子,身弱。月令亥水七杀,年月透二癸水官星,克泄过多又天寒地冻;
月令亥水,冲年支巳火,丙临官于巳,巳是禄神,也代表自己的身体;
月令亥水,合时辰寅木劫煞,寅刑巳禄;
巳火,带劫煞。

28岁,庚申大运,庚申年(1980年),大病一场,做了肠道手术。

为什么是庚申运、庚申年?
庚申运、庚申年,丙病于申,不吉。
申巳合,寅申冲,寅巳申三刑,不吉。
申是亡神煞,不吉。

仅供参考。

预告:下一篇文章是云计算行业的投资机会,搜集了挺多资料,自己也有蛮多想法……