最近访问用户问题的实现

前几天有个朋友提了一个问题:

应用平台需要统计最近访问的20个用户的信息。

我第一个想到的是用memcache之类的专用的缓冲,以用户名为键,以最后访问时间为值,如 果用户访问比较均匀,限定一个合适的 超时值,查询的时候遍历过滤就好了,虽然不是很 精确,但是实现起来够简单。

答:不可以,领导要求只在数据库端解决。

环境准备

提问的朋友使用的是 MySQL ,这里我用一个 PostgreSQL 8.4(Enterprise DB Postgres Plus Stand Server 8.4) 建立实验环境。操作系统是 Windows 7 RC。

客户端是 Emacs sql-postgres,/timing on。

使用 explain analyze 分析性能消耗。

尝试

那么,最简单的应该是在日志表上查询:

select 
 username, max
(logintime) from
 log group
 by
 username order
 by
 2 desc
 limit
 20;

这里假定的是会话表(log)中有名为 username 的用户标识列,名为 logintime 的时间戳字 段。

但是显然这个查询的性能并不好,它要遍历整个日志表。

首先,我们给日志表加一个聚集索引,使它按插入时间倒排(显然这对插入效率不利)。

create


 index idx_logintime on


 login_session(logintime desc


);

alter table login_session cluster on idx_logintime;

然后,再进行查询,可见效率有微弱提升。在我本机的 Postgres 8.4 上,通过 explain analyze 可见有不到百分之二的效率上升。我甚至怀疑这仅仅是源自一些随机事件的影响。

换一个角度分析,只要 select max ... group by ... 的模式不改变,就很难有根本的性 能提升,最好的办法仍然是缓存。

改进

有个简单的办法,可以在PG数据库中制造一个缓存表:

  • 首先,直接建立一个 user->timstamp 键值对表。如果用户量不大,可以直接这样:
  create
 table
 sessions
(username text, seqs serial, logintime timestamp
, primary
 key
 (username));
  • 然后创建一个触发器函数

create or replace function on_log () returns trigger as $$

begin

    if exists (select * from sessions where username=NEW.username) then

        update sessions set seqs = nextval('sessions_seqs_seq' ) where username=NEW.username;

      else
    
        insert
     into
     sessions(username, logintime) select
     NEW.username, NEW.logintime;
    end if;
    if (select count (*) from sessions)> 20 then delete from sessions
    where seqs < (select min (seqs) from (select seqs from sessions order by seqs desc limit 20) as t);
    end if;
    return NEW ;
    end ;
    $$ language plpgsql;

    create trigger onlog after insert on log for each row execute procedure on_log ();

    OK,这样一来,经过反复测试,在我的笔记本上,插入速度基本没有降低(约 2%~ 5%), 而查询“最新的20个用户”这一操作,速度提升了一百七十余倍。几乎可以视作是一个数据库 端的队列缓存了。这个数值是基于 log 表中有五万条数据,在实际应用中日志表十万以上 (按每日切分导出)比比皆是,性能差距会更为显著。

    分析

    “最新的 20 个用户”,这一问题,与“最近5分钟内的用户”此类问题的区别在于,它不能通 过获取当前时间,简单回溯得到有效数据区间。如何避免遍历整个日志表是一个关键问题———— 现代的在线服务系统每日访问日志量动辄数十万,几百上千万的也不罕见,第一种解法明显 不能满足。相比之下,第二种方案不需要干涉日志表的存储排序,不需要建立多余的索引。 通常我们讲触发器速度比较慢,但具体到这里,只是却是一个比较快速高效的实现方案。

    如果用户量不大,在几千以内,为用户表建立一个最后登录时间字段,绑定 null last 索 引(可以加上FASTUPDATE=ON),然后利用基本的order by limit就可以得到比较慢意的效 果了。但是对于大型SNS等应用,数十上百万用户也是有可能的(甚至大型企业内部应用, 数万乃至上十万用户的系统也不在少数)。此时,第二种解决方案就理想的多。虽然看起来 在session表上反复进行计算和写入,但是由于session表的数据量非常小,永远只有几十 条,所以计算速度很快。如果对 log 表进行恰当的分区,同时将sessions表存储到另一个 区域(甚至直接缓存到内存),那么对于海量数据,也会有一个稳定的,良好的性能表现。

    更进一步的解决方法,则应该是在数据库或应用层服务器环境,建立一个内存中的队列,来 记录这个数据。只要注意并发写入的问题,就可以得到很好的性能。这方面Haskell的STM机 制、我之前用在MSG.Summoner.Trac的旋转锁机制、都可以比较漂亮的解决这一问题。

    优化

    sessions 表的读写都非常频繁,通常有比较多的 update,,但是数据量基本恒定。应该积极的执行 vacuum。

    可以为日志表添加一个 uuid 字段,以便在数据量上升时按 hash 分表。这样可以获得更好 的写入性能。

    可以将 session 的主键索引设为 FASTUPDATE=ON 。

    触发器中设定

    SET LOCAL synchronous_commit TO OFF;

    打开 WAL 异步事务提交,可以进一步提高并发写入速度。

    更进一步的,可以写一个常驻的守护进程,用应用语言建立一个队列,将这一需求建立为独 立的服务。这个服务可以通过 pl 嵌入语言与数据库联接,也可以直接联接到应用层,有一 些应用层架构依赖于并发的多 fastcgi 实例,此时要注意并发访问冲突和数据同步的问题。

    总结

    之所以出现这样一个困扰开发人员的问题,根本在于项目领导不提供,也不认同使用灵活、 开放的思维方式解决此问题。服务项目的架构是一个系统工程,类似这样的问题,完全可以 用更开阔的眼光去寻找出路。建立一个简单的缓存队列实例,用perl或python,只需要数十 行。结合Postgres的plperl或plpython等嵌入脚本,可以非常简单的达到目的。甚至不用担 心使用多实例 fastcgi 时,多个应用服务进程争用缓存队列I/O的问题。

    即使从方案二出发,通过利用服务器环境的资源,也可以做出更多优化。如把“删除第20之 后的旧数据”这部分脚本,移到 crontab 中,与vacuum 结合执行。会得到更好的效率。

    找我请教的这位朋友,使用的不是Postgres,而是MySQL,相对来说很多PG的服务端编程技 巧难以照搬,再加上团队管理的政治问题,我也只有祝他好运了。

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包

    打赏作者

    ccat

    你的鼓励将是我创作的最大动力

    ¥1 ¥2 ¥4 ¥6 ¥10 ¥20
    扫码支付:¥1
    获取中
    扫码支付

    您的余额不足,请更换扫码支付或充值

    打赏作者

    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值