如何处理微信的并发推送并保证数据的准确性 作者: 不跑马拉松的摄影师不是好城续猿

如何处理微信的并发推送并保证数据的准确性

并于并发处理的问题已不新鲜,网上也有大量的解决方案及一些开源的代码。最近做的一个项目也遇到了并发的问题,并不是前端请求并发,而是微信推送事件的并发,略坑。

关于微信并发

并发在很多系统中都存在,比如抢红世,秒杀商品之类的。

我们做了一个通过微信的摇一摇与用户进行互动的一个模块(需要摇一摇的周边产品),当用户摇一摇的时候触发微信通知。

若按照片微信官方文档写的应该是第一次推送失败后会再次进行推送,最多推送三次。只要在5秒内返回空字符串就行。

但根据我日志来看,微信在同一时间同时推送了两条一样的记录。并且还多次重现了,为了解决这个问题我们想了几种解决方案。

当然还有很多其他解决方案,我这里只写最简单的。

我猜想的微信摇一摇周边实现

微信通过蓝牙搜索附近一摇一摇周边的蓝牙设备,然后获取到设备的基本信息与微信服务器上所配制的信息进行匹配。再把用户信息及摇一摇事件信息推送的当时所配制的第三方服务的地址。这时候第三方服务器接收到用户标识(openid)与摇一摇周边设备的信息进行验证,这样用户是否是通过运营方的设备摇进来的并且做相关的操作。

那么摇一摇事件微信本身是做了限制的,根据我们的测试一个手机设备一分钟最多只能摇25次,超过将进行锁定,几分钟后将可以再次摇。

在我看来单个用户同一时间请求两次这应该是一种小概率事件,很奇怪微信没限制住还是其他什么原因。部份手机重现过。

本项目的技术架构

由于种种原因我们尝试了这种技术架构,感觉好像是前无古人。哈哈哈哈哈哈(自嗨一下)但这种架构也踩了不少坑,当然这也是好事,有坑就踩实了,为了让以后的项目更好的绕过一些没必要重复踩的坑。

主要业务由ngx_lua来实现所有的交互逻辑包括微信的事件处理及其他业务处理,websocket由golang来实现,后台php实现(主要做控制、配制、入库),数据库是由Redis作为主要业务, mysql做为数据存储。

ngx_lua处理并发的能力还是不错的,至少在4核8G的测试环境上看QPS到1000+应该是没啥问题的,因为大多数据操作或外网api请求都是异步完成的。主流程很少会出现阻塞。

这个项目的接口文档及数据结构文档已经超过20页了,是不是很6 =_= 嘿嘿!

关于并发

并发就是在同一时间发起了多次请求,如果按正常处理的话。肯定会出现多次获取数据然后覆盖的问题。

若我们不对并发进行处理,那么很可能会出现一个商品被消费多次的情况。

为了模拟微信的这种事件推送,我用Python写了一个模拟并发请求的小工具:

模拟微信并发请求小工具

通过python的多线程模拟同一时间多个post请求。

若无法查看,请点击下面的[查看原文]

解决方案

我们知道Redis是单进程、单线程的程序。那么它是阻塞的,既然是阻塞的,那么就很好处理了。

比如队列、锁、事物,都能很好的处理并发问题,若是多线程的程序那么处理起来会比较麻烦一些。

当然也有组合的方式,比如下面我们所使用的就是组合的方式处理的。

队列

队列相对来说是一种相对来说比较安全的处理方式,但是它的弊端就是"慢",数据得一个一个出,一个一个处理,这可以保证数据的准确性,但若数据量实在太大的话数据会堆积越来越多,那么最后入的数据可能会等的时间会长一些才能被消费掉。

当然这种方案也适合在某些场景。

消费队列很重要,做好控制。

并发锁

并发锁其实是一个不错的方案,至少我是这么觉得的。因为我这是我目前的解决方案。

那为啥用“锁”的机制呢?

因为这样我不用改太多的代码呀,并且还能保证高效....何乐而不为呢?( ̄▽ ̄)

如果前面没控制好,再使用队列的话,那么就有可能有多个相同的数据被消费.

锁的正确使用方式

Redis 里有一个命令叫作SETNX, SETNX 有一个内部锁的机制,咱们可以利用它这一特性。当写入某个key 的时候如果key存在,则返回0否则返回1如果返回0跳出就行了,redis是单进行程单线程,那么同一时间只会有一个写,理论上来说不会出现两个或多个同时写一个key的情况。

那么如果加锁了,那就很有可能会出现死锁的情况,解决死锁的方法也很简单,那就是给这个锁加一个时间,若超过这个时间了,那么这个key就消失了,当然这得根据实际业务来写死锁的时间。

简单的伪代码

SETNX lock:test:{userId} hello // 根据用户id写入一个key若返回0 表示key已存在
EXPIRE lock:test:{userId} 5 // 给这个锁一个时间 咱们设置为5秒
HGET user:award {userId}  // 若已存在 删除锁 并跳出
RPOP award:list // 弹出消费
HSET user:award {userId} {AwardInfo}
// 若业务已处理完,只要把锁删掉就行了 这样锁就释放了
DEL lock:test:{userId}

这样就能保证单个用户,同时或在规定的时间内只操作一次。

那么后续的流程就需要队列了。

比如上面拦死后,我们就需要从队列里弹出理个值,进行验证,如果为空则跳出,不再继续了否则继续后续流程。

RPOP your:queue

通过这种简单的组合方式就可能保证数据的准确及效率了。

前者保证生个用户同时只能发起一个请求,后者保证仓库数值不会被多消费, 其实就是库存的问题。

事务

事物的处理方略复杂些,在大多数据库都自带有事物实现,Redis本身没有,但可根据事物的机制自己实现一套,原理是一样的。

一会还有事,先偷个懒就不写了(反正你们也不知道我用没用过)

尾巴

关于并发处理,欢迎大家提出问题,或提供其他解决方案。

写一个功能,能出问题就好,若出没遇到过的问题那是更好。当把一个非常难的问题解决之后的喜悦应该是非常有成就感的,非猿猿们可能体会不出这种喜悦。

同时感谢同事们的测试,感谢同事们的问题复现从而让我发现程序一些不够周全的地方。

最后,微信的服务也不是完全可靠的,大家注意着点。

本文地址: https://lattecake.com/post/20104


January 8, 2017 20:11



某一人似曾相识、某一刻似曾经历