程序员修仙之路--优雅快速的统计千万级别uv

640?wx_fmt=gif

菜菜,咱们网站现在有多少PV和UV了?

640?wx_fmt=png640?wx_fmt=gif

Y总,咱们没有统计pv和uv的系统,预估大约有一千万uv吧

640?wx_fmt=png640?wx_fmt=jpeg

写一个统计uv和pv的系统吧

640?wx_fmt=png640?wx_fmt=gif

网上有现成的,直接接入一个不行吗?

640?wx_fmt=png640?wx_fmt=jpeg

别人的不太放心,毕竟自己写的,自己拥有主动权。给你两天时间,系统性能不要太差呀

640?wx_fmt=png640?wx_fmt=gif

好吧~~~

640?wx_fmt=png640?wx_fmt=jpeg定义PV是page view的缩写,即页面浏览量,通常是衡量一个网络新闻频道或网站甚至一条网络新闻的主要指标。网页浏览数是评价网站流量最常用的指标之一,简称为PV


UV是unique visitor的简写,是指通过互联网访问、浏览这个网页的自然人。640?wx_fmt=gif

通过以上的概念,可以清晰的看出pv是比较好设计的,网站的每一次被访问,pv都会增加,但是uv就不一定会增加了,uv本质上记录的是按照某个标准划分的自然人,这个标准其实我们可以自己去定义,比如:可以定义同一个IP的访问者为同一个UV,这也是最常见的uv定义之一,另外还有根据cookie定义等等。无论是pv还是uv,都需要一个时间段来加以描述,平时我们所说的pv,uv数量指的都是24小时之内(一个自然日)的数据。


pv相比较uv来说,技术上比较容易一些,今天咱们就来说一说uv的统计,为什么说uv的统计相对来说比较难呢,因为uv涉及到同一个标准下的自然人的去重,尤其是一个uv千万级别的网站,设计一个好的uv统计系统也许并非想象的那么容易。


那我们就来设计一个以一个自然日为时间段的uv统计系统,一个自然人(uv)的定义为同一个来源IP(当然你也可以自定义其他标准),数据量级别假设为每日千万uv的量级。

640?wx_fmt=png

注意:今天我们讨论的重点是获取到自然人定义的信息之后如何设计uv统计系统,并非是如何获取自然人的定义。uv系统的设计并非想象的那么简单,因为uv可能随着网站的营销策略会出现瞬间大流量,比如网站举办了一个秒杀活动。

640?wx_fmt=png基于DB方案640?wx_fmt=gif

服务端编程有一句名言曰:没有一个表解决不了的功能,如果有那就两个表三个表。一个uv统计系统确实可以基于数据库来实现,而且也不复杂,uv统计的记录表可以类似如下(不要太纠结以下表设计是否合理):

字段类型描述
IPvarchar(30)客户端来源ip
DayIDint时间的简写,例如 20190629
其他字段int其他字段描述


640?wx_fmt=gif

当一个请求到达服务器,服务端每次需要查询一次数据库是否有当前IP和当前时间的访问记录,如果有,则说明是同一个uv,如果没有,则说明是新的uv记录,插入数据库。当然以上两步也可以写到一个sql语句中:

if exists( select 1 from table where ip='ip' and dayid=dayid )  Begin    return 0  Endelse  Begin     insert into table .......  End1 from table where ip='ip' and dayid=dayid )
  Begin
    return 0
  End
else
  Begin
     insert into table .......
  End
640?wx_fmt=gif

所有基于数据库的解决方案,在数据量大的情况下几乎都更容易出现瓶颈。面对每天千万级别的uv统计,基于数据库的这种方案也许并不是最优的。

优化方案640?wx_fmt=gif

面对每一个系统的设计,我们都应该沉下心来思考具体的业务。至于uv统计这个业务有几个特点:

1. 每次请求都需要判断是否已经存在相同的uv记录

2. 持久化uv数据不能影响正常的业务

3. uv数据的准确性可以忍受一定程度的误差

哈希表

640?wx_fmt=gif

基于数据库的方案中,在大数据量的情况下,性能的瓶颈引发原因之一就是:判断是否已经存在相同记录,所以要优化这个系统,肯定首先是要优化这个步骤。根据菜菜以前的文章,是否可以想到解决这个问题的数据结构,对,就是哈希表。哈希表根据key来查找value的时间复杂度为O(1)常数级别,可以完美的解决查找相同记录的操作瓶颈。

也许在uv数据量比较小的时候,哈希表也许是个不错的选择,但是面对千万级别的uv数据量,哈希表的哈希冲突和扩容,以及哈希表占用的内存也许并不是好的选择了。假设哈希表的每个key和value  占用10字节,1千万的uv数据大约占用 100M,对于现代计算机来说,100M其实不算大,但是有没有更好的方案呢?

优化哈希表

640?wx_fmt=gif

基于哈希表的方案,在千万级别数据量的情况下,只能算是勉强应付,如果是10亿的数据量呢?那有没有更好的办法搞定10亿级数据量的uv统计呢?这里抛开持久化数据,因为持久化设计到数据库的分表分库等优化策略了,咱们以后再谈。有没有更好的办法去快速判断在10亿级别的uv中某条记录是否存在呢?

为了尽量缩小使用的内存,我们可以这样设计,可以预先分配bit类型的数组,数组的大小是统计的最大数据量的一个倍数,这个倍数可以自定义调整。现在假设系统的uv最大数据量为1千万,系统可以预先分配一个长度为5千万的bit数组,bit占用的内存最小,只占用一位。按照一个哈希冲突比较小的哈希函数计算每一个数据的哈希值,并设置bit数组相应哈希值位置的值为1。由于哈希函数都有冲突,有可能不同的数据会出现相同的哈希值,出现误判,但是我们可以用多个不同的哈希函数来计算同一个数据,来产生不同的哈希值,同时把这多个哈希值的数组位置都设置为1,从而大大减少了误判率,刚才新建的数组为最大数据量的一个倍数也是为了减小冲突的一种方式(容量越大,冲突越小)。当一个1千万的uv数据量级,5千万的bit数组占用内存才几十M而已,比哈希表要小很多,在10亿级别下内存占用差距将会更大。


以下为代码示例:

class BloomFilter    {        BitArray container = null;      public BloomFilter(int length)        {            container = new BitArray(length);        }        public void Set(string key)        {            var h1 = Hash1(key);            var h2 = Hash2(key);            var h3 = Hash3(key);            var h4 = Hash4(key);            container[h1] = true;            container[h2] = true;            container[h3] = true;            container[h4] = true;        }        public bool Get(string key)        {            var h1 = Hash1(key);            var h2 = Hash2(key);            var h3 = Hash3(key);            var h4 = Hash4(key);            return container[h1] && container[h2] && container[h3] && container[h4];        }        //模拟哈希函数1         int Hash1(string key)        {            int hash = 5381;            int i;            int count;            char[] bitarray = key.ToCharArray();            count = bitarray.Length;            while (count > 0)            {                hash += (hash << 5) + (bitarray[bitarray.Length - count]);                count--;            }            return (hash & 0x7FFFFFFF) % container.Length;        }         int Hash2(string key)        {            int seed = 131; // 31 131 1313 13131 131313 etc..            int hash = 0;            int count;            char[] bitarray = (key+"key2").ToCharArray();            count = bitarray.Length;            while (count > 0)            {                hash = hash * seed + (bitarray[bitarray.Length - count]);                count--;            }            return (hash & 0x7FFFFFFF)% container.Length;        }         int Hash3(string key)        {            int hash = 0;            int i;            int count;            char[] bitarray = (key + "keykey3").ToCharArray();            count = bitarray.Length;            for (i = 0; i < count; i++)            {                if ((i & 1) == 0)                {                    hash ^= ((hash << 7) ^ (bitarray[i]) ^ (hash >> 3));                }                else                {                    hash ^= (~((hash << 11) ^ (bitarray[i]) ^ (hash >> 5)));                }                count--;            }            return (hash & 0x7FFFFFFF) % container.Length;        }        int Hash4(string key)        {            int hash = 5381;            int i;            int count;            char[] bitarray = (key + "keykeyke4").ToCharArray();            count = bitarray.Length;            while (count > 0)            {                hash += (hash << 5) + (bitarray[bitarray.Length - count]);                count--;            }            return (hash & 0x7FFFFFFF) % container.Length;        }    }BloomFilter
    {
        BitArray container = null;
      public BloomFilter(int length)
        
{
            container = new BitArray(length);
        }

        public void Set(string key)
        
{
            var h1 = Hash1(key);
            var h2 = Hash2(key);
            var h3 = Hash3(key);
            var h4 = Hash4(key);
            container[h1] = true;
            container[h2] = true;
            container[h3] = true;
            container[h4] = true;

        }
        public bool Get(string key)
        
{
            var h1 = Hash1(key);
            var h2 = Hash2(key);
            var h3 = Hash3(key);
            var h4 = Hash4(key);

            return container[h1] && container[h2] && container[h3] && container[h4];
        }

        //模拟哈希函数1
         int Hash1(string key)
        
{
            int hash = 5381;
            int i;
            int count;
            char[] bitarray = key.ToCharArray();
            count = bitarray.Length;
            while (count > 0)
            {
                hash += (hash << 5) + (bitarray[bitarray.Length - count]);
                count--;
            }
            return (hash & 0x7FFFFFFF) % container.Length;

        }
         int Hash2(string key)
        
{
            int seed = 131// 31 131 1313 13131 131313 etc..
            int hash = 0;
            int count;
            char[] bitarray = (key+"key2").ToCharArray();
            count = bitarray.Length;
            while (count > 0)
            {
                hash = hash * seed + (bitarray[bitarray.Length - count]);
                count--;
            }

            return (hash & 0x7FFFFFFF)% container.Length;
        }
         int Hash3(string key)
        
{
            int hash = 0;
            int i;
            int count;
            char[] bitarray = (key + "keykey3").ToCharArray();
            count = bitarray.Length;
            for (i = 0; i < count; i++)
            {
                if ((i & 1) == 0)
                {
                    hash ^= ((hash << 7) ^ (bitarray[i]) ^ (hash >> 3));
                }
                else
                {
                    hash ^= (~((hash << 11) ^ (bitarray[i]) ^ (hash >> 5)));
                }
                count--;
            }

            return (hash & 0x7FFFFFFF) % container.Length;

        }
        int Hash4(string key)
        
{
            int hash = 5381;
            int i;
            int count;
            char[] bitarray = (key + "keykeyke4").ToCharArray();
            count = bitarray.Length;
            while (count > 0)
            {
                hash += (hash << 5) + (bitarray[bitarray.Length - count]);
                count--;
            }
            return (hash & 0x7FFFFFFF) % container.Length;
        }
    }
640?wx_fmt=gif

测试程序为:

BloomFilter bf = new BloomFilter(200000000);            int exsitNumber = 0;            int noExsitNumber = 0;            for (int i=0;i < 10000000; i++)            {                string key = $"ip_{i}";                var isExsit= bf.Get(key);                if (isExsit)                {                    exsitNumber += 1;                }                else                {                    bf.Set(key);                    noExsitNumber += 1;                }            }            Console.WriteLine($"判断存在的数据量:{exsitNumber}");            Console.WriteLine($"判断不存在的数据量:{noExsitNumber}");200000000);
            int exsitNumber = 0;
            int noExsitNumber = 0;

            for (int i=0;i < 10000000; i++)
            {
                string key = $"ip_{i}";
                var isExsit= bf.Get(key);
                if (isExsit)
                {
                    exsitNumber += 1;
                }
                else
                {
                    bf.Set(key);
                    noExsitNumber += 1;
                }
            }
            Console.WriteLine($"判断存在的数据量:{exsitNumber}");
            Console.WriteLine($"判断不存在的数据量:{noExsitNumber}");
640?wx_fmt=gif

 测试结果:

判断存在的数据量:7017判断不存在的数据量:9992983


640?wx_fmt=png640?wx_fmt=gif

占用内存40M,误判率不到 千分之1,在这个业务场景下在可接受范围之内。在真正的业务当中,系统并不会在启动之初就分配这么大的bit数组,而是随着冲突增多慢慢扩容到一定容量的。

异步优化

640?wx_fmt=gif

当判断一个数据是否已经存在这个过程解决之后,下一个步骤就是把数据持久化到DB,如果数据量较大或者瞬间数据量较大,可以考虑使用mq或者读写IO比较大的NOSql来代替直接插入关系型数据库。

640?wx_fmt=png640?wx_fmt=gif

思路一转,整个的uv流程其实也都可以异步化,而且也推荐这么做。

640?wx_fmt=png640?wx_fmt=png


640?wx_fmt=gif程序员过关斩将--你为什么还在用存储过程?程序员过关斩将--小小的分页引发的加班血案●程序员修神之路--问世间异步为何物?●程序员修神之路--提高网站的吞吐量?●程序员修神之路--?分布式高并发下Actor模型如此优秀?●程序员过关斩将--论商品促销代码的优雅性●程序员过关斩将--你的面向接口编程一定对吗?●程序员修神之路--高并发下为什么更喜欢进程内缓存●程序员修神之路--高并发优雅的做限流640?wx_fmt=gif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/315183.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

P3391 【模板】文艺平衡树 fhq-treap 模板

传送门 文章目录题意&#xff1a;思路&#xff1a;题意&#xff1a; 思路&#xff1a; 这是学splaysplaysplay的时候的一个模板题&#xff0c;之前学过fhq−treapfhq-treapfhq−treap&#xff0c;但是没怎么用他做过题&#xff0c;今天一做还发现不少问题&#xff0c;真是眼高…

123. 买卖股票的最3佳时机 III

思路&#xff1a;动态规划&#xff0c; dp1是当前第一次买入的最大值 dp2是当前第一次卖出的最大值 dp3是当前第二次买入的最大值 dp4是当前第二次卖出的最大值 转移看代码&#xff1a; class Solution { public:int maxProfit(vector<int>& prices) {int dp1-…

微软正式开源WSL 2的内核源码

微软在今年5月举办的 Build 2019 上宣布了第二代 Windows 的 Linux 子系统 —— WSL 2。与第一代相比&#xff0c;WSL 2 重新设计了架构&#xff0c;使用真正的 Linux 内核&#xff0c;支持在 Windows 上运行 ELF64 Linux 二进制文件。按照计划&#xff0c;WSL 2 的初始版本已于…

【NOI2016】优秀的拆分【后缀数组】【ST表】【关键点】【调和级数复杂度】【差分】

传送门 题意&#xff1a;如果一个字符串可以拆分为AABB的形式&#xff0c;其中A和B是任意非空字符串&#xff0c;则我们这种拆分是优秀的。求给定串的所有子串的拆分方案数之和。 N≤30000N \leq30000N≤30000 本来是个神仙题 但明明一个O(nlogn)O(nlogn)O(nlogn)的题为啥只…

剑指 Offer 43. 1~n 整数中 1 出现的次数(数位dp)

思路&#xff1a;就是数位dp&#xff0c;dp[idx][sum][limit]代表&#xff0c;到idx位&#xff0c;前面有sum个0&#xff0c;有没有limit限制&#xff1b; class Solution { public:int dp[20][50][2];int len; int pos[20];int countDigitOne(int n) {for(int i0;i<15;i)f…

Windows新终端中玩转ASCII和Emoji游戏的正确姿势

前一段时间&#xff0c;我搬运了几个Windows Terminal中玩游戏的视频.Windows Terminal - 动图GIF作背景图Windows Terminal - 母牛说HiWindows Terminal - 字符水族箱今天我来给大家展示一下具体的玩法~Emoji版双人碰碰球目前有个现成的 .NET core 项目可以直接用&#xff0c;…

剑指 Offer 38. 字符串的排列(有重复元素的排列)

思路&#xff1a;dfs 暴搜 class Solution { public:vector<string> permutation(string s) {vector<string> a;function<bool(char,int,int)> check[&](char x,int st,int idx){if(st>idx) return false;for(int ist;i<idx;i) if(s[i]x) return …

nowcoder Forsaken的数列 fhq-treap

传送门 文章目录题意&#xff1a;思路&#xff1a;题意&#xff1a; 思路&#xff1a; 很明显的数据结构了&#xff0c;splaysplaysplay当然能写&#xff0c;但是fhq−treapfhq-treapfhq−treap更加简洁易懂。 考虑第一个操作&#xff0c;无非就是分裂出[1,pos−1][1,pos-1][1…

我与微软的不解之缘 - 我的Insider Dev Tour 2019讲师之旅

作者&#xff1a;Lamond Lu大家好&#xff0c;我是陆楠&#xff0c;来自北京盛安德科技发展有限公司青岛分公司&#xff0c;今年非常有幸作为讲师参加了微软Insider Dev Tour烟台站的活动&#xff0c;我主讲了如何使用最新的微软开发工具开发调试NodeJS项目。与微软结缘与大多数…

.NET Core 3.0之深入源码理解Kestrel的集成与应用(一)

写在前面ASP.NET Core 的 Web 服务器默认采用Kestrel&#xff0c;这是一个基于libuv(一个跨平台的基于Node.js的异步I/O库)的跨平台、轻量级的Web服务器。在开始之前&#xff0c;先回顾一下.NET Core 3.0默认的main()方法模板中&#xff0c;我们会调用Host.CreateDefaultBuilde…

P2596 [ZJOI2006]书架 无旋treap 按照排名分裂

传送门 文章目录题意&#xff1a;思路&#xff1a;题意&#xff1a; 实现如下操作&#xff1a; 思路&#xff1a; fhq-treap板子辣&#xff0c;不要被他的编号误导了&#xff0c;我们还是需要按照排名来分裂&#xff0c;从上到下标号1−n1-n1−n即可&#xff0c;注意在按照…

【水】几个网络流图论模型的记录

DAG相关 最小路径覆盖 定义&#xff1a;最少不重路径覆盖DAG 初始时每个点是独立的 之后每次加一条边把两个点连到一起 因为只能用一次&#xff0c;所以是个最大匹配 最小路径覆盖N-拆点后最大匹配\text{最小路径覆盖N-拆点后最大匹配}最小路径覆盖N-拆点后最大匹配 最小链…

surging 微服务引擎 2.0 会有多少惊喜?

surging 微服务引擎从2017年6月至今已经有两年的时间&#xff0c;这两年时间有多家公司使用surging 服务引擎&#xff0c;并且有公司搭建了CI/CD&#xff0c;并且使用了k8s 集群&#xff0c;这里我可以说下几家公司的服务搭建情况&#xff0c;公司名不便透露&#xff0c;我们就…

Codeforces Round #630 (Div. 2) E. Height All the Same 排列组合

传送门 文章目录题意&#xff1a;思路&#xff1a;题意&#xff1a; 思路&#xff1a; 由于n,mn,mn,m都很大&#xff0c;不难猜到这是一个公式题。 首先化简题目中的两个操作&#xff0c;第二个操作就是可以让奇偶性相同的位置的高度相同。第一个操作虽然是改变相邻两个的奇偶…

对产品质量的一点思考

不管是做产品还是做项目&#xff0c;也不管是采用瀑布模型还是敏捷开发&#xff0c;我们都有一个终极目标&#xff0c;就是能按时交付质量可靠的功能&#xff0c;其中质量尤为重要。本文是我对产品质量的一点思考&#xff0c;如果您所在的团队代码质量很高&#xff0c;很少出BU…

正确使用AES对称加密

正确使用AES对称加密经常我看到项目中有人使用了对称加密算法&#xff0c;用来加密客户或项目传输中的部分数据。但我注意到开发 人员由于不熟悉原理&#xff0c;或者简单复制网上的代码示例&#xff0c;有导致代码存在安全风险。我经常遇到的问题&#xff0c;有如下&#xff1…

Codeforces Round #709 (Div. 1) C. Skyline Photo dp + 单调栈优化

传送门 文章目录题意&#xff1a;思路&#xff1a;题意&#xff1a; 思路&#xff1a; 首先一个非常明显的dpdpdp式子就是f[i]max(f[j]val(j1,i))f[i]max(f[j]val(j1,i))f[i]max(f[j]val(j1,i))&#xff0c;其中val(j1,i)val(j1,i)val(j1,i)是[j1,i][j1,i][j1,i]中高度最小的美…

「数据分析」Sqlserver中的窗口函数的精彩应用之数据差距与数据岛(含答案)...

上一篇介绍过数据差距与数据岛的背景&#xff0c;这里不再赘述&#xff0c;请翻阅上一文。此篇在Sqlserver上给大家演示1000万条记录的计算性能。测试电脑软硬件说明一般般的笔记本电脑&#xff0c;2017年7月&#xff0c;价格&#xff1a;4500。电脑配置数据构造1000万行数据&a…

Codeforces Round #631 (Div. 2) C. Dreamoon Likes Coloring 构造

传送门 文章目录题意&#xff1a;思路&#xff1a;题意&#xff1a; 思路&#xff1a; 针灸思维不行&#xff0c;数据结构来凑呗。 一开始做的时候想简单了&#xff0c;一直wawawa&#xff0c;后来想到了hackhackhack样例&#xff0c;开始换思路构造&#xff0c;结果死活想不到…

用象棋的思维趣说IT人的职业发展和钱途

最近我花了不少功夫在学习象棋&#xff0c;也学习了王天一等高手的棋路&#xff0c;感觉IT人的职业和下棋一样&#xff0c;往好了讲&#xff0c;争主动权争实惠只争朝夕&#xff0c;往坏了讲&#xff0c;一步走错得用多步来弥补&#xff0c;如果错误太大未必能弥补回来。在本文…