<?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>Bitmap归档 - 枫阿雨&#039;s blog</title>
	<atom:link href="https://www.crazyfay.com/tag/bitmap/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.crazyfay.com/tag/bitmap/</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>Bitmap归档 - 枫阿雨&#039;s blog</title>
	<link>https://www.crazyfay.com/tag/bitmap/</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>
	</channel>
</rss>
