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

本文详细介绍了一个秒杀系统的Service层设计与实现过程,包括编写Service接口及其实现类、配置文件及测试类等内容。通过具体代码示例展示了如何暴露秒杀地址及执行秒杀逻辑,同时介绍了在开发过程中可能遇到的问题及解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值