最近我(楚吟风http://Chuyinfeng.com)查看服务器异常日志,发现有很多如下报错:
Duplicate entry ‘xxxxxx’ for key ‘yyyyy’
很显然,是重复插入相同唯一字段记录造成的。
但是,业务逻辑里面明明有做判断,先SELECT不到再做INSERT的,怎么还会重复呢?
继续跟踪后发现,统一请求竟然发送了3次,每次间隔才几ms而已。
这就可能造成如下情况:
请求A在00:00:00.000最先到达,在00:00:00.002先SELECT发现没有记录,在00:00:00.004进行INSERT,在00:00:00.007操作完成。
请求B在00:00:00.004正好到达,在00:00:00.006先SELECT发现没有记录,在00:00:00.008进行INSERT,发生Duplicate异常。
从经验判断,这种重复请求,很可能是对DOM中的包含了href的a元素做了click事件的ajax绑定。
但是IE6下这样做有个问题,a语义为超链接跳转的元素,就算在click后return false,也会进行一次页面重新导航,在IE编程中来说就是触发了navigate事件。这时候,ajax操作实际上会被浏览器强制中断。
这是产生问题的直接原因,根本原因在于后端代码不够强壮,没有做到对相同数据的请求做异常处理。
我们的多台WEB机使用nginx的upstream做负载均衡,采用默认轮询方式,所以需要实现分布式WEB上的相同数据请求的互斥锁。
mysql的select和insert操作都要消耗时间,并且由于mysql的锁等导致的性能抖动、网络抖动等原因,不能保证每台WEB机进行select和insert都消耗同样的时间。
DB层的解决方案可以忽略了。内存层面呢?没法做到分布式。那么分布式内存呢?很好,那就memcached吧。
很自然的,我们可以想到,对相同数据进行md5得到唯一key,先get判断该请求是否存在,如果不存在则set到memc中并对数据进行处理,否则直接丢弃请求。ok , just do it.
但是,同样的异常还在产生!
究其原因,是因为当PHP和MEMC在不同机器上时,每次操作的耗时并且均衡,甚至是一台机器上也是如此。
请求A在00:00:00.000到达,在00:00:00.001 get,在00:00:00.003返回数据,为空,在00:00:00.005 set成功。
请求B在00:00:00.001到达,在00:00:00.002 get,在00:00:00.004返回数据,为空,在00:00:00.006 set成功。
这样看来,由于要发起get/set两次操作才能得出结果,耗时已经在毫秒级了,自然起不到毫秒级过滤的作用。
再想想,memc的原子操作,自增incr呢?不存在则设置setex呢?很不好意思,setex是redis中的。那么找个类似的,不存在则增加add呢?
ok,再来尝试用memc的add做分布式锁吧。我们先看看add方法如何使用
Memcache::add()方法在缓存服务器之前不存在key时, 以key作为key存储一个变量var到缓存服务器。 同样可以使用函数 memcache_add()。
成功时返回 TRUE, 或者在失败时返回 FALSE。 如果这个key已经存在返回FALSE。
这样我们只需要一个add操作就可以得到结果了,处理速度基本依赖内存IO和cpu频率上,比毫秒级了不知还要小多少倍。我们假设一下:
情况1
请求A在00:00:00.000到达,在00:00:00.001 add 成功,在00:00:00.004返回数据,为true。
请求B在00:00:00.001到达,在00:00:00.002 add 成功,在00:00:00.003返回数据,为false。
情况2
请求A在00:00:00.000到达,在00:00:00.004 add,在00:00:00.005返回数据,为false。
请求B在00:00:00.001到达,在00:00:00.002 add,在00:00:00.003返回数据,为true。
可见,网络抖动对基于add的判断结果是没有影响的,可以保证同时只有一个请求能够返回true。
最终实现代码很简单,但是效果却非常好
1
2
3
4
5
6
7
8
9
10
11
12
13
14/**
* lock, 分布式互斥锁
*
* 楚吟风 http://chuyinfeng.com
*
* @param string $key, memcache键名
* @param int $timeout, kv对生存时间,默认30s
* @return boolean
*/
public function lock($key,$timeout = 30)
{
$memc = memcache_connect("localhost", 11211);
return $memc->add($key, 1, FALSE,$timeout);
}
Tags: Memcache, PHP, PHP