某张表有一批记录,a用户说,这批记录是我要的,但是我只要一条,b用户也说,这批记录是我要的,我也只要一条。
是不是有点像一群男人去逛怡红院,妹子们都是目标,但是今晚只要一位,至于是谁暂时还不确定,虽然不需要抢,但是得锁单。
被动分配式,等着妈妈给你分一个。
主动挑选式,主动到姑娘们群里挑,就涉及到锁单的问题了,一个妹子只能陪一位公子哦。
上面的例子可能不太适合未成年人,那么看看另一个形象的比喻,某处有一堆砖块,每块砖头都有一个唯一编号,然后一群小伙伴同时来取砖块(每人每次取1块),要求每个小伙伴拿到的砖块id是随机的,并且要求以最快的方式将砖块取完。
这次真的来搬砖了,来比一比谁的搬砖能力强吧。

我们将问题转化一下,一块砖一个id,作为一条记录存入数据库,假设我们有1000万块砖。有128个小伙伴同时来搬砖,怎么能以最快的速度,随机的把砖搬完呢?
这个场景实际上有一个来头,某个群红包口令业务,由于该业务没有对接账务系统,没有用户id也没有用户手机号,所以没法将领红包的资格做判定,为了防止任何人都能猜测口令的方式来领取红包,搞了一个批量生成随机口令的方法,发红包的时候从数据库取走一条(随机口令)。既要考虑随机,又要考虑用户体验,所以选择了8位数值(比较容易猜测),然后又要考虑高并发的发红包场景,所以还要求取值快。
理解了需求后,我们看看如何优化?
考虑随机、并发还不够,因为数据要取走(转移到一个已消耗的表中),因此还需要考虑数据的收缩。
比如postgresql的堆表,末端的空数据块是可以被回收的,那么我们在设计的时候,如果能从末端开始取,是最好的。
1. 插入时就让数据随机,而不是取时随机。
创建测试表, 存放一堆唯一值.
唯一值随机插入, 取数据时按照数据块倒序取出, 这么做的好处是vacuum时可以直接回收这部分空间.
随机的插入1000万数据
从数据来看 , 已经随机插入了.
在ctid(行号)上创建索引, 取数据时使用这个索引, 倒序从最后的数据块开始取数据.
例如:
为了防止多个进程重复取数据, 使用这种方法.
测试并行取数据.
测试方法, 将数据插入另一张表,表示数据从一张表搬运到另一张表。
使用pgbench 测试, 16个并行取数据进程, 每次取5条.
测试完成后, 查询test表, 看看有没有重复数据就知道这种方法是否靠谱了.
性能见下 :
经查没有重复数据, 方法靠谱,搬砖成功
以上方法数据是从堆表的末端开始搬运的,所以表会随着搬运,autovacuum使之变小。
但是实际上,以上query有一个问题,select没有加锁,在delete时,可能已经被其他并发进程搬走了。竞争的问题也被掩盖了。
为了改善这个问题,比如要求每次请求,必须搬走1块砖。那么需要加limit 1 for update skip locked,这样能解决竞争的问题,但是无法解决重复扫描的问题。
我们先看看效果
64个搬运工,每秒只能搬运4000条左右。
因为他们中最差的那个询问了64块砖才拿到搬运这块砖头的所有权,只有第一个人,询问了1块砖就拿到了所有权。
那么怎么优化呢? 如何让每个搬运工每次拿到的砖头id都是随机的,并且没人和他抢。
如何拿到随机的记录是关键,postgresql提供了一个随机访问接口tablesample,通过这个接口,可以随机访问数据(提供一个百分比1-100即可),注意随机访问的数据是在where过滤条件前,所以百分比太小的话,你可能会访问不到任何数据。
目前支持两种随机采样方法,1. system,按块随机(整个数据块的记录被取出);2. bernoulli扫全表,按百分比返回随机记录;因此bernoulli比system随机度更精准,但是system的效率更高。
压测
搬砖性能从4000提升到了将近9万。
除了这个搬砖场景,还有一些其他场景也能使用类似方法,感谢万能的postgresql。
比如有一个场景初始化了一批账号id,初始id=0,每次有用户来注册时,将id=0的记录修改为此次注册的用户id,相当于消耗一条id=0的记录。
使用采样的方法可以优化这个场景,不过别急着套用,因为数据采样是在过滤条件之前发生的,所以当所有数据范围都是我们的目标数据是没问题的,但是如果你把目标数据和非目标数据混到一起,这种采样的方法就可能导致冗余扫描,如果采样比例低,甚至找不到目标数据。因此前面的搬砖场景,我们每次都把数据搬走,剩余的所有数据依旧是目标数据,所以不存在问题。
那么了解了以上原理之后,第二个场景,我们也采样转移法,即申请id的时候,将数据转移走,而不仅仅是update id=newid的做法。
例子
函数
测试
每秒转移9.8万记录,采样法消除冲突后性能惊人。
1. 为了解决高并发的数据随机访问、更新、转移等热点与扫描相似悖论的问题,postgresql 采样接口打开一种很"无耻"的优化之门,让小伙伴们可以开足并发,卯足玛丽开搞。
为什么一个蛋糕,大家都要从一处抢呢,围成一圈,每人在各自的方向挖一勺不是更好么?就好像小时候长辈较我们夹菜,要夹靠近自己这一边的一样。
<a href="https://www.postgresql.org/docs/9.6/static/plpgsql-statements.html">https://www.postgresql.org/docs/9.6/static/plpgsql-statements.html</a>
<a href="https://wiki.postgresql.org/wiki/tablesample_implementation">https://wiki.postgresql.org/wiki/tablesample_implementation</a>