最近工作总结(50)

2021/07/17 Work

使用hash大幅度提高Redis value内存利用率

如果把要使用的 redis 数据都集中到一起,集中存放,则 value 的大小会远大于 key 和其他内存结构的大小,从而使内存利用率达到 50%~99%。然而此方案也有弊端:如果只想取某个子模块的数据也必须把整体数据都拉下来,无状态化的情况下本来就会频繁读写数据,此方案将显著增加 redis 的CPU压力。 redis 的 hash 类型既可以把数据集中存放,也支持 key 分开读写

返回结构体 还是结构体指针

1MiB字节以下,返回结构体都更有优势。那返回指针的方式是不是没用了呢?也不是,如果你最终的结构体,就是要存放到堆里,比如要存放到全局的map里,那返回指针优势就更大些,因为其省去了返回结构体时的拷贝操作

返回结构体指针性能会较差的原因是:结构体指针会分配在堆上,分配堆的函数比分配在栈上更复杂,所以更耗时。分配在堆上需要GC来回收

rz 命令上传文件到跳板机

  • 登入跳板机
  • 输入 rz命令
  • 弹出选择文件框,选择要上传的文件
  • 极速上传ing
  • It is so COOL!

旧版的MySQL字段是字符串类型,传入整数,不会自动转换,能得到数据,但索引会失效,是全表扫描

使用redis连接池处理链接是应对高并发的有效方式

当QPS很高的时候,比如10w QPS,如果不使用连接池,会导致大量的短连接请求,对于http请求,会有大量的三次握手和四次挥手,由于在挥手的时候,tcp有time wait 机制,会在1min-2min(根据系统而定)才会完全释放端口给下一个请求使用,所以在time wait时间,会导致端口耗尽,没有课使用的端口,使得短连接请求失败。 解决方法之一,就是加机器,比如加到100台甚至更多,这样能够有足够的端口使用,但这种方式会造成浪费CPU和内存的情况 第二种方式:使用连接池+合适的机器数量,让资源充分合理的使用

Linux的时间

内核有多种时间:

  • RTC 精度低,精确到毫秒。
  • wall time(xtime) 记录UTC 1970年1月24日到当前时刻所经历的纳秒,大部分时间函数或命令是从这里获取
  • monotonic time 单调递增,不会累加系统休眠时间,受到ntp adjtimex影响
  • raw monotonic time 不受到ntp影响
  • boot time

PHP & 取值符号,会升级临时变量的作用域

PHP & 取值符号,会升级临时变量的作用域,临时变量变为方法中的全局变量,如果之后的代码中有用相同的变量名称,是操作相同的变量地址

避免时间千年虫发方式

使用时间戳做时间的比较

raft的详细中文论文翻译

https://github.com/OneSizeFitsQuorum/raft-thesis-zh_cn/blob/master/raft-thesis-zh_cn.md

前端与后端

前端的问题不是难,而是它面对最终用户。只要用户的喜好和口味发生变化,前端就必须跟上。这导致前端不得不快速变化,因为用户的口味正在越来越快地改变。

后端不需要面对最终用户,需要解决的都是一些经典的计算科学问题,比如算法和数据结构。这些问题很少变化,可以利用以前的研究成果,所以变化速度慢得多。

前端的特征是混乱、嘈杂、易变,因为这些都是最终用户的特征,前端需要匹配用户。如果你不适应混乱、嘈杂、易变的开发,你就很难适应前端。
后端涉及到计算科学、语音设计、编译原理等高深内容,想要搞懂这些东西,绝非易事。

封装DAO层进行数据操作,避免在业务逻辑中写SQL

这样也能方便mock测试

简单的概率抽奖算法PHP

/**
    * 概率抽奖算法
    // TODO 测试50%概率
    // $proArr = [
    //     1 => 5000,
    //     2 => 5000,
    // ];
    */
    function run_get_rand($proArr) 
    {
        $prize = '';
        $proSum = array_sum($proArr);
        foreach($proArr as $key => $proCur) {
            $randNum = mt_rand(1, $proSum);
            if($randNum <= $proCur) {
                $prize = $key;
                break;
            } else {
                $proSum -= $proCur;
            }
        }
        return $prize;
    }
$a = 0;
$b = 0;
$p = [
  1 => 30,
  2 => 60
];

for($i=1;$i<=1000;$i++){
    if (run_get_rand($p)==1){
            $a++;
    } else {
            $b++;
    }
}

echo $a; // 得到的$a的值大概是300-350之间,占1000的三分之一左右


function run_get_rand($proArr)
    {
        $prize = '';
        $proSum = array_sum($proArr);
        foreach($proArr as $key => $proCur) {
            $randNum = mt_rand(1, $proSum);
            if($randNum <= $proCur) {
                $prize = $key;
                break;
            } else {
                $proSum -= $proCur;
            }
        }
        return $prize;
    }

InnoDB 的 MVCC 是如何实现的

InnoDB 是如何存储记录多个版本的?这些数据是 事务版本号,行记录中的隐藏列和Undo Log。

事务版本号 每开启一个日志,都会从数据库中获得一个事务ID(也称为事务版本号),这个事务 ID 是自增的,通过 ID 大小,可以判断事务的时间顺序。

行记录的隐藏列 row_id :隐藏的行 ID ,用来生成默认的聚集索引。如果创建数据表时没指定聚集索引,这时 InnoDB 就会用这个隐藏 ID 来创建聚集索引。采用聚集索引的方式可以提升数据的查找效率。 trx_id: 操作这个数据事务 ID ,也就是最后一个对数据插入或者更新的事务 ID 。 roll_ptr:回滚指针,指向这个记录的 Undo Log 信息。

Undo Log 事务前的备份记录,用于事务回滚 InnoDB 将行记录快照保存在 Undo Log 里。

数据行通过快照记录都通过链表的结构的串联了起来,每个快照都保存了 trx_id 事务ID,如果要找到历史快照,就可以通过遍历回滚指针的方式进行查找。

Read View 是啥? 如果一个事务要查询行记录,需要读取哪个版本的行记录呢? Read View 就是来解决这个问题的。Read View 可以帮助我们解决可见性问题。 Read View 保存了当前事务开启时所有活跃的事务列表。换个角度,可以理解为: Read View 保存了不应该让这个事务看到的其他事务 ID 列表。

trx_ids 系统当前正在活跃的事务ID集合。 low_limit_id ,活跃事务的最大的事务 ID。 up_limit_id 活跃的事务中最小的事务 ID。 creator_trx_id,创建这个 ReadView 的事务ID。 ReadView

如果当前事务的 creator_trx_id 想要读取某个行记录,这个行记录ID 的trx_id ,这样会有以下的情况:

如果 trx_id < 活跃的最小事务ID(up_limit_id),也就是说这个行记录在这些活跃的事务创建前就已经提交了,那么这个行记录对当前事务是可见的。 如果trx_id > 活跃的最大事务ID(low_limit_id),这个说明行记录在这些活跃的事务之后才创建,说明这个行记录对当前事务是不可见的。 如果 up_limit_id < trx_id <low_limit_id,说明该记录需要在 trx_ids 集合中,可能还处于活跃状态,因此我们需要在 trx_ids 集合中遍历 ,如果trx_id 存在于 trx_ids 集合中,证明这个事务 trx_id 还处于活跃状态,不可见,否则 ,trx_id 不存在于 trx_ids 集合中,说明事务trx_id 已经提交了,这行记录是可见的。 如何查询一条记录 获取事务自己的版本号,即 事务ID 获取 Read View 查询得到的数据,然后 Read View 中的事务版本号进行比较。 如果不符合 ReadView 规则, 那么就需要 UndoLog 中历史快照; 最后返回符合规则的数据 InnoDB 实现多版本控制 (MVCC)是通过 ReadView+ UndoLog 实现的,UndoLog 保存了历史快照,ReadView 规则帮助判断当前版本的数据是否可见。

总结 如果事务隔离级别是 ReadCommit ,一个事务的每一次 Select 都会去查一次ReadView ,每次查询的Read View 不同,就可能会造成不可重复读或者幻读的情况。 如果事务的隔离级别是可重读,为了避免不可重读读,一个事务只在第一次 Select 的时候会获取一次Read View ,然后后面索引的Select 会复用这个 ReadView.

https://zhuanlan.zhihu.com/p/147372839

PHP 中sort函数的区别

sort() 函数用于对数组单元从低到高进行排序。 rsort() 函数用于对数组单元从高到低进行排序。 asort() 函数用于对数组单元从低到高进行排序并保持索引关系。 arsort() 函数用于对数组单元从高到低进行排序并保持索引关系。 ksort() 函数用于对数组单元按照键名从低到高进行排序。 krsort() 函数用于对数组单元按照键名从高到低进行排序。

保持索引关系是指,原本的key会保持原样,不会被更改为0、1这样的整数索引,所以如果是对map形式的array进行排序,推荐使用asort、arsort

多个写操作逻辑,以最后一个写操作作为完成标识

高并发服务排行榜的解决方案

  1. 直接使用redis的sorted set有序集合 比如:只是”我的好友的xx排行“,则直接使用sorted set,能支撑1-2wQPS,没有什么问题。每个人的key是不同的,是在性能范围内的”热key“ 如果希望减小”写“的并发操作,则可以直接使用key value存分数值,通过mget(好友数量有一个限制,比如30),则可以减轻写操作的性能,虽然增加了mget的性能消耗

  2. 对全网的排行,如果还是用sorted set有序结合,会出现热key问题,只有一个全局的key,写操作和读操作可能达到10w以上,而1个key,无法使用到redis的分布式。导致热key超过了性能支撑范围

解决方案: 即时性不高,容许一定延迟,比如5分钟。则是用关系数据库存储数据和排行的权重(MySQL),后台启动一个定时任务,每隔5分钟,从数据库中order by 查找,对权重和更新时间进行排序,取前n个数据,然后写入到redis缓存。业务端,再从redis中读取出数据。这样,让写操作,写入关系数据库,redis承担读性能,这样可以支撑10w左右QPS的性能。而关系数据库可以分布式部署,一般写操作QPS没有那么高,一般是几千,分布式部署的关系数据库能够稳定支撑。 缺点: 需要能够接受一定的延迟(几分钟)

  1. 需要高即时性的排行榜。则要解决读写的性能问题和即时性。读写操作都作用于redis缓存。使用分布式的redis,一个热key,比如 xxx_score,将这个key合理copy n份,比如:50份。 写操作需要更新50个key,读操作时,将每个用户(某种维度),进行hash,然后mod取模,指定到某一份a key上,读取这个key的值。这样,分散读的热key。读性能可以扩展出10w QPS * 10-20倍的性能。同样的,可以定时任务,从关系数据库读取然后更新这n份key,将这个定时任务改成1s1次,则对于redis的写操作,只有1s1次更新n份key的性能,redis 可以承担10-20倍(几十倍以上)的读性能。 缺点: 定时任务写这n份key,增加了写失败的几率,但是,由于可以每秒进行一次同步,下一秒就有成功率去更新数据,这种大大的减少了数据的延迟,由原来分钟级别的延迟,变成了秒级的延迟

聊天会话系统架构中的“推拉”简记

聊天会话系统的消息发送,涉及到“推消息”和“拉消息”两种模式。 推消息:当A发出一个消息时,会给消息队列,推送这条消息,消息队列再进行消费处理,将消息发送给B客户端。 但是,推消息会有失败的情况,这时候就需要靠“拉消息”来保证消息不丢失。 拉消息:在每个客户端,都在本地保存一个消息的序列号(是递增的),保存的都是当前可接收的max(最大)的序列号,当B接收到一个消息时,该消息的序列号会和本地的最大序列号进行比较,然后更新本地的当前max序列号,如果该消息的序列号大于当前max的序列号,则表示有消息丢失,则B客户端主动去服务端拉取当前最大max序列号之后的所有消息,然后再按时间排序展示,然后更新当前最大的max序列号。

缓存和数据库同步一致性问题的解决

  1. 延时双删策略 ```
  2. 先删除缓存,再写数据库
  3. 休眠一段时间(比如500毫秒)
  4. 再次删除缓存

问题:

  1. 延时双删策略通过休眠一段时间,再次删除缓存来解决缓存不一致的问题。但是由于每次写都要睡眠一段时间, 写操作耗时较长,从而大大降低系统的吞吐量。 在第二次删除缓存失败后,缓存和数据库不一致。为了解决这个问题需要业务在失败后提供重试删除机制,大大增加了业务开发成本。 ```
  2. 异步更新缓存策略(基于订阅binlog的同步机制) ``` 写流程:

  3. 先删除缓存,再写数据库
  4. 额外组件通过解析从库 binlog, 将写操作发送到消息队列
  5. 缓存从消息队列中消费,更新缓存

读流程:

  1. 先从缓存读取
  2. 如果缓存未命中, 从数据库读取, 将数据发送到消息队列。
  3. 缓存从消息队列消费,更新缓存。

问题: 通过解析从库的 binlog ,再通过消息队列串行化发给缓存, 缓存较主库延迟较大。比如大概率会出现业务写入主库更新数据,缓存还未更新,业务从缓存中得到旧值。(延迟性较高) ```

epoll 10个笔记

  1. 只有底层驱动实现了file_operations汇总poll函数的文件才可以被epoll监视,socket类型的文件实现了
  2. ep->wq是一个等待对了,保存对某一个epoll实例调用epoll_wait()的所有进程
  3. epoll惊群:多个进程等待在ep->wq上,事件触发后素有进程都被唤醒,单只有其中1个进程能够成功继续执行的现象。
  4. ep->poll_wait是epoll实例中另一个等待队列。当被监视的文件是一个epool类型时,需要用这个等待队列来处理递归唤醒
  5. ep->rdllist:epoll实例中包含就绪时间的fd组成的链表。通过扫描该链表,内核可以获取当前有事件触发的fd,而不是像select、pool那样全量扫描所有被监视的fd,再从中找出有事件就绪的。当调用epoll_ctl新增一个被监视的fd时,会注册一下这个fd回调函数ep_poll_callback,当网卡收到数据包会触发一个中断,中断处理函数再回调ep_poll_callback将这个fd所属的epitem添加到epoll实例中的rdllist中
  6. ep->ovflist的作用:再rdllist被占用时,用来再不持有ep->lock的情况下手机有就绪事件的fd
  7. epitem->pwqlist:保存epitem的poll等待队列
  8. epmutex、ep->mtx、ep->lock 3把锁的区别:所得粒度和使用目的不同。
  9. epoll使用红黑树来维护一个epoll实例中所有的epitem。需要增、删、改、查等动作有比较高的效率,尤其是当epoll监视的文件数量达到百万级时,红黑树是总和性能优秀,最差的情况时间复杂度:O(logn)。AVL叔查询效率稍快,单插入和删除效率低于红黑树
  10. 水平触发:关注点是数据,读操作缓冲区不空,写操作缓冲区不满,epoll_wait总会返回就绪。LT是epol默认的工作模式 边缘触发:关注的是变化,只有监视的文件上有数据变化发生,epoll_wait才会返回。(读操作关注有数据写进缓冲区,写操作关注数据co哪个缓冲区取走)

天平称重找出次品球问题

https://www.youtube.com/watch?v=aEhp-N-kZFM 李永乐 一个天平,从N个球中找出次品球问题,需要几次。因为天平称重后可以得到3个结果:天平的左侧、右侧、和没有放上天平的部分。 能够得到球的数量和称重次数之间关系的 公式:N <= 3^k (N是球的数量,k是称重的次数)

问题升级:不仅要找出次品球,还要知道这个次品球是偏重还是偏轻。 两个球一组称重,每一次称,都会乘三分之一(可能的情况数量变为原来的三分之一),并且不断的需要将完好的球参与到称重中

N个球,有2N种可能性,可能轻,可能重 2N * (1/3)^k <= 1 => N <= 3^k/2 这是一个上限。 严格情况: 第一次分成 a a b 三部分球 两部分a球平衡:2b * (1/3)^(k-1) <= 1 => b <= 3^(k-1)/2, 由于3^(k-1)肯定是奇数,而b是整数,缩放一下得到, b <= (3^(k-1))/2 两部分a球不平衡:2a * (1/3)^(k-1) <= 1 => a <= 3^(k-1)/2 N = 2a+b,将上面的a b 公式代入,得到 N <= (3^k-3)/2 最后结论:N <= (3^k-3)/2

一种简单的使用redis实现的延迟队列

有序集合+队列。以时间戳进行排序,定时取有序集合排前的一些数据,判断时间戳是否到了,到了就lpush到队列中,队列另一头Brpop取出数据

db的性能比想象中的要脆弱

即使能使用到索引,但是也有可能要进行大量row行数的扫描,导致慢查询。比如 count, >,< 这种批量查的操作。在高并发下,也许是100 QPS以上,一个这种慢查询就会阻塞db,慢慢累积,导致db所有核心都满负载阻塞,无法再处理正常的所有逻辑。 快速的解决方式:要先把所有慢查询kill掉,将该慢SQL从逻辑中删除

解决浮点精度丢失的一种方法

将浮点计算值乘以100000(这个值可以进行调整)后存储,在最终要输出的阶段,再除以100000还远倍数,再保留需要的小数和取整方式

在mac上 sed -i 进行批量替换修改文件内容

不备份 sed -i “” ‘s/要被替换的字符串/新的字符串/g’ maxLvTime.csv 备份 sed -i “xxx.bak” ‘s/要被替换的字符串/新的字符串/g’ maxLvTime.csv

对于热key的写入的强烈建议

场景:

  1. 热key,QPS较高的读写 2.写操作的value值比较大,例如大于1k 会导致redis在主从同步时需要同步的数据太大,出现主从同步故障

强烈建议:不要让玩家在业务逻辑接口(高QPS的接口)中来触发更新写入热key的数据, 而是使用定时任务,每隔一段时间,查出需要的值来写一次这个热key,这样极大的减少了写操作导致的性能问题。

Search

    Table of Contents