<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>代码实战归档 - 枫阿雨&#039;s blog</title>
	<atom:link href="https://www.crazyfay.com/category/code-practice/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.crazyfay.com/category/code-practice/</link>
	<description>CrazyFay</description>
	<lastBuildDate>Sun, 09 Apr 2023 01:45:15 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.5.2</generator>

<image>
	<url>https://www.crazyfay.com/wp-content/uploads/2023/04/cropped-DockerGopher-32x32.png</url>
	<title>代码实战归档 - 枫阿雨&#039;s blog</title>
	<link>https://www.crazyfay.com/category/code-practice/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Bitmap的设计实现及实战应用</title>
		<link>https://www.crazyfay.com/2023/04/08/bitmap%e7%9a%84%e8%ae%be%e8%ae%a1%e5%ae%9e%e7%8e%b0%e5%8f%8a%e5%ae%9e%e6%88%98%e5%ba%94%e7%94%a8/</link>
					<comments>https://www.crazyfay.com/2023/04/08/bitmap%e7%9a%84%e8%ae%be%e8%ae%a1%e5%ae%9e%e7%8e%b0%e5%8f%8a%e5%ae%9e%e6%88%98%e5%ba%94%e7%94%a8/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Sat, 08 Apr 2023 11:52:01 +0000</pubDate>
				<category><![CDATA[代码实战]]></category>
		<category><![CDATA[Bitmap]]></category>
		<category><![CDATA[Golang]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=237</guid>

					<description><![CDATA[<p>大家估计都知道今天的主角Bitmap（位图）这个东西，也都知道它是一种非常有趣且鹅妹子嘤带点儿“黑科技”的数据 [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/04/08/bitmap%e7%9a%84%e8%ae%be%e8%ae%a1%e5%ae%9e%e7%8e%b0%e5%8f%8a%e5%ae%9e%e6%88%98%e5%ba%94%e7%94%a8/">Bitmap的设计实现及实战应用</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p>大家估计都知道今天的主角Bitmap（位图）这个东西，也都知道它是一种非常有趣且鹅妹子嘤带点儿“黑科技”的数据结构，它能够用非常非常低的存储成本存储数据的状态，在这篇文章中，我将从0到1实现一个Bitmap，并基于Redis的Bitmap在实际应用场景中发挥Bitmap的优点。</p>
<h2>实现一个Bitmap</h2>
<h3>原理与实现思路</h3>
<p>我们首先来简单的再过一遍位图的原理，Bitmap是一种将每一个字节用到极致的数据结构，位图的使用场景实际上和布尔数组是相同的，都是表示某个索引代表的数据非0即1的二元对立关系，只不过数据在bitmap中的存储会更加紧凑，使用bit位来作为下标索引，以0或1来代替bool的false or true。当需要在bool数组中判断下标1024的数据是否为true时，就等价于在bitmap中的第1024位是否为1。bool数组在大多数语言中每个bool值的大小都是1byte（8bit）这么算下来，bitmap可以省去8倍的存储空间。</p>
<blockquote>
<p>在Java中，单个bool值占4byte，而bool数组的底层实现实际为byte数组，即1byte</p>
</blockquote>
<p>在绝大多数的编程语言中，可供我们直接操作的最小单位是byte，只有一个字节，也就是8个bit（这与内存寻址是以byte为单位有关）。所以我们要想操作bit来实现bitmap，需要通过位移操作来完成。</p>
<pre><code class="language-golang">Bitmap实例与创建方法
// Bitmap的数据底层由byte slice组织
type Bitmap []byte

// 通过指定的bit长度新建一个Bitmap实例
func MakeBitmapWithBitSize(nBits int) Bitmap {
    // 规定一个最小长度
    if nBits &lt; 64 {
        nBits = 64
    }
    // 需要确保bit长度为8的倍数
    return MakeBitmapWithByteSize((nBits + 7) / 8)
}

// 通过指定的byte长度新建一个Bitmap实例
func MakeBitmapWithByteSize(nBytes int) Bitmap {
    return make([]byte, nBytes)
}

// 注: pos的起始位置为0
// 将指定pos的bit设置为1
func (b Bitmap) SetTrue(bitPos uint32) {
    b[bitPos/8] |= 1 &lt;&lt; (bitPos % 8)
}

// 将指定pos的bit设置为0
func (b Bitmap) SetFalse(bitPos uint32) {
    b[bitPos/8] &amp;= ^(1 &lt;&lt; (bitPos % 8))
}

// 判断指定pos的bit是否为1
func (b Bitmap) IsTrue(bitPos uint32) bool {
    return b[bitPos/8]&amp;(1&lt;&lt;(bitPos%8)) != 0
}

// 重置Bitmap
func (b Bitmap) Reset() {
    for i := range b {
        b[i] = 0
    }
}

// Bitmap所存储的字节数
func (b Bitmap) ByteSize(bitPos uint32) int {
    return len(b)
}

// Bitmap所存储的位数
func (b Bitmap) BitSize(bitPos uint32) int {
    return len(b) * 8
}</code></pre>
<p>是的，我们这样就实现了一个Bitmap，是不是会感觉 <strong>就这？</strong> 确实，Bitmap本身就是一个非常简单朴实的数据结构，给人一种大道至简的感觉。 所有的核心操作都集中以下三步当中，我们来逐一拆解操作过程</p>
<pre><code class="language-golang">// SetTrue
b[bitPos/8] |= 1 &lt;&lt; (bitPos % 8)
// SetFalse
b[bitPos/8] &amp;= ^(1 &lt;&lt; (bitPos % 8))
// IsTrue
b[bitPos/8] &amp; (1 &lt;&lt; (bitPos % 8)) != 0</code></pre>
<h3>bi[bitPos/8] |= 1 &lt;&lt; (bitPos % 8)</h3>
<p>这个操作将Bitmap的 bitPos 位置上的bit位设置为1。 我们通过结合例子的方式逐步用白话解析这个操作：</p>
<ul>
<li><strong>现假设我需要将<code>0110 0101</code>的第2位通过bitmap.SetTrue(1)置为1</strong></li>
</ul>
<ol>
<li><strong>首先</strong>，bitPos / 8 计算出需要操作的字节索引（即第几个字节）。因为一个字节有8个bit位，所以我们使用除法运算来计算该位所在的那个字节。在此例中就是在下标为0的索引上的byte处。</li>
<li><strong>然后</strong>，bitPos % 8 计算出该字节中需要操作的位数。使用取模运算，可以计算出该位在字节中的偏移量。在此例中偏移量为1</li>
<li><strong>接下来</strong>，执行 1 &lt;&lt; (bitPos % 8) 按位左移运算，将 1（即0000 0001） 左移 bitPos % 8 位（即位于字节中的偏移量）。在此例中就是将 0000 0001 向左移1位得到 0000 0010</li>
<li><strong>最后</strong>，执行 b[bitPos/8] |= 1 &lt;&lt; (bitPos % 8) 操作，我们采用位或运算的操作&quot;|&quot;进行赋值（0|0 = 0、0|1 = 1、1|1 = 1）将该位设置为 1。在此例中就是计算0110 0101 | 0000 0010 = 0111 最终得到我们想要的结果 <code>0110 0111</code>。</li>
</ol>
<h3>b[bitPos/8] &amp;= ^(1 &lt;&lt; (bitPos % 8))</h3>
<p>这个操作将Bitmap的 bitPos 位置上的bit位设置为0。 我们通过结合例子的方式逐步用白话解析这个操作：</p>
<ul>
<li><strong>现假设我需要将<code>0110 0101</code>的第6位通过bitmap.SetFalse(5)置为0</strong></li>
</ul>
<ol>
<li><strong>首先</strong>，bitPos / 8 计算出需要操作的字节索引（即第几个字节）。因为一个字节有8个bit位，所以我们使用除法运算来计算该位所在的那个字节。在此例中就是在下标为0的索引上的byte处。</li>
<li><strong>然后</strong>，bitPos % 8 计算出该字节中需要操作的位数。使用取模运算，可以计算出该位在字节中的偏移量。在此例中偏移量为5</li>
<li><strong>接下来</strong>，执行 1 &lt;&lt; (bitPos % 8) 按位左移运算，将 1（即0001） 左移 bitPos % 8 位（即位于字节中的偏移量）。在此例中就是将 0000 0001 向左移5位得到 0010 0000</li>
<li><strong>接着</strong>，应用按位取反运算符将掩码取反（在Go语言中 &quot;^&quot; 作为一元运算符时表示按位取反操作），得到一个这个除了我们要删除的那个bit位之外所有位都是1的掩码。再此例中就是 1101 1111.</li>
<li><strong>最后</strong>，执行 b[bitPos/8] &amp;= ^(1 &lt;&lt; (bitPos % 8)) 操作，我们采用位或运算的操作&quot;&amp;&quot;进行赋值（0|0 = 0、0|1 =0、1|1 = 1）将该位设置为 0。在此例中就是计算 0110 0101 &amp; 1101 1111 = 0111 最终得到我们想要的结果 <code>0100 0101</code>。</li>
</ol>
<h3>b[bitPos/8 ] &amp; (1 &lt;&lt; (bitPos % 8)) != 0</h3>
<p>这个操作判断 bitmap 的 bitPos 位置上的二进制位是否为1。 我们通过结合例子的方式逐步用白话解析这个操作：</p>
<ul>
<li><strong>现假设我需要使用bitmap.IsTrue(6)判断<code>0110 0101</code>的第7位是否为1</strong></li>
</ul>
<ol>
<li><strong>首先</strong>，bitPos / 8 计算出需要操作的字节索引（即第几个字节）。因为一个字节有8个bit位，所以我们使用除法运算来计算该位所在的那个字节。在此例中就是在下标为0的索引上的byte处。</li>
<li><strong>然后</strong>，bitPos % 8 计算出该字节中需要操作的位数。使用取模运算，可以计算出该位在字节中的偏移量。在此例中偏移量为6</li>
<li><strong>接下来</strong>，执行 1 &lt;&lt; (bitPos % 8) 按位左移运算，将 1（即0000 0001） 左移 bitPos % 8 位（即位于字节中的偏移量）。在此例中就是将 0000 0001 向左移6位得到 0100 0000。</li>
<li><strong>接着</strong>，执行 b[bitPos/8] &amp; (1 &lt;&lt; (bitPos % 8)) 操作，我们通过与运算只保留bitmap中的指定字节其他bit位均为0，如果不为1的话，那么这8个bit全部都归零，如果为1，则这8个bit组成的byte必不等于0。在此例中就是计算 0110 0101 &amp; 0100 0000 = 0100 0000</li>
<li><strong>最后</strong>，执行 != 0 操作，如果该位为1，则返回true；否则返回false。</li>
</ol>
<p>怎么样是不是非常的清清爽爽~</p>
<p>既然造完了轮子咱们就开始实战吧~</p>
<h2>利用Redis的Bitmap实现用户点赞</h2>
<p>嘿嘿，bitmap的实战我偏不用自己造的轮子，就是玩儿~</p>
<blockquote>
<p>其实主要是Redis中的数据结构是可以跨进程共享的，所以一般应用的更广泛些，我们造好的轮子将在后面的实战文章中使用（用来造另一个轮子XD）</p>
</blockquote>
<p>在很多的业务中模块，都会有一个点赞的功能，点赞操作可能会比较频繁，而且因为可能会涉及到资源的热度和推荐指数，且用户的直接感知很强，所以也算是一个相对重要的功能。</p>
<p>点赞功能的设计有以下几点：</p>
<ol>
<li>要能够及时反馈，让用户有所感知，并且要尽量避免用户显示数据的不一致</li>
<li>要能够标记用户是否已点赞，避免用户刷赞操作</li>
<li>一个页面加载时可能会有大量需要统计点赞的资源，需要对点赞进行缓存处理，避免大量的磁盘io</li>
</ol>
<p>结合以上设计需求，我先说出最终可以选择的方案：</p>
<p>采用基于Redis的异步缓存写入策略，以资源类型的前缀与资源ID作为Key，维护一个Bitmap，以<strong>用户的自增ID</strong>作为Bitmap中的标识索引，实现用户的点赞操作的记录。后台运行一个定时定时任务，定时将Redis中的数据批量同步到数据库的持久化存储中。</p>
<p>这样设计有以下的特点：</p>
<ol>
<li>用户获取点赞数、执行点赞操作都是直接和基于内存的Redis打交道，速度相对较快。</li>
<li>使用Bitmap存储用户点赞记录，可省下大量的内存空间。</li>
<li>采用异步缓存写入策略，本质上是<strong>依赖Redis作为稳定的存储介质</strong>而非简单的缓存，只要Redis不崩溃在客户端就不会发生不一致的现象（比如点赞后用户刷新，发现点赞消失）。</li>
</ol>
<blockquote>
<p>当然，模块的设计与技术选型是需要根据业务本身的特点而定的，在当前的业务设计中就是默认Redis是较为可靠的存储，基本不会出现Redis崩溃的状况。</p>
</blockquote>
<h3>代码示例</h3>
<p>以评论模块的点赞操作为例</p>
<p>一些const枚举值</p>
<pre><code class="language-golang">type IsLike int
const (
    UNLIKE IsLike = iota
    LIKE
)

const(
  // COMMENT:LIKE:commentID -&gt; bitmap
  COMMENT_LIKE_REDISPREKEY = &quot;COMMENT:LIKE:&quot;
)</code></pre>
<p>核心方法</p>
<pre><code class="language-golang">// 更改点赞状态，由上游传入的isLike判断是点赞操作还是取消点赞操作
func (c *CommentDomain) ConvertLikeState(commentId int64, uid int64, isLike IsLike) *errs.BError {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    key := COMMENT_LIKE_REDISPREKEY+strconv.FormatInt(commentId, 10)
    err := c.redis.SetBit(ctx, key, uid, int(isLike)).Error()
    if err != nil {
        zap.L().Error(&quot;updateLikeState redis SetBit ERROR&quot;, zap.Error(err))
        return errs.RedisError
    }
    return nil
}

// 判断用户是否已点赞（前端需要知晓的状态）
func (c *CommentDomain) HasLiked(commentId int64, uid int64) (bool, *errs.BError) {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    key := COMMENT_LIKE_REDISPREKEY+strconv.FormatInt(commentId, 10)
    bit, err := c.redis.GetBit(ctx, key, uid).Result()
    if err != nil {
        zap.L().Error(&quot;HasLiked redis GetBit ERROR&quot;, zap.Error(err))
        return false, errs.RedisError
    }
    return bit == 1, nil
}

// 获取评论的点赞数
func (c *CommentDomain) CountLikeNum(commentId int64) (int64, *errs.BError) {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    key := COMMENT_LIKE_REDISPREKEY+strconv.FormatInt(commentId, 10)
    count, err := c.redis.BitCount(ctx, key, &amp;redis.BitCount{Start: 0, End: -1}).Result()
    if err != nil {
        zap.L().Error(&quot;CountLikeNum redis BitCount ERROR&quot;, zap.Error(err))
        return 0, errs.RedisError
    }
    return count, nil
}</code></pre>
<p>简单的测试用例</p>
<pre><code class="language-golang">var commentDomain = domain.NewCommentDomain()

func TestLike(t *testing.T) {
    count, err := commentDomain.CountLikeNum(5)
    log.Printf(&quot;init comment %d Like Num %d \n&quot;, 5, count)

    err = commentDomain.ConvertLikeState(5, 1000, LIKE)
    count, err = commentDomain.CountLikeNum(5)
    log.Printf(&quot;comment %d Like Num %d  after %d like \n&quot;, 5, count, 1000)

    err = commentDomain.ConvertLikeState(5, 1001, LIKE)
    count, err = commentDomain.CountLikeNum(5)
    log.Printf(&quot;comment %d Like Num %d  after %d like \n&quot;, 5, count, 1000)

    err = commentDomain.ConvertLikeState(5, 1001, UNLIKE)
    count, err = commentDomain.CountLikeNum(5)
    log.Printf(&quot;comment %d Like Num %d  after %d unlike \n&quot;, 5, count, 1001)

    liked, err := commentDomain.HasLiked(5, 1001)
    count, err = commentDomain.CountLikeNum(5)
    log.Printf(&quot;%d hasLiked comment %d : %t \n&quot;, 1001, count, liked)

    liked, err = commentDomain.HasLiked(5, 1000)
    count, err = commentDomain.CountLikeNum(5)
    log.Printf(&quot;%d hasLiked comment %d : %t \n&quot;, 1000, count, liked)

    if err != nil {
        log.Fatal(err)
    }
}</code></pre>
<p>输出结果</p>
<pre><code class="language-sh">=== RUN   TestLike
2023/04/08 16:52:33 init comment 5 Like Num 0 
2023/04/08 16:52:33 comment 5 Like Num 1  after 1000 like 
2023/04/08 16:52:33 comment 5 Like Num 2  after 1000 like 
2023/04/08 16:52:33 comment 5 Like Num 1  after 1001 unlike 
2023/04/08 16:52:33 1001 hasLiked comment 1 : false 
2023/04/08 16:52:33 1000 hasLiked comment 1 : true 
--- PASS: TestLike (0.02s)
PASS</code></pre>
<p>这样，一个最基本的评论点赞模块就做好了，当然，这是将封装好的模块拆出来放在这里的示例代码，在实际应用中应该遵循项目的模块进行封装</p>
<h3>Extra</h3>
<p>在调研其他网站的api设计的时候，发现了一个很有意思的点。CSDN在点赞模块的设计上，点赞与取消点赞的客户端请求<strong>完全一样</strong>！</p>
<p>无论是请求URL、请求方法、表单数据全部都一样！</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/da6cd9b4459447f5fa33a663f10d7d55.png" alt="截图" /></p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/433cb8d7b8a7f274465c9338c5cef8b2.png" alt="截图" /></p>
<p>在返回相应的时候，数据才会有所不同</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/3040d9343b4d18eefc6d4ee8c114399a.png" alt="截图" /></p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/696d458c963cc150f365f9f745c1d46d.png" alt="截图" /></p>
<p>这是一个很有意思的点，如果前端不传状态的话，后端该如何判断此操作是取消点赞还是点赞操作呢？</p>
<p>既然前端没有标明，那就只能后端先查再改了，但是这无疑是比较吃资源的行为。假设csdn的后台也是用Redis来存储点赞标识，那么也需要查两次Redis，多一次网络通信的开销，我能想到的稍微合理一点的操作逻辑是配合使用lua脚本，在Redis中执行lua脚本进行分支判断进行Compare And Swap的原子操作，代码实例如下</p>
<pre><code class="language-golang">func (c *CommentDomain) ConvertLikeStateCAS(commentId int64, uid int64) (int64, bool, *errs.BError) {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    // lua脚本
    script := `
        local key = KEYS[1]
        local userId = ARGV[1]
        local isLiked = redis.call(&quot;GETBIT&quot;, key, userId)
        if isLiked == 1 then
            redis.call(&quot;SETBIT&quot;, key, userId, 0)
            local isLiked = 0
        else
            redis.call(&quot;SETBIT&quot;, key, userId, 1)
            local isLiked = 1
        end
        local likeCount = redis.call(&quot;BITCOUNT&quot;, key)
        return {likeCount, isLiked}
    `
    key := COMMENT_LIKE_REDISPREKEY + strconv.FormatInt(commentId, 10)
    result, err := c.redis.Eval(ctx, script, []string{key}, uid)
    resArr := result.([]interface{})
    likeCount := resArr[0].(int64)
    hassLiked := resArr[1].(int64)
    if err != nil {
        zap.L().Error(&quot;ConvertLikeStateCAS redis Eval ERROR&quot;, zap.Error(err))
        return 0, false, errs.RedisError
    }
    return likeCount, hassLiked == 0, nil
}</code></pre>
<p>简单的测试用例</p>
<pre><code class="language-golang">func TestEval(t *testing.T) {
    // 1000 1001 1002 点赞 1002取消赞
    num, like, bError := commentDomain.ConvertLikeStateCAS(6, 1000)
    log.Println(&quot;after 0 like status: &quot;, like, &quot; likeNum: &quot;, num)

    num, like, bError = commentDomain.ConvertLikeStateCAS(6, 1001)
    log.Println(&quot;after 1 like status: &quot;, like, &quot; likeNum: &quot;, num)

    num, like, bError = commentDomain.ConvertLikeStateCAS(6, 1002)
    log.Println(&quot;after 2 like status: &quot;, like, &quot; likeNum: &quot;, num)

    num, like, bError = commentDomain.ConvertLikeStateCAS(6, 1003)
    log.Println(&quot;after 3 like status: &quot;, like, &quot; likeNum: &quot;, num)

    num, like, bError = commentDomain.ConvertLikeStateCAS(6, 1001)
    log.Println(&quot;after 1 unlike status: &quot;, like, &quot; likeNum: &quot;, num)

    for i := 1000; i &lt; 1004; i++ {
        hasLiked, _ := commentDomain.HasLiked(6, int64(i))
        log.Println(&quot;member&quot;, i, &quot;hasliked &quot;, hasLiked)
    }
    if bError != nil {
        log.Fatal(bError)
    }
    log.Println(&quot;after likeNum: &quot;, num)
}</code></pre>
<p>简单的测试用例</p>
<pre><code class="language-golang">func TestEval(t *testing.T) {
    // 1000 1001 1002 点赞 1002取消赞
    num, like, bError := commentDomain.ConvertLikeStateCAS(6, 1000)
    log.Println(&quot;after 1000 like status: &quot;, like, &quot; likeNum: &quot;, num)
    num, like, bError = commentDomain.ConvertLikeStateCAS(6, 1001)
    log.Println(&quot;after 1001 like status: &quot;, like, &quot; likeNum: &quot;, num)
    num, like, bError = commentDomain.ConvertLikeStateCAS(6, 1002)
    log.Println(&quot;after 1002 like status: &quot;, like, &quot; likeNum: &quot;, num)
    num, like, bError = commentDomain.ConvertLikeStateCAS(6, 1003)
    log.Println(&quot;after 1003 like status: &quot;, like, &quot; likeNum: &quot;, num)
    num, like, bError = commentDomain.ConvertLikeStateCAS(6, 1001)
    log.Println(&quot;after 1001 unlike status: &quot;, like, &quot; likeNum: &quot;, num)

    for i := 1000; i &lt; 1004; i++ {
        hasLiked, _ := commentDomain.HasLiked(6, int64(i))
        log.Println(&quot;member&quot;, i, &quot;hasliked &quot;, hasLiked)
    }
    if bError != nil {
        log.Fatal(bError)
    }
    log.Println(&quot;after likeNum: &quot;, num)
}</code></pre>
<p>输出结果</p>
<pre><code class="language-sh">=== RUN   TestEval
2023/04/08 19:34:29 after 1000 like status:  true  likeNum:  1
2023/04/08 19:34:29 after 1001 like status:  true  likeNum:  2
2023/04/08 19:34:29 after 1002 like status:  true  likeNum:  3
2023/04/08 19:34:29 after 1003 like status:  true  likeNum:  4
2023/04/08 19:34:29 after 1001 unlike status:  false  likeNum:  3
2023/04/08 19:34:29 member 1000 hasliked  true
2023/04/08 19:34:29 member 1001 hasliked  false
2023/04/08 19:34:29 member 1002 hasliked  true
2023/04/08 19:34:29 member 1003 hasliked  true
2023/04/08 19:34:29 after likeNum:  3
--- PASS: TestEval (0.03s)
PASS</code></pre>
<p>这样一来，就可以实现和csdn的点赞一样的效果了（虽然至今我也不知道这样设计的优点在哪里，但还是挺有意思的</p>
<blockquote>
<p>说起csdn，推荐一个油猴脚本 ，可以避免登录才能复制之类的讨厌的限制（虽然改变不了csdn里面的内容多数都是又水又烂的现状，能在国内的掘金知乎找到的东西还是别去看csdn了吧<br />
<a href="https://greasyfork.org/zh-CN/scripts/378351-持续更新-csdn广告完全过滤-人性化脚本优化-不用再登录了-让你体验令人惊喜的崭新csdn"><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" />持续更新<img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> CSDN广告完全过滤、人性化脚本优化：<img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f195.png" alt="🆕" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 不用再登录了！让你体验令人惊喜的崭新CSDN (greasyfork.org)</a></p>
</blockquote>
<h2>Tips：</h2>
<p>以上的设计是基于用户id为自增id的基础上而实现的，如果在你的系统中使用的是UUID或者雪花算法生成的ID请勿直接使用bitmap，这会导致bitmap爆掉~<br />
UUID是128位的字符串，且完全没有顺序，并不适合使用bitmap存储<br />
雪花算法生成ID是64位的整型，也不能直接使用bitmap因为 <strong>根 本 存 不 下</strong>！</p>
<pre><code class="language-shell">2^64 bit = 18446744073709551616 bit
= 2305843009213696 MB
= 2251799813685248 GB
= 2202670619951340.672 TB</code></pre>
<p>（不过雪花算法是有序的，所以理论上可以从最小id开始记录）<br />
所以你也看到了bitmap的一个缺陷，它最适合存储的是连续的数据，它所占用的空间也是根据最大的数而定的，如果系统中的数据分布的非常广，可能bitmap就并不是很合适，这种情况下，可能采用hash的方式是更合理的。<br />
此外，Redis中还提供了一种叫做HyperLogLog的数据结构用于基数统计，它要比bitmap还要更“黑科技”，不过相应的，它也是以部分准确性的代价来换取的，可以用在对准确性没有那么高的统计数值上，比如“浏览量”，之后有时间的话也许会写一片文章来介绍一下HLL在实践中的应用吧XD</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/04/08/bitmap%e7%9a%84%e8%ae%be%e8%ae%a1%e5%ae%9e%e7%8e%b0%e5%8f%8a%e5%ae%9e%e6%88%98%e5%ba%94%e7%94%a8/">Bitmap的设计实现及实战应用</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2023/04/08/bitmap%e7%9a%84%e8%ae%be%e8%ae%a1%e5%ae%9e%e7%8e%b0%e5%8f%8a%e5%ae%9e%e6%88%98%e5%ba%94%e7%94%a8/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Window-TinyLFU缓存实现</title>
		<link>https://www.crazyfay.com/2023/02/02/window-tinylfu%e7%bc%93%e5%ad%98%e5%ae%9e%e7%8e%b0/</link>
					<comments>https://www.crazyfay.com/2023/02/02/window-tinylfu%e7%bc%93%e5%ad%98%e5%ae%9e%e7%8e%b0/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Thu, 02 Feb 2023 06:12:50 +0000</pubDate>
				<category><![CDATA[代码实战]]></category>
		<category><![CDATA[cache]]></category>
		<category><![CDATA[Golang]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=222</guid>

					<description><![CDATA[<p>在我的一个基于LSM-Tree结构的存储引擎项目中，由于LSM-Tree结构的查询操作高度依赖缓存，所以我需要 [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/02/02/window-tinylfu%e7%bc%93%e5%ad%98%e5%ae%9e%e7%8e%b0/">Window-TinyLFU缓存实现</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p>在我的一个基于LSM-Tree结构的存储引擎项目中，由于LSM-Tree结构的查询操作高度依赖缓存，所以我需要一个高性能的本地缓存模块来提高查询效率，抱着学习的目的，我们来 <del>重复造轮子</del> 亲自实现一个拥有良好性能的缓存模块。</p>
<p>所谓缓存，本质上就是 <code>映射+淘汰策略</code> ，如何在有限的空间存储最”有用“的数据，也就是如何设定淘汰策略往往是我们所需要关注的，在Redis中，我们很多时候都是用的TTL（Time To Live），以固定生存时间的角度控制数据的淘汰，但是对于一个存储引擎来说，显然不是一个良好的方案，而除了固定生存时间的角度就是访问频率的角度，基于访问的角度则指向了两种算法： <code>LRU</code> 和 <code>LFU</code> 。</p>
<h2>LRU 和 LFU 算法</h2>
<h3>LRU 算法</h3>
<p>LRU （Least Recently Used） 即最近最久未使用算法</p>
<p>LRU 算法的宗旨是，在定量空间的存储数据时，当新增数据后超出缓存设定的阈值，淘汰最长时间没有访问到数据。在 LRU 算法的思想下，如果一段数据在最近的时间内没有被访问到，那么它接下来被访问到的概率也很小，执行淘汰。</p>
<p>在某些情况下，LRU算法是简单且有效的，但是在有些情况下，它就并不是十分的合理，如下操作。</p>
<table>
<thead>
<tr>
<th>最早访问</th>
<th>中间访问</th>
<th>最晚访问</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>A</td>
<td>B</td>
<td>C</td>
<td>初始数据</td>
</tr>
<tr>
<td>B</td>
<td>C</td>
<td>A</td>
<td>访问A</td>
</tr>
<tr>
<td>B</td>
<td>A</td>
<td>C</td>
<td>访问C</td>
</tr>
<tr>
<td>B</td>
<td>A</td>
<td>C</td>
<td>访问C</td>
</tr>
<tr>
<td>B</td>
<td>C</td>
<td>A</td>
<td>访问A</td>
</tr>
<tr>
<td>B</td>
<td>A</td>
<td>C</td>
<td>访问C</td>
</tr>
<tr>
<td>A</td>
<td>C</td>
<td>B</td>
<td>访问B</td>
</tr>
<tr>
<td>C</td>
<td>B</td>
<td>D</td>
<td>插入D</td>
</tr>
</tbody>
</table>
<p>经过多次访问后，最终插入数据时淘汰了数据A，而通过访问情况我们可以发现，数据A和C的访问频率都相比B要高，而淘汰策略却保留了B舍弃了A。在局部性显著的时候，LRU这种淘汰策略也是正确的，但是在其它局部性不显著的大量数据访问的情况下（如全量遍历），缓存就有可能被污染，导致查询性能下降。且LRU对热门数据的保护不强，不过这也使得LRU有用更强的访问模式适应能力</p>
<blockquote>
<p>此处由于展示方式有限，数据量较少，可自行脑补大量数据时产生热门数据被淘汰的特殊情况</p>
</blockquote>
<h3>LFU 算法</h3>
<p>LFU（Least Frequently Used）即最近最少使用算法。</p>
<p>LFU算法根据数据的历史访问频率来淘汰数据，其核心思想是“如果数据过去被访问多次，那么将来被访问的频率也更高”。</p>
<p>由于其淘汰策略所致，对于突发的稀疏流量，LFU的应对能力不如LRU，大量新数据可能不被缓存，但LFU所带来的好处就是其对于热点数据的缓存命中率会更高</p>
<h3>优缺点对比</h3>
<p>LRU</p>
<ul>
<li>优点：实现简单，可以应对突变的访问模式</li>
<li>缺点：难以应对缓存污染，对于热数据的缓存命中率低于LFU</li>
</ul>
<p>LFU</p>
<ul>
<li>优点：拥有更高的热门数据命中率</li>
<li>缺点：难以应对突变的稀疏流量、可能存在旧数据长期不淘汰，且需要额外消耗来记录更新访问次数</li>
</ul>
<h2>Window-TinyLFU算法</h2>
<p>在 <code>Java</code> 中有一个很出名的 <code>Caffeine</code> 的高性能本地缓存库，正是因为其实现的 <code>Window-TinyLFU</code> 的回收策略为它提供了良好的缓存命中率。</p>
<p>在《TinyLFU: A Highly E cient Cache Admission Policy》论文中，详细介绍了 TinyLFU 这种通过LRU实现类LFU功能的结构设计。</p>
<h2>实现</h2>
<p>对外暴露的结构体，其中包含了读写锁、window-lr、分段lru、布隆过滤器、cmSketch算法次数统计器、保险的阈值</p>
<pre><code class="language-go">type Cache struct {
    // 读写锁
    m         sync.RWMutex
    // window-lru
    lru       *windowLRU
    // 分段lru
    slru      *segmentedLRU
    // 布隆过滤器
    bf        *BloomFilter
    // cmSketch 次数统计器
    c         *cmSketch
    // total 总共的访问次数
    t         int32
    // 保险设计的阈值 
    threshold int32
    // 数据的实际存储
    data      map[uint64]*list.Element
}</code></pre>
<h2>window-lru</h2>
<pre><code class="language-go">type windowLRU struct {
    data map[uint64]*list.Element
    cap  int
    list *list.List
}

type storeItem struct {
    stage    int
    key      uint64
    conflict uint64
    value    interface{}
}

func newWindowLRU(size int, data map[uint64]*list.Element) *windowLRU {
    return &amp;windowLRU{
        data: data,
        cap:  size,
        list: list.New(),
    }
}

func (lru *windowLRU) add(newItem storeItem) (eitem storeItem, evicted bool) {
    // If part of window is not full, insert it directly
    if lru.list.Len() &lt; lru.cap {
        lru.data[newItem.key] = lru.list.PushFront(&amp;newItem)
        return storeItem{}, false
    }7

    evictItem := lru.list.Back()
    item := evictItem.Value.(*storeItem)

    delete(lru.data, item.key)

    eitem, *item = *item, newItem

    lru.data[item.key] = evictItem
    lru.list.MoveToFront(evictItem)
    return eitem, true
}

func (lru *windowLRU) get(v *list.Element) {
    lru.list.MoveToFront(v)
}

func (lru *windowLRU) String() (s string) {
    for e := lru.list.Front(); e != nil; e = e.Next() {
        s += fmt.Sprintf(&quot;%v,&quot;, e.Value.(*storeItem).value)
    }
    return s
}</code></pre>
<h2>segmented-lru</h2>
<pre><code class="language-go">type segmentedLRU struct {
    data                     map[uint64]*list.Element
    stageOneCap, stageTwoCap int
    stageOne, stageTwo       *list.List
}

const (
    STAGE_ONE = iota + 1
    STAGE_TWO
)

func newS2LRU(data map[uint64]*list.Element, stageOneCap, stageTwoCap int) *segmentedLRU {
    return &amp;segmentedLRU{
        data:        data,
        stageOneCap: stageOneCap,
        stageTwoCap: stageTwoCap,
        stageOne:    list.New(),
        stageTwo:    list.New(),
    }
}

func (s2lru *segmentedLRU) add(newItem storeItem) {
    // 先进来的都放 stageOne
    newItem.stage = STAGE_ONE
    // 如果 stageOne 没满, 整个LFU区域也没满
    if s2lru.stageOne.Len() &lt; s2lru.stageOneCap || s2lru.Len() &lt; s2lru.stageOneCap+s2lru.stageTwoCap {
        s2lru.data[newItem.key] = s2lru.stageOne.PushFront(&amp;newItem)
        return
    }
    // 走到这里说明 stageOne 满了,或者整个LFU都满了
    // 则需要载 stageOne 淘汰数据
    e := s2lru.stageOne.Back()
    item := e.Value.(*storeItem)
    // 淘汰数据
    delete(s2lru.data, item.key)
    *item = newItem
    s2lru.data[item.key] = e
    s2lru.stageOne.MoveToFront(e)
}

func (s2lru *segmentedLRU) get(v *list.Element) {
    item := v.Value.(*storeItem)

    // 若访问的缓存数据已经载StageTwo,只需要按照LRU规则提前即可
    if item.stage == STAGE_TWO {
        s2lru.stageTwo.MoveToFront(v)
        return
    }
    // 若访问的数据还在StageOne,那么两次被访问倒,就需要提升到StageTwo阶段了
    if s2lru.stageTwo.Len() &lt; s2lru.stageTwoCap {
        s2lru.stageOne.Remove(v)
        item.stage = STAGE_TWO
        s2lru.data[item.key] = s2lru.stageTwo.PushFront(item)
        return
    }
    // 新数据加入StageTwo, 需要淘汰旧数据
    // StageTwo 中淘汰的数据不会丢失,会进入StageOne
    // StageOne 中,访问频率低的数据,可能会被淘汰
    // 将第二个链表和第一个链表中的数据进行交换
    back := s2lru.stageTwo.Back()
    bItem := back.Value.(*storeItem)
    *bItem, *item = *item, *bItem
    bItem.stage = STAGE_TWO
    item.stage = STAGE_ONE

    // 数据提前
    s2lru.data[item.key] = v
    s2lru.data[bItem.key] = back
    s2lru.stageOne.MoveToFront(v)
    s2lru.stageOne.MoveToFront(back)
}
func (s2lru *segmentedLRU) victim() *storeItem {
    // 如果s2lru的容量未满,不需要淘汰
    if s2lru.Len() &lt; s2lru.stageOneCap+s2lru.stageTwoCap {
        return nil
    }
    // 如果已经满了, 则需要从20%的区域淘汰数据,直接从末尾部拿最后一个数据即可
    v := s2lru.stageOne.Back()
    return v.Value.(*storeItem)
}

func (s2lru *segmentedLRU) String() (s string) {
    for e := s2lru.stageTwo.Front(); e != nil; e = e.Next() {
        s += fmt.Sprintf(&quot;%v,&quot;, e.Value.(*storeItem).value)
    }
    s += fmt.Sprintf(&quot; | &quot;)
    for e := s2lru.stageOne.Front(); e != nil; e = e.Next() {
        s += fmt.Sprintf(&quot;%v,&quot;, e.Value.(*storeItem).value)
    }
    return s
}

func (s2lru *segmentedLRU) Len() int {
    return s2lru.stageOne.Len() + s2lru.stageTwo.Len()
}</code></pre>
<h2>cmSketch</h2>
<pre><code class="language-go">const (
    cmDepth = 4
)

type cmSketch struct {
    rows [cmDepth]cmRow
    seed [cmDepth]uint64
    mask uint64
}

func newCmSketch(numCounters int64) *cmSketch {
    if numCounters == 0 {
        panic(&quot;cmSketch: invalid numCounters&quot;)
    }
    // 因为在位图的实际存储中2个Counters存放在一个byte中，所以numCounters为一定为偶数
    numCounters = next2Power(numCounters)
    // mask 为numcounter - 1 即一定是0111...111，用以保留后n位
    sketch := &amp;cmSketch{mask: uint64(numCounters - 1)}
    source := rand.New(rand.NewSource(time.Now().UnixNano()))

    // 假设预计cache 6条数据，初始化[4]rows如下
    // 0000,0000|0000,0000|0000,0000
    // 0000,0000|0000,0000|0000,0000
    // 0000,0000|0000,0000|0000,0000
    // 0000,0000|0000,0000|0000,0000
    for i := 0; i &lt; cmDepth; i++ {
        sketch.seed[i] = source.Uint64()
        sketch.rows[i] = newCmRow(numCounters)
    }
    return sketch
}

// 在计数器中增加某key的计数
func (s *cmSketch) Increment(hashed uint64) {
    // 对于每行进行相同操作
    for i := range s.rows {
        s.rows[i].increment((hashed ^ s.seed[i]) &amp; s.mask)
    }
}

// 估算的访问次数
func (s *cmSketch) Estimate(hashed uint64) int64 {
    min := byte(255)
    for i := range s.rows {
        val := s.rows[i].get((hashed ^ s.seed[i]) &amp; s.mask)
        if val &lt; min {
            min = val
        }
    }
    return int64(min)
}

// 将所有计数器值减半，即保鲜机制
func (s *cmSketch) Reset() {
    for _, r := range s.rows {
        r.reset()
    }
}

// 将所有计数器归零
func (s *cmSketch) Clear() {
    for _, r := range s.rows {
        r.clear()
    }
}

// 快速计算大于 X，且最接近 X 的二次幂
func next2Power(x int64) int64 {
    x--
    x |= x &gt;&gt; 1
    x |= x &gt;&gt; 2
    x |= x &gt;&gt; 4
    x |= x &gt;&gt; 8
    x |= x &gt;&gt; 16
    x |= x &gt;&gt; 32
    x++
    return x
}

// 计数器位图
type cmRow []byte

// 计数器的每个key的计数值（counter）占用4bit，每个byte为8bit，故cmRow的长度为计数总量的一半
func newCmRow(numCounters int64) cmRow {
    return make(cmRow, numCounters/2)
}

func (r cmRow) get(n uint64) byte {
    return r[n/2] &gt;&gt; ((n &amp; 1) * 4) &amp; 0x0f
}

func (r cmRow) increment(n uint64) {
    // 定位到第i个couter
    i := n / 2
    // 右移距离，偶数为0，奇数为4
    // 决定了取前4bit 还是后4bit
    s := (n &amp; 1) * 4
    v := (r[i] &gt;&gt; s) &amp; 0x0f
    // 若没有超过最大计数，则计数+1
    if v &lt; 15 {
        r[i] += 1 &lt;&lt; s
    }
}

// 保险机制
func (r cmRow) reset() {
    // 给每个byte中的2个counter同时减半
    for i := range r {
        r[i] = (r[i] &gt;&gt; 1) &amp; 0x77
    }
}

// 清零
func (r cmRow) clear() {
    for i := range r {
        r[i] = 0
    }
}

func (r cmRow) String() (s string) {
    for i := uint64(0); i &lt; uint64(len(r)*2); i++ {
        s += fmt.Sprintf(&quot;%02d &quot;, (r[(i/2)]&gt;&gt;((i&amp;1)*4))&amp;0x0f)
    }
    s = s[:len(s)-1]
    return s
}</code></pre>
<h2>最终的封装</h2>
<pre><code class="language-go">type Cache struct {
    m         sync.RWMutex
    lru       *windowLRU
    slru      *segmentedLRU
    bf        *BloomFilter
    c         *cmSketch
    t         int32
    threshold int32
    data      map[uint64]*list.Element
}

type Options struct {
    lruPct uint8
}

// NewCache size: 要缓存的数据数量
func NewCache(size int) *Cache {
    // 定义window部分缓存所占百分比,这里定义为1%
    const lruPct = 1
    // 计算window部分的容量
    lruSize := (lruPct * size) / 100
    if lruSize &lt; 1 {
        lruSize = 1
    }
    // 计算LFU部分的缓存容量
    slruSize := int(float64(size) * ((100 - lruPct) / 100.0))
    if slruSize &lt; 1 {
        slruSize = 1
    }
    // LFU 分为两部分, stageOne部分占比20%
    slru1 := int(0.2 * float64(slruSize))
    if slru1 &lt; 1 {
        slru1 = 1
    }
    data := make(map[uint64]*list.Element, size)
    return &amp;Cache{
        lru:  newWindowLRU(lruSize, data),
        slru: newS2LRU(data, slru1, slruSize-slru1),
        bf:   NewBloomFilter(size, 0.01),
        c:    newCmSketch(int64(size)),
        data: data,
    }
}

// Set
// todo Optimize this method by using generics
func (c *Cache) Set(key, value interface{}) bool {
    c.m.Lock()
    defer c.m.Lock()
    return c.set(key, value)
}

func (c *Cache) set(key, value interface{}) bool {
    // keyHash 用来快速定位, conflictHash 用来判断冲突
    keyHash, conflictHash := c.key2Hash(key)
    // 刚放进去的缓存都先放到window lru 中, 所以stage = 0
    i := storeItem{
        stage:    0,
        key:      keyHash,
        conflict: conflictHash,
        value:    value,
    }
    // 如果window 已满, 返回被淘汰的数据
    eitem, evicted := c.lru.add(i)

    if !evicted {
        return true
    }

    // 如果window中有被淘汰的数据,会走到这里
    // 需要从LFU的stageOne 部分找到一个淘汰者
    // 二者进行再次比较
    victim := c.slru.victim()
    // 如果LFU未满,那么window lru的淘汰数据,可以进入stageOne
    if victim == nil {
        c.slru.add(eitem)
        return true
    }
    // 先在bloomfilter中查找
    // 如果存在,说明访问频率 &gt;= 2
    if !c.bf.Allow(uint32(eitem.key)) {
        return true
    }
    // 估算windowlru和LFU中淘汰数据, 历史访问频次
    // 访问频率高的,更有资格留下
    vcount := c.c.Estimate(victim.key)
    ocount := c.c.Estimate(eitem.key)
    if ocount &lt; vcount {
        return true
    }
    // 留下来的进入 stageOne
    c.slru.add(eitem)
    return true
}

func (c *Cache) Get(key interface{}) (interface{}, bool) {
    c.m.RLock()
    defer c.m.RUnlock()
    return c.get(key)
}

func (c *Cache) get(key interface{}) (interface{}, bool) {
    c.t++
    if c.t == c.threshold {
        c.c.Reset()
        c.bf.Reset()
        c.t = 0
    }
    keyHash, confilctHash := c.key2Hash(key)
    val, ok := c.data[keyHash]
    if !ok {
        c.bf.Allow(uint32(keyHash))
        c.c.Increment(keyHash)
        return nil, false
    }
    item := val.Value.(*storeItem)
    if item.conflict != confilctHash {
        c.bf.Allow(uint32(keyHash))
        c.c.Increment(keyHash)
        return nil, false
    }
    c.bf.Allow(uint32(keyHash))
    c.c.Increment(item.key)

    v := item.value
    if item.stage == 0 {
        c.lru.get(val)
    } else {
        c.slru.get(val)
    }
    return v, true
}

func (c *Cache) Del(key interface{}) (interface{}, bool) {
    c.m.Lock()
    defer c.m.Unlock()
    return c.del(key)
}

func (c *Cache) del(key interface{}) (interface{}, bool) {
    keyHash, conflictHash := c.key2Hash(key)
    val, ok := c.data[keyHash]
    if !ok {
        return 0, false
    }
    item := val.Value.(*storeItem)

    if conflictHash != 0 &amp;&amp; (conflictHash != item.conflict) {
        return 0, false
    }
    delete(c.data, keyHash)
    return item.conflict, true
}

func (c *Cache) key2Hash(key interface{}) (uint64, uint64) {
    if key == nil {
        return 0, 0
    }
    switch k := key.(type) {
    case uint64:
        return k, 0
    case string:
        return MemHashString(k), xxhash.Sum64String(k)
    case []byte:
        return MemHash(k), xxhash.Sum64(k)
    case byte:
        return uint64(k), 0
    case int:
        return uint64(k), 0
    case int32:
        return uint64(k), 0
    case uint32:
        return uint64(k), 0
    case int64:
        return uint64(k), 0
    default:
        panic(&quot;Key type not supported&quot;)
    }
}

type stringStruct struct {
    str unsafe.Pointer
    len int
}

//go:noescape
//go:linkname memhash runtime.memhash
func memhash(p unsafe.Pointer, h, s uintptr) uintptr

func MemHashString(str string) uint64 {
    ss := (*stringStruct)(unsafe.Pointer(&amp;str))
    return uint64(memhash(ss.str, 0, uintptr(ss.len)))
}

func MemHash(data []byte) uint64 {
    ss := (*stringStruct)(unsafe.Pointer(&amp;data))
    return uint64(memhash(ss.str, 0, uintptr(ss.len)))
}</code></pre>
<h2>扩展阅读</h2>
<p>[《TinyLFU: A Highly E cient Cache Admission Policy》](<a href="https://arxiv.org/abs/1512.00727">[1512.00727] TinyLFU: A Highly Efficient Cache Admission Policy (arxiv.org)</a>)</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/02/02/window-tinylfu%e7%bc%93%e5%ad%98%e5%ae%9e%e7%8e%b0/">Window-TinyLFU缓存实现</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2023/02/02/window-tinylfu%e7%bc%93%e5%ad%98%e5%ae%9e%e7%8e%b0/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Golang实现支持中间件的简易TCP框架</title>
		<link>https://www.crazyfay.com/2022/08/16/golang%e5%ae%9e%e7%8e%b0%e6%94%af%e6%8c%81%e4%b8%ad%e9%97%b4%e4%bb%b6%e7%9a%84%e7%ae%80%e6%98%93tcp%e6%a1%86%e6%9e%b6/</link>
					<comments>https://www.crazyfay.com/2022/08/16/golang%e5%ae%9e%e7%8e%b0%e6%94%af%e6%8c%81%e4%b8%ad%e9%97%b4%e4%bb%b6%e7%9a%84%e7%ae%80%e6%98%93tcp%e6%a1%86%e6%9e%b6/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Tue, 16 Aug 2022 07:48:29 +0000</pubDate>
				<category><![CDATA[代码实战]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[TCP]]></category>
		<category><![CDATA[网络]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=138</guid>

					<description><![CDATA[<p>在golang的标准库中没有为tcp直接提供像http那样简单易用的服务框架，我们不妨自己手动实现一个 主体思 [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/08/16/golang%e5%ae%9e%e7%8e%b0%e6%94%af%e6%8c%81%e4%b8%ad%e9%97%b4%e4%bb%b6%e7%9a%84%e7%ae%80%e6%98%93tcp%e6%a1%86%e6%9e%b6/">Golang实现支持中间件的简易TCP框架</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p>在golang的标准库中没有为tcp直接提供像http那样简单易用的服务框架，我们不妨自己手动实现一个</p>
<h2>主体思路</h2>
<p>我们的实现的主题思路分为以下四个内容</p>
<ol>
<li>监听服务</li>
<li>获取构建新连接对象并设置超时时间及keepalive</li>
<li>设置方法退出时连接关闭</li>
<li>调用回调接口 TcpHandler</li>
</ol>
<h2>主要结构体和接口</h2>
<p>首先是TCPServer的结构体，我们希望用户可以自由构建TcpServer并设置超时时间等自定义选项</p>
<pre><code class="language-go">type TcpServer struct {
   Addr    string
   Handler TCPHandler  &lt;- 对外提供的服务方法接口
   err     error
   BaseCtx context.Context

   WriteTimeout     time.Duration
   ReadTimeout      time.Duration
   KeepAliveTimeout time.Duration

   mu         sync.Mutex
   inShutdown int32
   doneChan   chan struct{}
   l          *onceCloseListener
}</code></pre>
<p>像httpHandler一样，对外提供抽象的ServeTCP方法</p>
<pre><code class="language-go">type TCPHandler interface {
   ServeTCP(ctx context.Context, conn net.Conn)
}</code></pre>
<h2>服务启动方法</h2>
<p>用户可以通过自行构建TcpServer实例再通过ListenAndServe()调用服务，或通过<code>tcp.ListenAndServe(&quot;:8080&quot;, handler)</code> 使用默认的TcpServer实例快速启动服务。</p>
<p>在 <code>ListenAndServe()</code> 方法中，进行参数的校验和初始化操作 </p>
<p><code>Serve(l net.Listener)</code> 方法中，通过 <code>l.Accept()</code> 接收信息，包装接收到的conn并另起一个协程处理服务</p>
<pre><code class="language-go">func ListenAndServe(addr string, handler TCPHandler) error {
    server := &amp;TcpServer{Addr: addr, Handler: handler, doneChan: make(chan struct{})}
    return server.ListenAndServe()
}

func (s *TcpServer) ListenAndServe() error {
   if s.shuttingDown() {
      return ErrServerClosed
   }
   if s.doneChan == nil {
      s.doneChan = make(chan struct{})
   }
   addr := s.Addr
   if addr == &quot;&quot; {
      return errors.New(&quot;need addr&quot;)
   }
   ln, err := net.Listen(&quot;tcp&quot;, addr)
   if err != nil {
      return err
   }
   return s.Serve(tcpKeepAliveListener{
      ln.(*net.TCPListener)})
}

func (s *TcpServer) Serve(l net.Listener) error {
    s.l = &amp;onceCloseListener{Listener: l}
    defer s.l.Close() //执行listener关闭
    if s.BaseCtx == nil {
        s.BaseCtx = context.Background()
    }
    baseCtx := s.BaseCtx
    ctx := context.WithValue(baseCtx, ServerContextKey, s) &lt;- 将TcpServer实例存入context中
    for {
        rw, e := l.Accept()
        if e != nil {
            select {
            case &lt;-s.getDoneChan():
                return ErrServerClosed
            default:
            }
            fmt.Printf(&quot;accept fail, err: %v\n&quot;, e)
            continue
        }
        c := s.newConn(rw)
        go c.serve(ctx)
    }
    return nil
}</code></pre>
<p>包装 <code>net.Conn</code> 为 <code>tcp.conn</code> </p>
<pre><code class="language-go">type conn struct {
    server     *TcpServer   // 反引用TcpServer
    remoteAddr string       // 发送端地址
    rwc        net.Conn
}

func (s *TcpServer) newConn(rwc net.Conn) *conn {
   c := &amp;conn{
      server: s,
      rwc:    rwc,
   }
   // 设置参数
   if d := c.server.ReadTimeout; d != 0 {
      c.rwc.SetReadDeadline(time.Now().Add(d))
   }
   if d := c.server.WriteTimeout; d != 0 {
      c.rwc.SetWriteDeadline(time.Now().Add(d))
   }
   if d := c.server.KeepAliveTimeout; d != 0 {
      if tcpConn, ok := c.rwc.(*net.TCPConn); ok {
         tcpConn.SetKeepAlive(true)
         tcpConn.SetKeepAlivePeriod(d)
      }
   }
   return c
}</code></pre>
<p>由 <code>tcp.conn.Server(ctx)</code> 调用回调函数进行服务处理</p>
<pre><code class="language-go">func (c *conn) serve(ctx context.Context) {
   defer func() {
      if err := recover(); err != nil &amp;&amp; err != ErrAbortHandler {
         const size = 64 &lt;&lt; 10
         buf := make([]byte, size)
         buf = buf[:runtime.Stack(buf, false)]
         fmt.Printf(&quot;tcp: panic serving %v: %v\n%s&quot;, c.remoteAddr, err, buf)
      }
      c.close()
   }()
   c.remoteAddr = c.rwc.RemoteAddr().String()
   ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
   if c.server.Handler == nil {
      panic(&quot;handler empty&quot;)
   }
   c.server.Handler.ServeTCP(ctx, c.rwc)
}</code></pre>
<p>这样，一个简单易用的TCP服务框架就搭建完成了，其中一些<code>close()</code> 等方法在此处没有展示出来，更多详细代码可在我的代码仓库中查看：<a href="https://github.com/Kirov7/fayUtils/net/tcp">https://github.com/Kirov7/fayUtils/net/tcp</a></p>
<h2>扩展中间件的实现</h2>
<p>扩展中间件功能的实现思路</p>
<ul>
<li>方法构建
<ul>
<li>构建中间件URL路由</li>
<li>构建URL的中间件方法数组</li>
<li>使用Use方法整合路由与方法数组</li>
</ul>
</li>
<li>方法调用
<ul>
<li>构建方法请求逻辑</li>
<li>封装TCPHandler接口与TcpServer整合</li>
</ul>
</li>
</ul>
<p><code>TcpSliceRouter.Group(path)</code> 方法，初始化路由分组（默认只能全局）</p>
<pre><code class="language-go">// 创建 Group
func (g *TcpSliceRouter) Group(path string) *TcpSliceGroup {
   if path != &quot;/&quot; {
      panic(&quot;only accept path=/&quot;)
   }
   return &amp;TcpSliceGroup{
      TcpSliceRouter: g,
      path:           path,
   }
}</code></pre>
<p><code>TcpSliceGroup.Use(middlewares ...TcpHandlerFunc)</code> 构造回调方法</p>
<p>调用 <code>Use</code> 方法传入中间件集合，添加到切片 <code>c.handlers</code> 中</p>
<pre><code class="language-go">// 构造回调方法
func (g *TcpSliceGroup) Use(middlewares ...TcpHandlerFunc) *TcpSliceGroup {
   g.handlers = append(g.handlers, middlewares...)
   existsFlag := false
   for _, oldGroup := range g.TcpSliceRouter.groups {
      if oldGroup == g {
         existsFlag = true
      }
   }
   if !existsFlag {
      g.TcpSliceRouter.groups = append(g.TcpSliceRouter.groups, g)
   }
   return g
}</code></pre>
<p>通过 <code>NewTcpSliceRouterHandler</code> 方法传入最后调用的逻辑方法<code>coreFunc</code>并传入已经 <code>Use</code> 了中间件的， <code>TcpSliceRouter</code> </p>
<pre><code class="language-go">func NewTcpSliceRouterHandler(coreFunc func(*TcpSliceRouterContext) tcp_server.TCPHandler, router *TcpSliceRouter) *TcpSliceRouterHandler {
   return &amp;TcpSliceRouterHandler{
      coreFunc: coreFunc,
      router:   router,
   }
}</code></pre>
<p>最终的回调函数 <code>ServeTCP((ctx context.Context, conn net.Conn)</code>，初始化 <code>context</code> 之后将 <code>coreFunc</code> 追加到 <code>c.handlers</code>，重置执行光标，从第一个 <code>c.handlers</code> 开始执行中间件</p>
<pre><code class="language-go">func (w *TcpSliceRouterHandler) ServeTCP(ctx context.Context, conn net.Conn) {
   c := newTcpSliceRouterContext(conn, w.router, ctx)
   c.handlers = append(c.handlers, func(c *TcpSliceRouterContext) {
      w.coreFunc(c).ServeTCP(ctx, conn)
   })
   c.Reset()
   c.Next()
}</code></pre>
<p>在中间件中自行调用<code>Next()</code>、<code>Abort()</code>等中间件逻辑，最后所有中间件执行完毕之后执行 <code>coreFunc</code>（已经被追加到<code>c.handlers</code>的最后位置）</p>
<pre><code class="language-go">// 从最先加入中间件开始回调
func (c *TcpSliceRouterContext) Next() {
   c.index++
   for c.index &lt; int8(len(c.handlers)) {
      c.handlers[c.index](c)
      c.index++
   }
}

// 跳出中间件方法
func (c *TcpSliceRouterContext) Abort() {
    c.index = abortIndex
}</code></pre>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/08/16/golang%e5%ae%9e%e7%8e%b0%e6%94%af%e6%8c%81%e4%b8%ad%e9%97%b4%e4%bb%b6%e7%9a%84%e7%ae%80%e6%98%93tcp%e6%a1%86%e6%9e%b6/">Golang实现支持中间件的简易TCP框架</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2022/08/16/golang%e5%ae%9e%e7%8e%b0%e6%94%af%e6%8c%81%e4%b8%ad%e9%97%b4%e4%bb%b6%e7%9a%84%e7%ae%80%e6%98%93tcp%e6%a1%86%e6%9e%b6/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Golang实现四种负载均衡算法</title>
		<link>https://www.crazyfay.com/2022/08/10/golang%e5%ae%9e%e7%8e%b0%e5%9b%9b%e7%a7%8d%e8%b4%9f%e8%bd%bd%e5%9d%87%e8%a1%a1%e7%ae%97%e6%b3%95/</link>
					<comments>https://www.crazyfay.com/2022/08/10/golang%e5%ae%9e%e7%8e%b0%e5%9b%9b%e7%a7%8d%e8%b4%9f%e8%bd%bd%e5%9d%87%e8%a1%a1%e7%ae%97%e6%b3%95/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Wed, 10 Aug 2022 02:56:32 +0000</pubDate>
				<category><![CDATA[代码实战]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[负载均衡]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=158</guid>

					<description><![CDATA[<p>负载均衡算法实现 在这篇文章中，我将实现最常见的四种负载均衡算法，即随机负载、轮询负载、加权负载和一致性has [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/08/10/golang%e5%ae%9e%e7%8e%b0%e5%9b%9b%e7%a7%8d%e8%b4%9f%e8%bd%bd%e5%9d%87%e8%a1%a1%e7%ae%97%e6%b3%95/">Golang实现四种负载均衡算法</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<h1>负载均衡算法实现</h1>
<p>在这篇文章中，我将实现最常见的四种负载均衡算法，即随机负载、轮询负载、加权负载和一致性hash负载</p>
<h2>随机负载</h2>
<p>随机挑选目标服务器ip</p>
<pre><code class="language-go">type RandomBalance struct {
   curIndex int
   rss      []string
}

// 添加新的服务ip
func (r *RandomBalance) Add(params ...string) error {
   if len(params) == 0 {
      return errors.New(&quot;param len 1 at least&quot;)
   }
   addr := params[0]
   r.rss = append(r.rss, addr)
   return nil
}

// 采用rand.Intn随机取一个服务ip
func (r *RandomBalance) Next() string {
   if len(r.rss) == 0 {
      return &quot;&quot;
   }
   r.curIndex = rand.Intn(len(r.rss))
   return r.rss[r.curIndex]
}

func (r *RandomBalance) Get(key string) (string, error) {
   return r.Next(), nil
}</code></pre>
<h2>轮询负载</h2>
<p>ABC三台服务器,A B C A B C依次轮询</p>
<pre><code class="language-go">type RoundRobinBalance struct {
   curIndex int
   rss      []string
}

func (r *RoundRobinBalance) Add(params ...string) error {
   if len(params) == 0 {
      return errors.New(&quot;param len 1 at least&quot;)
   }
   addr := params[0]
   r.rss = append(r.rss, addr)
   return nil
}

func (r *RoundRobinBalance) Next() string {
   if len(r.rss) == 0 {
      return &quot;&quot;
   }
   lens := len(r.rss)
   if r.curIndex &gt;= lens {
      r.curIndex = 0
   }
    // 保存一个服务ip的游标
   curAddr := r.rss[r.curIndex]
   r.curIndex = (r.curIndex + 1) % lens
   return curAddr
}

func (r *RoundRobinBalance) Get(key string) (string, error) {
   return r.Next(), nil
}</code></pre>
<h2>加权负载</h2>
<p>给目标设置访问权重,按照权重轮询</p>
<p><strong>nginx的加权负载均衡策略</strong></p>
<p>计算策略：</p>
<ol>
<li>currentWeight += effectiveWeight</li>
<li>选出最大的currentWeight节点作为选中节点</li>
<li>currentWeight -= totalWeight
<ul>
<li>其中effectiveWeight的值不变，只有当服务异常的时候减少</li>
</ul>
</li>
</ol>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20221123201123507.png" alt="image-20221123201123507" /></p>
<pre><code class="language-go">type WeightRoundRobinBalance struct {
   curIndex int
   rss      []*WeightNode
   rsw      []int
}

type WeightNode struct {
   addr            string
   weight          int //权重值
   currentWeight   int //节点当前权重
   effectiveWeight int //有效权重
}

func (r *WeightRoundRobinBalance) Add(params ...string) error {
   if len(params) != 2 {
      return errors.New(&quot;param len need 2&quot;)
   }
   parInt, err := strconv.ParseInt(params[1], 10, 64)
   if err != nil {
      return err
   }
   node := &amp;WeightNode{addr: params[0], weight: int(parInt)}
   node.effectiveWeight = node.weight
   r.rss = append(r.rss, node)
   return nil
}

// Next 参考了 nginx 的加权负载均衡的策略
func (r *WeightRoundRobinBalance) Next() string {
   total := 0
   var best *WeightNode
   for i := 0; i &lt; len(r.rss); i++ {
      w := r.rss[i]
      //step 1 统计所有有效权重之和
      total += w.effectiveWeight

      //step 2 变更节点临时权重为的节点临时权重+节点有效权重
      w.currentWeight += w.effectiveWeight

      //step 3 有效权重默认与权重相同，通讯异常时-1, 通讯成功+1，直到恢复到weight大小
      if w.effectiveWeight &lt; w.weight {
         w.effectiveWeight++
      }
      //step 4 选择最大临时权重点节点
      if best == nil || w.currentWeight &gt; best.currentWeight {
         best = w
      }
   }
   if best == nil {
      return &quot;&quot;
   }
   //step 5 变更临时权重为 临时权重-有效权重之和
   best.currentWeight -= total
   return best.addr
}

func (r *WeightRoundRobinBalance) Get(key string) (string, error) {
   return r.Next(), nil
}</code></pre>
<h2>一致性hash负载</h2>
<p>请求固定URL访问指定IP</p>
<pre><code class="language-go">type Hash func(data []byte) uint32

type UInt32Slice []uint32

func (s UInt32Slice) Len() int {
   return len(s)
}

func (s UInt32Slice) Less(i, j int) bool {
   return s[i] &lt; s[j]
}

func (s UInt32Slice) Swap(i, j int) {
   s[i], s[j] = s[j], s[i]
}

type ConsistentHashBanlance struct {
   mux      sync.RWMutex
   hash     Hash
   replicas int               // 复制因子
   keys     UInt32Slice       // 已排序的节点hash切片
   hashMap  map[uint32]string // 节点哈希和Key的map,键是hash值，值是节点key
}

func NewConsistentHashBanlance(replicas int, fn Hash) *ConsistentHashBanlance {
   m := &amp;ConsistentHashBanlance{
      replicas: replicas,
      hash:     fn,
      hashMap:  make(map[uint32]string),
   }
   if m.hash == nil {
      //最多32位,保证是一个2^32-1环
      m.hash = crc32.ChecksumIEEE
   }
   return m
}

// 验证是否为空
func (c *ConsistentHashBanlance) IsEmpty() bool {
   return len(c.keys) == 0
}

// Add 方法用来添加缓存节点，参数为节点key，比如使用IP
func (c *ConsistentHashBanlance) Add(params ...string) error {
   if len(params) == 0 {
      return errors.New(&quot;param len 1 at least&quot;)
   }
   addr := params[0]
   c.mux.Lock()
   defer c.mux.Unlock()
   // 结合复制因子计算所有虚拟节点的hash值，并存入m.keys中，同时在m.hashMap中保存哈希值和key的映射
   for i := 0; i &lt; c.replicas; i++ {
      hash := c.hash([]byte(strconv.Itoa(i) + addr))
      c.keys = append(c.keys, hash)
      c.hashMap[hash] = addr
   }
   // 对所有虚拟节点的哈希值进行排序，方便之后进行二分查找
   sort.Sort(c.keys)
   return nil
}

// Get 方法根据给定的对象获取最靠近它的那个节点
func (c *ConsistentHashBanlance) Get(key string) (string, error) {
   if c.IsEmpty() {
      return &quot;&quot;, errors.New(&quot;node is empty&quot;)
   }
   hash := c.hash([]byte(key))

   // 通过二分查找获取最优节点，第一个&quot;服务器hash&quot;值大于&quot;数据hash&quot;值的就是最优&quot;服务器节点&quot;
   idx := sort.Search(len(c.keys), func(i int) bool { return c.keys[i] &gt;= hash })

   // 如果查找结果 大于 服务器节点哈希数组的最大索引，表示此时该对象哈希值位于最后一个节点之后，那么放入第一个节点中
   if idx == len(c.keys) {
      idx = 0
   }
   c.mux.RLock()
   defer c.mux.RUnlock()
   return c.hashMap[c.keys[idx]], nil
}

func (c *ConsistentHashBanlance) SetConf(conf LoadBalanceConf) {
   c.conf = conf
}</code></pre>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/08/10/golang%e5%ae%9e%e7%8e%b0%e5%9b%9b%e7%a7%8d%e8%b4%9f%e8%bd%bd%e5%9d%87%e8%a1%a1%e7%ae%97%e6%b3%95/">Golang实现四种负载均衡算法</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2022/08/10/golang%e5%ae%9e%e7%8e%b0%e5%9b%9b%e7%a7%8d%e8%b4%9f%e8%bd%bd%e5%9d%87%e8%a1%a1%e7%ae%97%e6%b3%95/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
