我负责的是秒杀模块和网关认证授权功能,秒杀模块其实是促销模块的一个子功能,由于我们的项目老开发人员比较急,为了优先实现功能,多个功能模块只是多模块开发,模块之间调用需要通过httpClient,并没有引入RPC框架;而且各模块的数据库的表也比较混乱没有分库,功能逻辑耦合度也比较大,而且并发能力也比较弱只能达到几百,我们做秒杀功能模块其实是为了后期把服务都迁移到新框架中。
我这次负责的是促销系统的秒杀功能,因为要抗击高并发,所以单独创建独立微服务,独立数据库。大佬把旧项目各个模块的数据库也进行了分库,然后做了微服务适配。我创建秒杀模块创建成独立服务,引入微服务网关进行认证和授权。我对系统安全方面和应对高并发方面比较了解。
http://www.woshipm.com/pd/916015.html
风控系统比较简单,是通过设定业务规则,限定一个用户只能参加一个活动,而且只能抢购有限数量的商品
后台订单系统和前端订单系统展示的信息相对应,包括订单列表以及订单详情的展示。
我们系统是微服务架构,分布式部署,我们可以将秒杀也单独做一个服务。
单独给秒杀服务创建一个秒杀库:秒杀订单,秒杀商品
单一职责的好处就是就算秒杀没抗住,秒杀库崩了,服务挂了,也不会影响到其他服务。
秒杀前按钮都是置灰的,秒杀时间到了才能点击。
按钮点击之后也给它置灰几秒,防止重复点击。
验证码登陆验证。
秒杀的本质,就是对库存的抢夺。通过redis预扣库存的实现,避免了到底是支付减库存、下单减库存两种方案的选择,因为其各有利弊。而且还将验库存和减库存通过mq进行了解耦,极大的提升系统并发度和响应时间。使得系统从qps不到1000提升到上万的能力。
每个秒杀用户都去数据库查询库存效验库存,然后扣减库存,所有的操作都在数据库,会导致数据库顶不住
秒杀前通过定时任务将库存加载到redis中去,让效验库存的操作在redis中进行,然后发mq同步redis和数据库的数据。
在高并发场景下,多个线程同时效验该商品库存满足条件,然后同时扣减库存导致超卖问题发生。
Lua脚本功能时Redis2.6版本最大的亮点,通过对Lua脚本的支持,Redis解决了长久以来不能高效处理CAS命令的缺点,并且可以通过组合多个redis命令,轻松实现以前很难实现或者不能高效实现的模式。
Lua脚本实现了redis事务操作,我们将判断扣减库存的操作 和 预减库存的操作写到一个Lua脚本,返回扣减后的库存数量,如果库存数量<0则判断为发生超卖,将之前扣减的数目再加回去;如果库存数量>0,则发一个订单消息,然后订单模块生成订单,库存模块扣减库存,支付模块进行支付;支付成功后发送一个支付成功消息,然后优惠券模块扣减优惠券,积分模块增加积分,最后向用户发送短信通知。
JSONObject.parseObject(data);
库存=配额
key=手机号/微信ID+活动ID value=订单数据
kafka的key=redis的key value=orderId
通过Lua脚本实现抢红包功能,很优秀
## 尝试获得红包,如果成功,则返回json字符串,如果不成功,则返回空
## eval函数(脚本名称,参数个数,keys1,keys2,keys3,keys4)
## keys1:预生成的红包队列 keys2: 已消费的红包队列 keys3: 去重map keys4:用户id
static String tryGetHongBaoScript =
"if redis.call('hexists', KEYS[3], KEYS[4]) ~= 0 then\n" //如果用户已经抢过红包,则返回nill
+ "return nil\n"
+ "else\n"
+ "local hongBao = redis.call('rpop', KEYS[1]);\n" //取出一个小红包
+ "if hongBao then\n"
+ "local x = cjson.decode(hongBao);\n"
+ "x['userId'] = KEYS[4];\n" //加入用户信息
+ "local re = cjson.encode(x);\n"
+ "redis.call('hset', KEYS[3], KEYS[4], KEYS[4]);\n" //将用户放到去重的set中去,防止多次抢红包
+ "redis.call('lpush', KEYS[2], re);\n" //将红包放入以消费队列中
+ "return re;\n"
+ "end\n"
+ "end\n"
+ "return nil";
static public void testTryGetHongBao() throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(threadCount);
long startTime = System.currentTimeMillis();
System.err.println("start:" + startTime);
for(int i = 0; i < threadCount; ++i) {
final int temp = i;
Thread thread = new Thread() {
public void run() {
Jedis jedis = new Jedis(host, port);
String sha = jedis.scriptLoad(tryGetHongBaoScript);
int j = honBaoCount/threadCount * temp;
while(true) {
//抢红包方法
Object object = jedis.eval(tryGetHongBaoScript, 4,
hongBaoList/*预生成的红包队列*/,
hongBaoConsumedList, /*已经消费的红包队列*/
hongBaoConsumedMap, /*去重的map*/
"" + j /*用户id*/
);
j++;
if (object != null) {
//do something...
// System.out.println("get hongBao:" + object);
}else {
//已经取完了
if(jedis.llen(hongBaoList) == 0)
break;
}
}
latch.countDown();
}
};
thread.start();
}
刚开始我们使用的是付款减库存,防止恶意买家大量下单。
微信支付回调会返回微信生成的订单号以及我们自己生成的订单号
a) 设置的秒杀活动的库存,总是莫名其妙的减少了。分析发现是因为我们把减库存放在微信支付的成功回调里面的,而微信会回调这个url8次,导致多次减库存。最后我们通过接口幂等进行了解决
b) 用户下单显示的不是最新的数据库,支付时用户经常由于库存不足而支付失败,这导致用户体验十分不好。
备用库存:商品库用完之后,如果还有用户支付,则直接扣减备用库
优点:缓解部门用户支付失败问题
缺点:不能从根本解决问题,若并发量很大,还是会出现大量用户下单成功缺库存不足而支付失败的问题
微信支付成功后,微信支付平台会发送8次回调地址,这样就得做接口幂等
链接要是提前暴露出去可能有人直接访问url就提前秒杀了
刚开始我们想到做个秒杀开始、截止时间,但是这种方案解决不了通过程序进行抢购。
将URL动态化,通过MD5之类的加密随机的字符串去做url,然后通过前端代码获取url后台效验才能通过
redis集群,主从同步,读写分离。
开启持久化保证高可用
Nginx是高性能web服务器,并发轻松上万并发,但普通Tomcat只能顶住几百的并发。
买流量机
几万并发——>Nginx——>Tomcat集群