1. 首页
  2. IT资讯

大型车祸现场,电商秒杀超卖,这个锅到底有谁来背?

“u003Cdivu003Eu003Cblockquoteu003Eu003Cpu003E作者: 小柒u003Cu002Fpu003Eu003Cpu003E出处: https:u002Fu002Fblog.52itstyle.vipu003Cu002Fpu003Eu003Cu002Fblockquoteu003Eu003Cdiv class=”pgc-img”u003Eu003Cimg src=”http:u002Fu002Fp3.pstatp.comu002Flargeu002Fpgc-imageu002F8a9ad81ea9624d938e7d8b98bfad779a” img_width=”720″ img_height=”405″ alt=”大型车祸现场,电商秒杀超卖,这个锅到底有谁来背?” inline=”0″u003Eu003Cp class=”pgc-img-caption”u003Eu003Cu002Fpu003Eu003Cu002Fdivu003Eu003Cpu003Eu003Cstrongu003E背景u003Cu002Fstrongu003Eu003Cu002Fpu003Eu003Cpu003E小明在一家在线购物商城工作,最近来了一个新需求,需要他负责开发一个商品秒杀模块,而且需求很紧急,老板要求必须尽快上线。u003Cu002Fpu003Eu003Cpu003Eu003Cstrongu003E方案u003Cu002Fstrongu003Eu003Cu002Fpu003Eu003Cpu003E小明一开始是这么做的,直接用数据库锁进行控制,获取秒杀商品数量并加锁,如果数量大于零则成功,否则秒杀失败。u003Cu002Fpu003Eu003Cpreu003E @Overrideu003Cbru003E @Transactionalu003Cbru003E public Result startSeckilDBPCC_ONE(long seckillId, long userId) {u003Cbru003E u002Fu002F获取秒杀商品数量并加锁u003Cbru003E String nativeSql = “SELECT number FROM seckill WHERE seckill_id=? FOR UPDATE”;u003Cbru003E Object object = dynamicQuery.nativeQueryObject(nativeSql, new Object[]{seckillId});u003Cbru003E Long number = ((Number) object).longValue();u003Cbru003E if(number>0){u003Cbru003E nativeSql = “UPDATE seckill SET number=number-1 WHERE seckill_id=?”;u003Cbru003E dynamicQuery.nativeExecuteUpdate(nativeSql, new Object[]{seckillId});u003Cbru003E SuccessKilled killed = new SuccessKilled();u003Cbru003E killed.setSeckillId(seckillId);u003Cbru003E killed.setUserId(userId);u003Cbru003E killed.setState((short)0);u003Cbru003E killed.setCreateTime(new Timestamp(new Date().getTime()));u003Cbru003E dynamicQuery.save(killed);u003Cbru003E return Result.ok(SeckillStatEnum.SUCCESS);u003Cbru003E }else{u003Cbru003E return Result.error(SeckillStatEnum.END);u003Cbru003E }u003Cbru003E }u003Cbru003Eu003Cu002Fpreu003Eu003Cpu003E写了并发线程,跑了一下,没问题,搞定!但是,小明转头一想,老板曾经说过,这次活动宣传力度很大,有可能会有很多用户参与活动。恰好项目中使用了 Redis 作为缓存,何不借用一下 Redis 的发布订阅功能,实现秒杀队列,从而减轻后端数据库的访问压力,提升服务性能!这可是个升职加薪,当上总经理,出任CTO,迎娶白富美的好机会。说干就干,复制、黏贴一把撸,很快小明就把消息队列方案搞定了。u003Cu002Fpu003Eu003Cpu003Eu003Cstrongu003E事故u003Cu002Fstrongu003Eu003Cu002Fpu003Eu003Cpu003E开发、测试、上线一条龙,活动开始了,秒杀商品是 100 部苹果手机,活动结束以后,居然产生了 106 个订单!老板很生气,后果很严重,这个锅必须有人得背,吓得小明赶紧仔细复查复制粘贴的代码。u003Cu002Fpu003Eu003Cpu003E监听配置 RedisSubListenerConfig :u003Cu002Fpu003Eu003Cpreu003E@Configurationu003Cbru003Epublic class RedisSubListenerConfig {u003Cbru003E u002Fu002F初始化监听器u003Cbru003E @Beanu003Cbru003E RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,u003Cbru003E MessageListenerAdapter listenerAdapter) {u003Cbru003E RedisMessageListenerContainer container = new RedisMessageListenerContainer();u003Cbru003E container.setConnectionFactory(connectionFactory);u003Cbru003E container.addMessageListener(listenerAdapter, new PatternTopic(“seckill”));u003Cbru003E return container;u003Cbru003E }u003Cbru003E u002Fu002F利用反射来创建监听到消息之后的执行方法u003Cbru003E @Beanu003Cbru003E MessageListenerAdapter listenerAdapter(RedisConsumer redisReceiver) {u003Cbru003E return new MessageListenerAdapter(redisReceiver, “receiveMessage”);u003Cbru003E }u003Cbru003E u002Fu002F使用默认的工厂初始化redis操作模板u003Cbru003E @Beanu003Cbru003E StringRedisTemplate template(RedisConnectionFactory connectionFactory) {u003Cbru003E return new StringRedisTemplate(connectionFactory);u003Cbru003E }u003Cbru003E}u003Cbru003Eu003Cu002Fpreu003Eu003Cpu003E生产者 RedisSender:u003Cu002Fpu003Eu003Cpreu003Eu002F**u003Cbru003E * 生产者u003Cbru003E * @author 爪哇笔记 By https:u002Fu002Fblog.52itstyle.vipu003Cbru003E *u002Fu003Cbru003E@Serviceu003Cbru003Epublic class RedisSender {u003Cbru003E @Autowiredu003Cbru003E private StringRedisTemplate stringRedisTemplate;u003Cbru003E public void sendChannelMess(String channel, String message) {u003Cbru003E stringRedisTemplate.convertAndSend(channel, message);u003Cbru003E }u003Cbru003E}u003Cbru003Eu003Cu002Fpreu003Eu003Cpu003E消费者 RedisConsumer:u003Cu002Fpu003Eu003Cpreu003Eu002F**u003Cbru003E * 消费者u003Cbru003E * @author 爪哇笔记 By https:u002Fu002Fblog.52itstyle.vipu003Cbru003E *u002Fu003Cbru003E@Serviceu003Cbru003Epublic class RedisConsumer {u003Cbru003E u003Cbru003E @Autowiredu003Cbru003E private ISeckillService seckillService;u003Cbru003E @Autowiredu003Cbru003E private RedisUtil redisUtil;u003Cbru003E u003Cbru003E public void receiveMessage(String message) {u003Cbru003E u002Fu002F收到通道的消息之后执行秒杀操作u003Cbru003E String[] array = message.split(“;”);u003Cbru003E if(redisUtil.getValue(array[0])==null){u002Fu002Fcontrol层已经判断了,其实这里不需要再判断了u003Cbru003E Result result = seckillService.startSeckilDBPCC_TWO(Long.parseLong(array[0]), Long.parseLong(array[1]));u003Cbru003E if(result.equals(Result.ok(SeckillStatEnum.SUCCESS))){u003Cbru003E WebSocketServer.sendInfo(array[0], “秒杀成功”);u002Fu002F推送给前台u003Cbru003E }else{u003Cbru003E WebSocketServer.sendInfo(array[0], “秒杀失败”);u002Fu002F推送给前台u003Cbru003E redisUtil.cacheValue(array[0], “ok”);u002Fu002F秒杀结束u003Cbru003E }u003Cbru003E }else{u003Cbru003E WebSocketServer.sendInfo(array[0], “秒杀失败”);u002Fu002F推送给前台u003Cbru003E }u003Cbru003E }u003Cbru003E}u003Cbru003Eu003Cu002Fpreu003Eu003Cpu003E数据层代码:u003Cu002Fpu003Eu003Cpreu003E@Overrideu003Cbru003E@Transactionalu003Cbru003Epublic Result startSeckil(long seckillId,long userId) {u003Cbru003E u002Fu002F由于使用了队列,小明这里没用数据库锁u003Cbru003E String nativeSql = “SELECT number FROM seckill WHERE seckill_id=?”;u003Cbru003E Object object = dynamicQuery.nativeQueryObject(nativeSql, new Object[]{seckillId});u003Cbru003E Long number = ((Number) object).longValue();u003Cbru003E if(number>0){u003Cbru003E u002Fu002F扣库存u003Cbru003E nativeSql = “UPDATE seckill SET number=number-1 WHERE seckill_id=?”;u003Cbru003E dynamicQuery.nativeExecuteUpdate(nativeSql, new Object[]{seckillId});u003Cbru003E u002Fu002F创建订单u003Cbru003E SuccessKilled killed = new SuccessKilled();u003Cbru003E killed.setSeckillId(seckillId);u003Cbru003E killed.setUserId(userId);u003Cbru003E killed.setState((short)0);u003Cbru003E Timestamp createTime = new Timestamp(new Date().getTime());u003Cbru003E killed.setCreateTime(createTime);u003Cbru003E dynamicQuery.save(killed);u003Cbru003E u002Fu002F支付u003Cbru003E return Result.ok(SeckillStatEnum.SUCCESS);u003Cbru003E }else{u003Cbru003E return Result.error(SeckillStatEnum.END);u003Cbru003E }u003Cbru003E}u003Cbru003Eu003Cu002Fpreu003Eu003Cpu003E小明重新审读了代码,一开始小明觉得既然使用了队列,数据库层面就没必要用数据库锁了,然后去掉了 for update,很显然问题就出在这里。导致超卖的因素只有一个,那就是多线程并发抢占资源,如果业务逻辑没有做相应的措施,很有可能导致超卖。u003Cu002Fpu003Eu003Cpu003E回到代码来看,虽然秒杀用户进入了队列,但是 RedisConsumer 端有可能是多线程处理队列数据,小明为了验证想法,在消费端加入了以下代码来打印线程名称。u003Cu002Fpu003Eu003Cpreu003EThread th=Thread.currentThread();u003Cbru003ESystem.out.println(“Tread name:”+th.getName());u003Cbru003Eu003Cu002Fpreu003Eu003Cpu003E再次运行任务,果不其然,每个秒杀用户都开启了一个线程处理任务:u003Cu002Fpu003Eu003Cpreu003ETread name:container-1u003Cbru003ETread name:container-2u003Cbru003ETread name:container-3u003Cbru003ETread name:container-4u003Cbru003ETread name:container-5u003Cbru003ETread name:container-6u003Cbru003E……u003Cbru003Eu003Cu002Fpreu003Eu003Cpu003E各位看官到这里,线索已经很明确了,我们只需要把消费端改造成单线程处理,问题就迎刃而解了。u003Cu002Fpu003Eu003Cpu003Eu003Cstrongu003E解决方案u003Cu002Fstrongu003Eu003Cu002Fpu003Eu003Cpu003E使用 Redis 消息队列,出现超卖问题是因为RedisMessageListenerContainer 的默认使用线程池是SimpleAsyncTaskExecutor,每次消费都会创建一个线程来处理,这样就会有大量的新线程被创建。有兴趣的小伙伴可以跟进源码,了解更多详细内容。u003Cu002Fpu003Eu003Cpu003E监听配置 RedisSubListenerConfig 改造为 :u003Cu002Fpu003Eu003Cpreu003E@Beanu003Cbru003ERedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,u003Cbru003E MessageListenerAdapter listenerAdapter) {u003Cbru003E RedisMessageListenerContainer container = new RedisMessageListenerContainer();u003Cbru003E container.setConnectionFactory(connectionFactory);u003Cbru003E container.addMessageListener(listenerAdapter, new PatternTopic(“seckill”));u003Cbru003E u002F**u003Cbru003E * 如果不定义线程池,每一次消费都会创建一个线程,如果业务层面不做限制,就会导致秒杀超卖。u003Cbru003E * 此处感谢网友 DIscordu003Cbru003E *u002Fu003Cbru003E ThreadFactory factory = new ThreadFactoryBuilder()u003Cbru003E .setNameFormat(“redis-listener-pool-%d”).build();u003Cbru003E Executor executor = new ThreadPoolExecutor(u003Cbru003E 1,u003Cbru003E 1,u003Cbru003E 5L,u003Cbru003E TimeUnit.SECONDS,u003Cbru003E new LinkedBlockingQueue<>(1000),u003Cbru003E factory);u003Cbru003E container.setTaskExecutor(executor);u003Cbru003E return container;u003Cbru003E}u003Cbru003Eu003Cu002Fpreu003Eu003Cpu003E然后测试改造效果:u003Cu002Fpu003Eu003Cpreu003ETread name:redis-listener-pool-0u003Cbru003ETread name:redis-listener-pool-0u003Cbru003ETread name:redis-listener-pool-0u003Cbru003E……u003Cbru003Eu003Cu002Fpreu003Eu003Cpu003Eu003Cstrongu003E小结u003Cu002Fstrongu003Eu003Cu002Fpu003Eu003Cpu003E那么问题来了,这个锅到底谁来背,开发、测试还是产品?这么好的宣传机会,直接上头条”XX 电商系统 bug 超卖,亏损超 10W 仍坚持发货,称不能亏了消费者”然后超的钱相关责任人担一部分, perfect~。本故事纯属虚构,谁也不怪,如有雷同,纯属巧合。u003Cu002Fpu003Eu003Cpu003Eu003Cstrongu003E源码u003Cu002Fstrongu003Eu003Cu002Fpu003Eu003Cpu003E分布式秒杀现场:https:u002Fu002Fgitee.comu002F52itstyleu002Fspring-boot-seckillu003Cu002Fpu003Eu003Cu002Fdivu003E”

原文始发于:大型车祸现场,电商秒杀超卖,这个锅到底有谁来背?

主题测试文章,只做测试使用。发布者:逗乐男神i,转转请注明出处:http://www.cxybcw.com/26496.html

联系我们

13687733322

在线咨询:点击这里给我发消息

邮件:1877088071@qq.com

工作时间:周一至周五,9:30-18:30,节假日休息

QR code