美文网首页
SSM实现高并发秒杀功能之Service层

SSM实现高并发秒杀功能之Service层

作者: 秃头哥编程 | 来源:发表于2018-06-17 22:58 被阅读0次

    一、Service层的设计

    12.png

    (1)编写service接口

    /**
     * 秒杀的业务接口
     * @author liu
     */
    public interface SeckillService {
        /**
         * 查询所有秒杀的商品
         * @return
         */
        List<Seckill> getSeckillList();
        
        /**
         * 查询单个秒杀的商品
         * @param seckillId
         * @return
         */
        Seckill getById(long seckillId);
        
        /**
         * 到了秒杀的时间,暴露秒杀的地址
         * @param seckillId
         * @return 秒杀的地址
         */
        Exposer exportSeckillUrl(long seckillId);
        
        /**
         * 执行秒杀,验证用户手机号
         * @param seckillId
         * @param userPhone
         * @param MD5
         * @return 秒杀的结果
         * @throws SeckillException
         * @throws SeckillRepeatException
         * @throws SeckillCloseException
         */
        SeckillExecution executeSeckill(long seckillId, long userPhone, String MD5) 
                throws SeckillException, SeckillRepeatException, SeckillCloseException;
    }
    

    可以看到定义了四个方法,重点讲讲后两个方法。

    • 暴露秒杀地址:可以看到返回值是Exposer,该类主要定义了一些字段,用于判断是否开启秒杀地址。

    • 执行秒杀:该类是核心方法,返回值是SeckillExecution,SeckillExecution类封装的是秒杀的结果(包括成功秒杀和秒杀失败),在执行秒杀的方法中,如果秒杀成功,则减少库存,增加秒杀记录,并封装秒杀结果,这里要使用spring事务控制。

    (2)编写service接口的实现类

    @Service
    public class SeckillServlceImpl implements SeckillService {
        // 获取日志对象
        private Logger logger = LoggerFactory.getLogger(this.getClass());
        // 秒杀dao的对象
        @Autowired
        private SeckillDao sd;
        // 秒杀成功dao的对象
        @Autowired
        private SuccessKilledDao skd;
        
        // MD5盐值字符串,用于混淆MD5
        private final String salt = "hjhad892998^456#$(@8K";
        
        @Override
        /**
         * 获取所有的秒杀商品
         */
        public List<Seckill> getSeckillList() {
            return sd.queryAll(0, 4);
        }
    
        @Override
        /**
         * 根据id获取秒杀商品
         */
        public Seckill getById(long seckillId) {
            return sd.queryById(seckillId);
        }
    
        @Override
        /**
         * 暴露秒杀地址
         */
        public Exposer exportSeckillUrl(long seckillId) {
            Seckill seckill = sd.queryById(seckillId);
            // 如果要秒杀的商品不存在
            if(seckill == null) {
                // 调用相关的构造函数
                return new Exposer(false, seckillId);
            }
            // 获取当前的时间
            Date date = new Date();
            // 获取秒杀开启时间
            Date startTime = seckill.getStartTime();
            // 获取秒杀结束时间
            Date endTime = seckill.getEndTime();
            // 如果当前时间大于秒杀结束时间或小于秒杀开始时间,则不开启秒杀接口
            if(date.getTime() < startTime.getTime() ||
                    date.getTime() > endTime.getTime()) {
                return new Exposer(false, seckillId, date.getTime(), startTime.getTime(), endTime.getTime());
            }
            // 转化特定字符串的过程,不可逆
            String MD5 = getMD5(seckillId);
            return new Exposer(true, MD5, seckillId);
        }
        
        /**
         * 获取MD5
         * @return
         */
        private String getMD5(long seckillId) {
            String base = seckillId + "/" + salt;
            String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
            return md5;
        }
        
        @Override
        /**
         * 执行秒杀
         * 使用注解控制事务的优点
         * 1 开发团队达成一致约定,明确标注事务方法的编程风格
         * 2 保证事务方法的执行时间尽可能短,不要穿插其他网络请求比如RPC/HTTP等,如果要,请剥离到事务方法外部
         * 3 不是所有方法都需要事务,比如只有一条修改操作,只读操作不需要事务控制
         */
        @Transactional
        public SeckillExecution executeSeckill(long seckillId, long userPhone, String MD5)
                throws SeckillException, SeckillRepeatException, SeckillCloseException {
            if(MD5 == null || !MD5.equals(getMD5(seckillId))) {
                throw new SeckillException("秒杀数据被重写");
            }
            // 执行秒杀的逻辑:减库存+增加购买记录
            Date now = new Date();
            try {
                // 减库存
                int updateCount = sd.reduceNumber(seckillId, now);
                // 如果没有更新记录,则说明秒杀结束
                if(updateCount <= 0) {
                    throw new SeckillCloseException("秒杀结束");
                } else {
                    // 插入秒杀成功的记录
                    int insertCount = skd.insertSuccessKilled(seckillId, userPhone);
                    // 如果插入的记录小于0,说明重复插入了
                    if(insertCount <= 0) {
                        throw new SeckillRepeatException("重复秒杀");
                    } else {
                        // 秒杀成功,查询出插入的秒杀成功的记录
                        SuccessKilled sk = skd.queryByIdWithSeckill(seckillId, userPhone);
                        // 封装成功秒杀结果返回
                        return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, sk);
                    }
                }
            } catch(SeckillCloseException e1) {
                throw e1;
            } catch(SeckillRepeatException e2) {
                throw e2;
            } catch(SeckillException e) {
                logger.error(e.getMessage(), e);
                throw new SeckillException("秒杀错误:" + e.getMessage());
            }
        }
    }
    

    主要注意暴露秒杀接口和执行秒杀这两个方法,清楚里面的逻辑。同时这里使用了MD5加密,防止同一用户重复秒杀。

    同时注意执行秒杀的这个方法中,有三个自定义的异常,都继承了runtimeException,之所以继承runtimeException,是因为运行时异常才能引起spring的事务回滚。

    (3)编写配置文件

        <!-- 扫描service包下所有的注解 -->  
         <context:component-scan base-package="com.codeliu.service"></context:component-scan>
         
         <!-- 配置事务管理器 -->
         <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <!-- 注入数据源 -->
            <property name="dataSource" ref="dataSource"></property>
         </bean>
         
         <!-- 配置基于注解的声明式事务 -->
         <tx:annotation-driven transaction-manager="transactionManager"/>
    

    这里配置了事务管理器,其他的都使用注解。

    (4)编写测试类进行测试

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(value = {"classpath:spring/spring-dao.xml", "classpath:spring/spring-service.xml"})
    public class SeckillServiceTest {
        
        private final Logger logger = LoggerFactory.getLogger(this.getClass());
        
        @Autowired
        private SeckillService seckillService;
    
        @Test
        public void testGetSeckillList() {
            List<Seckill> list = seckillService.getSeckillList();
            // 输出时会把list放入占位符{}中
            logger.info("list = {}" + list);
        }
        
        @Test
        public void testGetById() { 
            long id = 1000L;
            Seckill seckill = seckillService.getById(id);
            logger.info("seckill = {}" + seckill);
        }
        
        @Test
        /**
         * exposer = {}Exposer [exposed=true, MD5=073ece008a409c7bc971949f1b183fe9, seckillId=1000, now=0, start=0, end=0]
         */
        public void testSeckillLogic() {
            long id = 1000L;
            Exposer exposer = seckillService.exportSeckillUrl(id);
            // 如果秒杀已经开始了
            if(exposer.isExposed()) {
                long userPhone = 18970718197L;
                String md5 = exposer.getMD5();
                try {
                    SeckillExecution se = seckillService.executeSeckill(id, userPhone, md5);
                    logger.info("se = {}" + se);
                } catch(SeckillCloseException e1) {
                    throw e1;
                } catch(SeckillRepeatException e2) {
                    throw e2;
                }
            } else {
                // 秒杀未开始,打印警告
                logger.warn("exposer = {}" + exposer);
            }
            
        }
    }
    

    二、遇到的错误

    (1)如下

    com.mchange.v2.resourcepool.TimeoutException: A client timed out while waiting to acquire a resource from com.mchange.v2.resourcepool.BasicResourcePool@4c3487 -- timeout at awaitAvailable()
    

    出现该错误的原因,并不是代码写出了,是因为我配置c3p0私有属性的时候,把一个属性的值配置的太小了,如下

    <!-- 设置连接超时时间 ,电脑卡,这个数值得配大点,不然一直超时-->
    <property name="checkoutTimeout" value="1000"></property>
    

    把数值改大后就解决了,这个视情况而定,因为我的电脑太卡了。

    三、总结

    • 在设计一个项目的时候,没写完一个阶段的代码,就应该进行单元测试,不要在全部写完后才进行测试,这样debug会很困难,大神除外。

    • 在service层,你应该考虑如何设计一个优雅的接口,去实现相应的功能。

    相关文章

      网友评论

          本文标题:SSM实现高并发秒杀功能之Service层

          本文链接:https://www.haomeiwen.com/subject/owqreftx.html