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

一、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层,你应该考虑如何设计一个优雅的接口,去实现相应的功能。

猜你喜欢

转载自blog.csdn.net/a_helloword/article/details/80721455
今日推荐