这段时间由于即将换工作(工作交接),所以处于比较空闲,也有时间空闲下来整理下技术方面的东西,社区论坛也逛得多了点,看到了网上不少相关于秒杀和抢红包之类的文章,做了对比,分享下:
首先秒杀、抢红包或者投票之类都属于瞬间高并发,所以本文只针对NOSQL(redis)做后端,各个的方案的性能做了对比:
基本业务逻辑是这样的:每个用户只允许抢一次,保证余额等操作的原子性,最后保证数据的一致性;
基于redis乐观锁方案:下面的代码转摘自本社区的某位朋友,
private int bid(HttpServletRequest request, HttpServletResponse response, Jedis jedis) throws Exception { int flag = 0;// 1,成功,2已经购买,3已经没钱了,其他異常 // 每个请求对应一个userId int userId = new Random().nextInt(999999); // 观察 总标值,每人抢购一元 while ("OK".equals(jedis.watch("accountBalance"))) { // 判断是否购买过 Boolean isBuy = RedisAPI.sismember("userIdSet", userId + ""); if (isBuy) { flag = 2; return flag; } //投资额 int r = 1;// new Random().nextInt(2); int lastAccount = 0; String balance = RedisAPI.get("accountBalance"); if (StringUtils.isNotBlank(balance)) { lastAccount = Integer.valueOf(balance) - r; } if (lastAccount < 0) { flag = 3; break; } Transaction tx = jedis.multi(); tx.set("accountBalance", lastAccount + ""); List<Object> result = tx.exec(); if (result == null || result.isEmpty()) { jedis.unwatch(); } else { System.out.println("恭喜您," + userId + "已经中标" + r + "元,标余额" + lastAccount + "元"); RedisAPI.set(Thread.currentThread().getName(), r + ""); RedisAPI.sadd("userIdSet", userId + ""); flag = 1; break; } }u return flag; }
我们看到,代码中对
while ("OK".equals(jedis.watch("accountBalance"))) {
加了乐观锁,并且也判断了
// 判断是否购买过 Boolean isBuy = RedisAPI.sismember("userIdSet", userId + "");
那么是否真的如作者所想的,保证了每个用户只能领一次么?答案显然是否定!因为servlet中线程是不安全的,在并发下
Boolean isBuy = RedisAPI.sismember("userIdSet", userId + "");
这是有漏洞的!而且即使这里做了线程安全,如果放在分布式环境中,跨JVM的线程安全也有隐患;
解决方法有两个:1.是方法前加同步锁,当然这样的性能是最差的不建议;ab测试结果:
不用太关注测试值的绝对值,cpu1.8GHz硬件环境不是太好,哈,只用关注各个方案之间的对比
2.对userid乐观锁,也做这样能保证一个用户只能领一次;ab测试结果:
那么,贴了这两种方案,其实从性能来看是没达到预期的;第一种是最暴力的java同步jizhi,以牺牲性能为代价的;
第二种性能有了很大的提高;
于是转悠中,发现了另一种比较好方案:
利用redis + lua解决抢红包高并发的方案
为尊重原作者的劳动成果,我这里只做转摘,原文网址:http://blog.csdn.net/hengyunabc/article/details/19433779/
里面写的相当的详细了,对于并发安全,操作原子性以及性能做相当细的分析;
正是基本lua脚本方式,以及redis服务端的方式形式,对类似于抢红包这样的高并发性能又了很显著的提升,原文作者是在java里面调用redis,我将其中代码部署在TOMCAT下,nginx做了反向代理;ab测试结果如下:
1000RPS,哈哈,终于有了质的飞跃,接近了预期;
那么如果不放在tomcat下,而直接放到nginx下呢结果又会怎么样?我们都知道tomcat作为WEB容器性能是有瓶颈的,我pc试过放个最简单的动态页面性能也才1K多;
我们都知道openresty将nginx所用到模块做了很好的整合,异步非阻塞会大大提高服务端的并发性能:
这里不对openresty环境如何搭建做进一步的展kai了;这里只贴下lua脚本,
local math = require("math") userid=math.random(0, 10000000) --连接redis local redis = require "resty.redis" cjson = require "cjson" local cache= redis.new() local ok , err = cache.connect(cache,"127.0.0.1","6379") cache:set_timeout(60000) local sha1, err = cache:script("load", "if redis.call('hexists',KEYS[3], KEYS[4]) ~= 0 then return nil else local hongBao = redis.call('rpop', KEYS[1]) if hongBao then local x = cjson.decode(hongBao) local food=redis.call('get','accountBalance') food=food-1 redis.call('set','accountBalance',food) x['userId'] =KEYS[4] local re = cjson.encode(x) redis.call('hset',KEYS[3], KEYS[4], KEYS[4]) redis.call('lpush',KEYS[2], re) return re end end return nil"); if not sha1 then ngx.say("load script error : ", err) return close_redis(cache) end ngx.say("sha1 : ", userid, "<br/>") local resp, err = cache:evalsha(sha1,4, "hongBaoList", "hongBaoConsumedList", "hongBaoConsumedMap",userid);
然后再用AB做下压力测试:
2.4K了是 刚才的1倍多,而且细心的同学会发现,我前面两种方案的AB压的是200个客户端;
那么我们来看下后台数据是否一致:
说明操作还是原子性的,因为放到了redis的服务端操作了;
总结:
OpenResty 通过汇聚各种设计精良的 Nginx 模块(主要由 OpenResty 团队自主kai发),从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。这样,Web kai发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单ji并发连接的高性能 Web 应用系统。
有兴趣的同学可以百度openresty获取相关资料,对本文感兴趣的也可以私信我要代码;
最后吐槽:第一次在oschina上写博客,编辑器的敏感词检查实在不敢恭维啊!!!
愣是找不到包含这样的敏感词,后来才发现 JI器、KAI发者,这些都当成了敏感词了,一片博客修改了半天,才发出去,希望官方能有所改进!!