TIGERB

TIGERB 查看完整档案

北京编辑天津师范大学  |  电信 编辑Xiaomi  |  Web开发 编辑 tigerb.cn 编辑
编辑

// Trying to be the person you want to be.

// 时刻夯实基础
// 时刻对新技术保持热忱

// 个人博客 http://TIGERB.cn
// 轻量级PHP框架EasyPHP 作者 http://easy-php.tigerb.cn
// 电商设计手册|SkrShop 作者 https://github.com/skr-shop/m...

// 新的目标成为一名优秀的 Gopher

个人动态

TIGERB 分享了头条 · 2月1日

我们想搞懂Go语言的内存分配原理前,必须先了解TCMalloc内存分配器,以便于我们更好的理解Go语言的内存分配原理。

赞 0 收藏 0 评论 0

TIGERB 发布了文章 · 1月27日

64位平台下,指针自身的大小为什么是8字节?

系列导读

本系列基于64位平台、1Page=8KB

今天我们开始拉开《Go语言轻松系列》第二章「内存与垃圾回收」的序幕。

关于「内存与垃圾回收」章节,大体从如下三大部分展开:

  • 知识预备:为后续的内容做一些知识储备,知识预备包括

    • 指针的大小
    • Tcmalloc内存分配原理
  • Go内存设计与实现
  • Go的垃圾回收原理

本篇前言

第一部分知识预备的第一个知识点指针的大小

为什么指针的大小会作为一个知识点呢?

因为后续内存管理的内容会涉及一些数据结构,这些数据结构使用到了指针,同时存储指针的值是需要内存空间的,所以我们需要了解指针的大小,便于我们理解一些设计的意图;其次,这也是困扰我的一个问题,因为有看见64位平台下指针底层定义的类型为uint64

为了搞清楚这个问题,我们需要了解两个知识点:

  1. 存储单元
  2. CPU总线

什么是存储单元?

存储单元是存储器(本文指内存)的基本单位,每个存储单元是8bit,也就是1Byte,如下图所示:

同时从上图中我们可以看出,每个存储单元会被编号,这个编号又是什么呢?

  • 就是我们通常所谓的“内存的地址”
  • 也就是指针的值
结论:指针的值就是存储单元的编号。

接着,我们只需要知道这个「编号」的最大值是多少,就可以知道存储「指针」的值所需的大小。要找到这个最大值就需要了解CPU总线的知识了。

CPU总线的概念


CPU总线由系统总线、等等其他总线组成。

总线的组成
系统总线
等等其他总线...

系统总线由一系列总线组成。

系统总线的组成
地址总线
数据总线
信号总线

内存的地址(存储单元的编号)是通过地址总线传递的,地址总线里的“每一根线”传递二进制01,如下图所示(实际不是这么简单,图示为了便于大家理解)。

地址总线的宽度决定了一次能传递多少个01,由于64位CPU每次可处理64位数据,所以理论上地址总线的宽度可以支持到最大64,也就是2^64种组合,可代表的数字范围为0 ~ 2^64-1

结论:理论上64位CPU地址总线可传输的10进制数范围为0 ~ 2^64-1

上面知道64位CPU的地址总线可寻址范围 为 0 ~ 2^64-1,需要一个类型可以存储这个指针的值,毫无疑问就是uint64uint64又是多大呢?是不是8byte。所以:64位平台下,一个指针的大小是8字节

顺便扩充个问题:

为什么32位平台下,可寻址空间是4GB?
备注:64位太大,我们这里用32位来看这个问题

我们来分析一下:

  • 由于,32位平台可支持地址总线的最大宽度为32,及代表的存储单元编号的范围:0 ~ 2^32-1
  • 则,最多可以找到2^32个存储单元
  • 又有,存储单元的大小为8bit(1Byte)

所以我们可以得到,32位平台最多可以寻找到2^32个存储单元,再翻译下2^32个存储单元这句话:

2^32个存储单元 == 2^32个1Byte == 2^32Byte == 4GByte == 4GB

做个总结哈

我们回头再来看,本次内容可以get到如下知识点:

  • 存储器的基本单位是存储单元
  • 存储单元为8bit
  • 指针的值就是存储单元的编号
  • CPU地址总线的宽度决定了指针的值的最大范围

查看《Go语言轻松系列》更多内容

链接 http://tigerb.cn/go/#/kernal/

3911642037-d2bb08d8702e7c91_articlex.jpg

查看原文

赞 2 收藏 1 评论 0

TIGERB 发布了文章 · 1月27日

由浅到深,入门Go语言Map实现原理

导读

Go源码版本1.13.8

今天要分享的是主要内容是Go语言Map底层实现,目的让大家快速了解Go语言Map底层大致的实现原理。读完本篇文章你可以获得收益、以及我所期望你能获取的收益如下:

收益序号收益描述掌握程度
收益1大致对Go语言Map底层实现有一个了解必须掌握
收益2大致知道Go语言Map是如何读取数据的必须掌握
收益3熟悉Go语言Map底层核心结构体hmap可选
收益4熟悉Go语言Map底层核心结构体bmap可选
收益5熟悉Go语言Map底层里的溢出桶可选
收益6熟悉Go语言Map是如何读取数据的可选

收益1和收益2是看了本篇文章希望大家必须掌握的知识点,其他的为可选项,如果你对此感兴趣或者已经掌握了收益1、2可以继续阅读此处的内容。

对于本篇文章的结构主要按如下顺序开展:

  • 简单看看一般Map的实现思路
  • Go语言里Map的实现思路(入门程度:包含收益1、2)
  • Go语言里Map的实现思路(熟悉程度:包含收益3、4、5、6)

其次,本篇文章主要以Map的读来展开分析,因为读弄明白了,其他的写、更新、删除等基本操作基本都可以猜出来了,不是么😏。

简单看看一般Map的实现思路

直入主题,一般的Map会包含两个主要结构:

  • 数组:数组里的值指向一个链表
  • 链表:目的解决hash冲突的问题,并存放键值

大致结构如下:
http://cdn.tigerb.cn/20201216161128.png

读取一个key值的过程大致如下:

                  key
                   |
                   v                 
+------------------------------------+
|      key通过hash函数得到key的hash    |
+------------------+-----------------+
                   |
                   v
+------------------------------------+
|       key的hash通过取模或者位操作     |
|          得到key在数组上的索引        |
+------------------------------------+
                   |
                   v
+------------------------------------+
|         通过索引找到对应的链表         |
+------------------+-----------------+
                   |
                   v
+------------------------------------+
|       遍历链表对比key和目标key        |
+------------------+-----------------+
                   |
                   v
+------------------------------------+
|              相等则返回value         |
+------------------+-----------------+
                   |
                   v                
                 value 

接着我们来简单看看Go语言里Map的实现思路。

Go语言里Map的实现思路(入门程度)

包含收益1、2

Go语言解决hash冲突不是链表,实际主要用的数组(内存上的连续空间),如下图所示:

备注:后面我们会解释上面为啥用的“主要”两个字。

http://cdn.tigerb.cn/20201219202458.png

但是并不是只使用一个数组(连续内存空间)存放键和值,而是使用了两个数组分别存储键和值,图示如下:

http://cdn.tigerb.cn/20201217210507.png

上图中:

  • 分别对应的是两个核心的结构体hmapbmap
  • bmap里有两个数组分别存放key和value

把上面简化的关系转换一下,其实就是这样的一个大致关系,如下图所示:

http://cdn.tigerb.cn/20201217210752.png

我们通过一次读操作为例,看看读取某个key的值的一个大致过程

步骤编号描述
通过hash函数获取目标key的哈希,哈希和数组的长度通过位操作获取数组位置的索引(备注:获取索引值的方式一般有取模或位操作,位操作的性能好些)
遍历bmap里的键,和目标key对比获取key的索引(找不到则返回空值)
根据key的索引通过计算偏移量,获取到对应value

读过程图示如下:

http://cdn.tigerb.cn/20201217210816.png

这么看起来是不是“很简单”、很清晰,所以读到这里,你是不是已经入门了Go语言Map底层实现并且:

  • 大致对Go语言Map底层实现有一个了解(收益1)
  • 大致知道Go语言Map是如何读取数据的(收益2)

然而实际情况不止如此,我们再稍微深入的探索下,有兴趣的可以继续往下看,没兴趣可以不用继续往下看了(开玩笑=^_^=),反正已经达到目的了,哈哈😏。

Go语言里Map的实现思路(熟悉程度)

包含收益3、4、5、6

想要深入学习,首先得了解下上面提到了实现Map的两个核心结构体hmapbmap

核心结构体hmap

收益3: 熟悉Go语言Map底层核心结构体`hmap`

hmap的结构其实刚开始看起来其实还是比较复杂的,有不少的字段,具体字段如下图所示:

http://cdn.tigerb.cn/20201218132443.png

字段释义如下:

字段解释
count键值对的数量
B2^B=len(buckets)
hash0hash因子
buckets指向一个数组(连续内存空间),数组的类型为[]bmap,bmap类型就是存在键值对的结构下面会详细介绍,这个字段我们可以称之为正常桶。如下图所示
oldbuckets扩容时,存放之前的buckets(Map扩容相关字段)
extra溢出桶结构,正常桶里面某个bmap存满了,会使用这里面的内存空间存放键值对
noverflow溢出桶里bmap大致的数量
nevacuate分流次数,成倍扩容分流操作计数的字段(Map扩容相关字段)
flags状态标识,比如正在被写、buckets和oldbuckets在被遍历、等量扩容(Map扩容相关字段)
备注:本次内容不涉及Map的扩容逻辑。

重点看一些字段的含义和用处。

字段buckets

http://cdn.tigerb.cn/20201216202022.png

buckets指向了一个数组(连续的内存空间),数组的元素是bmap类型,这个字段我们称之为正常桶。

hmap的源码和地址如下:

// https://github.com/golang/go/blob/go1.13.8/src/runtime/map.go
type hmap struct {
    count     int 
    flags     uint8
    B         uint8 
    noverflow uint16 
    hash0     uint32
    buckets    unsafe.Pointer
    oldbuckets unsafe.Pointer
    nevacuate  uintptr 
    extra *mapextra
}

核心结构体bmap

收益4: Go语言Map底层核心结构体`bmap`

正常桶hmap.buckets的元素是一个bmap结构。bmap的具体字段如下图所示:

http://cdn.tigerb.cn/20201216202114.png

字段释义如下:

字段解释
topbits长度为8的数组,[]uint8,元素为:key获取的hash的高8位,遍历时对比使用,提高性能。如下图所示
keys长度为8的数组,[]keytype,元素为:具体的key值。如下图所示
elems长度为8的数组,[]elemtype,元素为:键值对的key对应的值。如下图所示
overflow指向的hmap.extra.overflow溢出桶里的bmap,上面的字段topbitskeyselems长度为8,最多存8组键值对,存满了就往指向的这个bmap里存
pad对齐内存使用的,不是每个bmap都有会这个字段,需要满足一定条件

http://cdn.tigerb.cn/20201216202224.png

推断出bmap结构字段的代码和位置如下:

// https://github.com/golang/go/blob/go1.13.8/src/cmd/compile/internal/gc/reflect.go
func bmap(t *types.Type) *types.Type {
  // 略...

  field := make([]*types.Field, 0, 5)

    field = append(field, makefield("topbits", arr))

  // 略...
  
    keys := makefield("keys", arr)
    field = append(field, keys)

  // 略...
  
    elems := makefield("elems", arr)
    field = append(field, elems)

  // 略...
  
    if int(elemtype.Align) > Widthptr || int(keytype.Align) > Widthptr {
        field = append(field, makefield("pad", types.Types[TUINTPTR]))
    }

  // 略...
  
    overflow := makefield("overflow", otyp)
    field = append(field, overflow)

  // 略...
}
结论:每个bmap结构最多存放8组键值对。

hmapbmap的基本结构合起来

分别了解了hmapbmap的基本结构后,我们把上面的内容合并起来,就得到如下的Map结构图:

http://cdn.tigerb.cn/20201216202349.png

溢出桶

收益5: 熟悉Go语言Map底层里的溢出桶

上面讲bmap的时候,我们不是得到了个结论么“每个bmap结构最多存放8组键值对。”,所以问题来了:

正常桶里的bmap存满了怎么办?

解决这个问题我们就要说到hmap.extra结构了,hmap.extra是个结构体,结构图示和字段释义如下:

http://cdn.tigerb.cn/20201216202608.png

字段解释
overflow称之为溢出桶。和hmap.buckets的类型一样也是数组[]bmap,当正常桶bmap存满了的时候就使用hmap.extra.overflowbmap。所以这里有个问题正常桶hmap.buckets里的bmap是怎么关联上溢出桶hmap.extra.overflowbmap呢?我们下面说。
oldoverflow扩容时存放之前的overflow(Map扩容相关字段)
nextoverflow指向溢出桶里下一个可以使用的bmap

源码和地址如下:

// https://github.com/golang/go/blob/go1.13.8/src/runtime/map.go
type mapextra struct {
    overflow    *[]*bmap
    oldoverflow *[]*bmap
    nextOverflow *bmap
}
问题:正常桶hmap.buckets里的bmap怎么关联上溢出桶hmap.extra.overflowbmap呢?

答:就是我们介绍bmap结构时里的bmap.overflow字段(如下图所示)。bmap.overflow是个指针类型,存放了对应使用的溢出桶hmap.extra.overflow里的bmap的地址。

http://cdn.tigerb.cn/20201221131007.png

问题又来了

问题:正常桶hmap.buckets里的bmap什么时候关联上溢出桶hmap.extra.overflowbmap呢?

答:Map写操作的时候。这里直接看关键代码:

// https://github.com/golang/go/blob/go1.13.8/src/runtime/map.go
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
  // 略
again:
    // 略...
    var inserti *uint8
  // 略...
bucketloop:
    for {
        for i := uintptr(0); i < bucketCnt; i++ {
      // key的hash高8位不相等
            if b.tophash[i] != top {
        // 当前位置bmap.tophash的元素为空且还没有写入的记录(inserti已经写入的标记为)
                if isEmpty(b.tophash[i]) && inserti == nil {
          // inserti赋值为当前的hash高8位 标记写入成功
                    inserti = &b.tophash[i]
                    // 略...
                }
                // 略...
                continue
            }
            // 略...
            goto done
    }
    // 正常桶的bmap遍历完了 继续遍历溢出桶的bmap 如果有的话
        ovf := b.overflow(t)
        if ovf == nil {
            break
    }
        b = ovf
    }

  // 略...

  // 没写入成功(包含正常桶的bmap、溢出桶的bmap(如果有的话))
    if inserti == nil {
    // 分配新的bmap写
    newb := h.newoverflow(t, b)
    // 略...
    }

    // 略...
}

// 继续看h.newoverflow的代码
func (h *hmap) newoverflow(t *maptype, b *bmap) *bmap {
  var ovf *bmap
  // 如果hmap的存在溢出桶 且 溢出桶还没用完
    if h.extra != nil && h.extra.nextOverflow != nil {
    // 使用溢出桶的bmap
    ovf = h.extra.nextOverflow
    // 判断桶的bmap的overflow是不是空
    // 这里很巧妙。为啥?
    // 溢出桶初始化的时候会把最后一个bmap的overflow指向正常桶,值不为nil
    // 目的判断当前这个bmap是不是溢出桶里的最后一个
        if ovf.overflow(t) == nil {
      // 是nil
      // 说明不是最后一个
            h.extra.nextOverflow = (*bmap)(add(unsafe.Pointer(ovf), uintptr(t.bucketsize)))
        } else {
      // 不是nil
      // 则重置当前bmap的overflow为空
      ovf.setoverflow(t, nil)
      // 且 标记nextOverflow为nil 说明当前溢出桶用完了
            h.extra.nextOverflow = nil
        }
    } else {
    // 没有溢出桶 或者 溢出桶用完了
    // 内存空间重新分配一个bmap
        ovf = (*bmap)(newobject(t.bucket))
  }
  // 生成溢出桶bmap的计数器计数
    h.incrnoverflow()
  // 略...
  // 这行代码就是上面问题我们要的答案:
  // 正常桶`hmap.buckets`里的`bmap`在这里关联上溢出桶`hmap.extra.overflow`的`bmap`
    b.setoverflow(t, ovf)
    return ovf
}

// setoverflow函数的源码
func (b *bmap) setoverflow(t *maptype, ovf *bmap) {
  // 这行代码的意思:通过偏移量计算找到了bmap.overflow,并把ovf这个bmap的地址赋值给了bmap.overflow
    *(**bmap)(add(unsafe.Pointer(b), uintptr(t.bucketsize)-sys.PtrSize)) = ovf
}

下面代码这段代码解释了,上面的源码中为何如此判断预分配溢出桶的bmap是最后一个的原因。

// https://github.com/golang/go/blob/go1.13.8/src/runtime/map.go
// 创建hmap的正常桶
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
  // 略...
    if base != nbuckets {
    // 略...
    last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize)))
    // 把溢出桶里 最后一个 `bmap`的`overflow`指先正常桶的第一个`bmap`
    // 获取预分配的溢出桶里`bmap`时,可以通过判断overflow是不是为nil判断是不是最后一个
        last.setoverflow(t, (*bmap)(buckets))
  }
  // 略...
}

hmap存在溢出桶时,且当前溢出桶只被使用了一个bmap时,我们可以得到如下的关系图:

http://cdn.tigerb.cn/20201217165310.png

同时我们可以看出正常桶的bmap和溢出桶的bmap实际构成了链表关系,所以这也解释了开篇我们说到的“Go里面Map的实现主要用到了数组”,其次还用到了链表。

再次分析Map的读

收益6: 熟悉Go语言Map是如何读取数据的

通过上面的学习,我们再次通过一次读操作为例,看看读取某个key的值的一个大致过程:

http://cdn.tigerb.cn/20201217165551.png

结合代码分析下整个大体的过程:

func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
    // ...略
    
    // ①通过hash函数获取当前key的哈希
    hash := alg.hash(key, uintptr(h.hash0))
    m := bucketMask(h.B)
    // ②通过当前key的哈希获取到对应的bmap结构的b
    // 这里的b 我们称之为“正常桶的bmap”
    // “正常桶的bmap”可能会对应到溢出桶的bmap结构,我们称之为“溢出桶的bmap”
    b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
    
    // ...略
    
    // 获取当前key的哈希的高8位
    top := tophash(hash)
bucketloop:
    // 下面的for循环是个简写,完整如下。
    // for b = b; b != nil; b = b.overflow(t) {
    // 可以知道b的初始值为上面的“正常桶的bmap”,则:
    // 第一次遍历:遍历的是“正常桶的bmap”
    // 如果正常桶没找到,则
    // 绿色线条④ 继续遍历:如果当前“正常桶的bmap”中的overflow值不为nil(说明“正常桶的bmap”关联了“溢出桶的bmap”),则遍历当前指向的“溢出桶的bmap”继续 蓝色线条的③④⑤步骤
    for ; b != nil; b = b.overflow(t) {
        // 由于b的初始值为“正常桶的bmap”,第一次先遍历“正常桶的bmap”
        for i := uintptr(0); i < bucketCnt; i++ {
            // 蓝色线条③ 对比key哈希的高8位
            // 对比哈希的高8位目的是为了加速
            if b.tophash[i] != top {
                // emptyRest 标志位:表示当前位置已经是末尾了;删除操作会设置此标志位
                if b.tophash[i] == emptyRest {
                    break bucketloop
                }
                continue
            }
            // 找到了相同的hash高8位,则:找到对应索引位置i的key
            k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
            if t.indirectkey() {
                k = *((*unsafe.Pointer)(k))
            }
            // 蓝色线条④ 对比key是不是一致
            if alg.equal(key, k) {
                // 蓝色线条⑤ key是一致,则:获取对应索引位置的值
                e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
                if t.indirectelem() {
                    e = *((*unsafe.Pointer)(e))
                }
                // 返回找到的结果
                return e
            }
        }
    }
    // 正常桶、溢出桶都没找到则返回 “空值”
    return unsafe.Pointer(&zeroVal[0])
}
参考:
1.《Go语言设计与实现》https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-hashmap/
2. Go源码版本1.13.8 https://github.com/golang/go/tree/go1.13.8/src

3911642037-d2bb08d8702e7c91_articlex.jpg

查看《Go语言轻松系列》更多内容

链接 http://tigerb.cn/go/#/kernal/

查看原文

赞 3 收藏 2 评论 4

TIGERB 分享了头条 · 2020-12-21

今天要分享的是主要内容是Go语言Map底层实现,目的让大家快速了解Go语言Map底层大致的实现原理。

赞 1 收藏 3 评论 0

TIGERB 分享了头条 · 2020-11-05

嗯,Go设计模式实战系列,一个设计模式业务真实使用的golang系列。

赞 1 收藏 1 评论 0

TIGERB 分享了头条 · 2020-10-27

千呼万唤始出来😁 SkrShop《订单中心》第1篇 🎉🎉🎉~ 尽力为你解开「电商订单系统」的面纱🤷‍♀️

赞 2 收藏 0 评论 0

TIGERB 关注了专栏 · 2020-07-09

人云思云

码农一只

关注 207

TIGERB 发布了文章 · 2020-07-01

你想知道的优惠券业务,SkrShop来告诉你

经过两年的更新「SkrShop」已经构成了下面的架构:

图中紫色的内容就是本编文章的主要内容:营销体系的基础服务「优惠券服务」。但是呢,首先要说的是关于不断被催更的事。

关于催更?

我给出了如下解释:人逢假日懒🤷‍♀️(我没错😭)、工作紧、需要保证质量,就酱。但是我一定能保证的是一直会更新下去,希望得到大家理解。

关于下期内容?

之前在Github上的Issues大家一致想看关于订单相关的内容,所以更新完本期「优惠券」之后就开始了订单之旅

Issues如下:

1. https://github.com/skr-shop/manuals/issues/25
2. https://github.com/skr-shop/manuals/issues/18

进入正题,营销体系的基础服务「优惠券服务」。通过如下问题来介绍优惠券:

  • 优惠券有哪些类型
  • 优惠券有哪些适用范围
  • 优惠券有哪些常见的场景
  • 优惠券服务要有哪些服务能力
  • 优惠券服务的风控怎么做?

优惠券有哪些类型?

对于获取优惠券的用户而言:关注的是优惠券的优惠能力,所以按优惠能力维度优惠券主要分为下面三类:

优惠能力维度描述
满减券满多少金额(不含邮费)可以减多少金额
现金券抵扣多少现金(无门槛)
抵扣券抵扣某Sku全部金额(一个数量)
折扣券打折

对于发放优惠券的运营人员而言:

一种是「固定有效期」,优惠券的生效时间戳和过期时间戳,在创建优惠券的时候已经确定。用户在任意时间领取该券,该券的有效时间都是之前设置的有效时间的开始结束时间。

另一种是「动态有效期」,创建优惠券设置的是有效时间段,比如7天有效时间、12小时有效时间等。这类优惠券以用户领取优惠券的时间为优惠券的有效时间的开始时间,以以用户领取优惠券的时间+有效时间为有效时间的结束时间。

有效期维度优惠券类型优惠券生效时间优惠券失效时间描述
固定固定有效期优惠券类型被创建时已确定优惠券类型被创建时已确定无论用户什么时间领取该优惠券,优惠券生效的时间都是设置好的统一时间
动态动态有效期用户领取优惠券时,当前时间戳用户领取优惠券时,当前时间戳 + N*24*60*60优惠券类型被创建时,只确定了该优惠券的有效,例如6小时、7天、一个月

小结如下:

优惠券有哪些适用范围?

运营策略

运营策略描述
(非)指定SkuSku券
(非)指定SpuSpu券
(非)指定类别类别券
指定店铺店铺券
全场通用平台券

适用终端

适用终端(复选框)描述
Android安卓端
iOSiOS端
PC网页电脑端
Mobile网页手机端
Wechat微信端
微信小程序微信小程序
All以上所有

适用人群

适用人群描述
白名单测试用户
会员会员专属

小结如下:

优惠券有哪些常见的场景?

领取优惠券场景

领取优惠券场景描述
活动页面大促、节假日活动页面展示获取优惠券的按钮
游戏页面通过游戏获取优惠券
店铺首页店铺首页展示领券入口
商品详情商品详情页面展示领券入口
积分中心积分兑换优惠券

展示优惠券场景

展示优惠券场景描述
活动页面大促、节假日活动页面展示可以领取的优惠券
商品详情商品详情页面展示可以领取、可以使用的优惠券列表
个人中心-我的优惠券我的优惠券列表
订单结算页面结算页面,适用该订单的优惠券列表以及推荐
积分中心展示可以兑换的优惠券详情

选择优惠券场景

选择优惠券场景描述
商品详情商品详情页面展示该用户已有的,且适用于该商品的优惠券
订单结算页面-优惠券列表选择可用优惠券结算
订单结算页面-输入优惠码输入优惠码结算

返还优惠券场景

返还优惠券场景描述
未支付订单取消未支付的订单,用户主动取消返还优惠券,或超时关单返还优惠券
已支付订单全款取消已支付的订单,订单部分退款不返还,当整个订单全部退款返还优惠券

场景示例

场景示例描述
活动页领券大促、节假日活动页面展示获取优惠券的按钮
游戏发券游戏奖励
商品页领券-
店铺页领券-
购物返券购买某个Sku,订单妥投后发放优惠券
新用户发券新用户注册发放优惠券
积分兑券积分换取优惠券

小结如下:

优惠券服务要有哪些服务能力?

服务能力1: 发放优惠券

发放方式描述
同步发放适用于用户点击领券等实时性要求较高的获取券场景
异步发放适用于实时性要求不高的发放券场景,比如新用户注册发券等场景
发放能力描述
单张发放指定一个优惠券类型ID,且指定一个UID只发一张该券
批量发放指定一个优惠券类型ID,且指定一批UID,每个UID只发一张该券
发放类型描述
优惠券类型标识通过该优惠券类型的身份标识发放,比如创建一个优惠券类型时会生成一个16位标识码,用户通过16位标识码领取优惠券;这里不使用自增ID(避免对外泄露历史创建了的优惠券数量),
优惠码code创建一个优惠券类型时,运营人员会给该券填写一个6位左右的Ascall码,比如SKR6a6,用户通过该码领取优惠券

服务能力2: 撤销优惠券

撤销能力描述
单张撤销指定一个优惠券类型ID,且指定一个UID只撤销一张该券
批量撤销指定一个优惠券类型ID,且指定一批UID,每个UID撤销一张该券

服务能力3: 查询优惠券

用户优惠券列表子类描述
全部-查询该用户所有的优惠券
可以使用全部查询该用户所有可以使用的优惠券
-适用于某个spu或sku查询该用户适用于某个spu或sku可以使用的优惠券
-适用于某个类别查询该用户适用于某个类别可以使用的优惠券
-适用于某个店铺查询该用户适用于某个店铺可以使用的优惠券
无效全部查询该用户所有无效的优惠券
-过期查询该用户所有过期的优惠券
-失效查询该用户所有失效的优惠券

服务能力4: 结算页优惠券推荐

订单结算页面推荐一张最适合该订单的优惠券

小结如下:

优惠券服务的风控怎么做?

一旦有发生风险的可能则触发风控:

  • 对用户,提示稍后再试或联系客服
  • 对内部,报警提示,核查校验报警是否存在问题

频率限制

领取描述
设备ID每天领取某优惠券的个数限制
UID每天领取某优惠券的个数限制
IP每天领取某优惠券的个数限制
使用描述
设备ID每天使用某优惠券的个数限制
UID每天使用某优惠券的个数限制
IP每天使用某优惠券的个数限制
手机号每天使用某优惠券的个数限制
邮编比如注重邮编的海外地区,每天使用某优惠券的个数限制

用户风险等级

依托用户历史订单数据,得到用户成功完成交易(比如成功妥投15天+)的比率,根据此比率对用户进行等级划分,高等级进入通行Unblock名单,低等级进入Block名单,根据不同用户级别设置限制策略。等其他大数据分析手段。

阈值

  • 发券预算
  • 实际使用券预算

根据预算值设置发券总数阈值,当触发阈值时阻断并报警。

优惠券不要支持虚拟商品

优惠券尽量不要支持虚拟商品以防止可能被利用的不法活动。


3911642037-d2bb08d8702e7c91_articlex.jpg

查看原文

赞 2 收藏 2 评论 0

认证与成就

  • SegmentFault 讲师
  • 获得 1472 次点赞
  • 获得 138 枚徽章 获得 5 枚金徽章, 获得 53 枚银徽章, 获得 80 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • easy-php

    A Faster Lightweight Full-Stack PHP Framework

  • easy-tips

    A little tips in my code career with PHP

  • easy-vue

    An easy example using the vue to implement easy web

  • naruto

    An object-oriented multi process manager for PHP

  • 电商设计手册

    Do design No code | 只设计不码码

注册于 2016-01-08
个人主页被 17.3k 人浏览