绝大多数秒杀系统都须要实现高并发,这样就必须在原来的项目基础上进行优化。简单的优化颇有可能就会很大地提升系统的并发性能,可是这些优化每每是系统开发人员不多注意的,或者直接被人们忽略。所以要成为一个出色的开发人员,学会优化技巧与时刻具有系统优化的意识是必须的。java
http://git.oschina.net/COOLFLYCOOL/seckillmysql
先是UPDATE货存(货存减1),再是INSERT购买明细。中间可能会出现重复秒杀,秒杀结束,系统内部错误等异常,只要出现异常,事务就会回滚。git
当一个事务开启的时候拿到了数据库表中某一行的行级锁,另外一个事务进来数据库时发现锁住了同一行,若以前的事务不提交或回滚,这个行级锁不会被释放,后面进来的那个事务就要等待行级锁。当第一个事务提交或回滚后,行级锁被释放,第二个事务就能得到这个行级锁进行数据操做,多个事务以此类推,这些过程是一个串行化的操做,也是一个含有大量阻塞的操做。这是MySQL数据库或是绝大多数关系型数据库事务实现的方案。web
注:Java的GC操做:项目中DAO层各数据库操做类经过MyBatis实现的生成相应对象注入spring容器中,当使用后再也不被使用时,就会进行垃圾回收。spring
经过分析事务的行为与秒杀系统瓶颈能够知道,要减小事务等待的时间,削弱阻塞的过程,就要想办法减小行级锁持有的时间。sql
分析:数据库
参照优化思路一,持有行级锁在UPDATE上,INSERT不涉及行级锁(没INSERT以前根本不存在相应的行,更不可能会有行级锁)。所以能够先插入购买明细,这个过程虽然存在网络延迟,可是各个事务之间是能够并行的因此不须要等待,这样就能够减小各个事务一部分的等待与阻塞。实现减小MySQL row lock的持有时间。(但仍是要把UPDATE库存的结果返回给客户端,客户端再决定是否提交事务,即还有2次网络延迟)网络
修改秒杀业务核心代码顺序后:并发
int insertCount = successKilledDao.insertSuccessKilled(seckillId,userPhone,nowTime); //惟一:seckillId,userPhone(联合主键) if(insertCount<=0){ //重复秒杀 throw new RepeatKillException("seckill repeated"); } else { int updateCount = seckillDao.reduceNumber(seckillId, nowTime); if (updateCount <= 0) { //并发量过高,有可能在等行级锁的时候库存没有了,而且秒杀时间问题在前面已经验证。 throw new SeckillCloseException("seckill is closed"); } else { //秒杀成功 SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone); return new SeckillExecution(seckillId, SeckillStateEnums.SUCCESS, successKilled); //枚举 } }
参照优化思路二,利用存储过程将秒杀业务核心事务SQL放在MySQL端执行,这样就能够避免事务执行过程当中的网络延迟与GC影响,事务行级锁持有时间几乎就是数据库数据操做的时间。大大削弱了事务等待的阻塞效应。高并发
秒杀核心SQL事务存储过程:
DELIMITER // CREATE PROCEDURE excuteSeckill(IN fadeSeckillId INT,IN fadeUserPhone VARCHAR (15),IN fadeKillTime TIMESTAMP ,OUT fadeResult INT) BEGIN DECLARE insertCount INT DEFAULT 0; START TRANSACTION ; INSERT IGNORE success_killed(seckill_id,user_phone,state,create_time) VALUES(fadeSeckillId,fadeUserPhone,0,fadeKillTime); --先插入购买明细 SELECT ROW_COUNT() INTO insertCount; IF(insertCount = 0) THEN ROLLBACK ; SET fadeResult = -1; --重复秒杀 ELSEIF(insertCount < 0) THEN ROLLBACK ; SET fadeResult = -2; --内部错误 ELSE --已经插入购买明细,接下来要减小库存 UPDATE seckill SET number = number -1 WHERE seckill_id = fadeSeckillId AND start_time < fadeKillTime AND end_time > fadeKillTime AND number > 0; SELECT ROW_COUNT() INTO insertCount; IF (insertCount = 0) THEN ROLLBACK ; SET fadeResult = 0; --库存没有了,表明秒杀已经关闭 ELSEIF (insertCount < 0) THEN ROLLBACK ; SET fadeResult = -2; --内部错误 ELSE COMMIT ; --秒杀成功,事务提交 SET fadeResult = 1; --秒杀成功返回值为1 END IF; END IF; END // DELIMITER ; SET @fadeResult = -3; CALL excuteSeckill(8,13813813822,NOW(),@fadeResult); SELECT @fadeResult;
Java客户端(MyBatis)调用数据库存储过程:
首先,在Dao层新建一个接口:void killByProcedure(Map [泛型:String,Object] paramMap); 而后在相应的XML中配置实现(注意:jdbcType没有INT类型的枚举,要使用BIGINT;一样没有VARCHAR的枚举,要使用BIGINT代替。):
<!--MyBatis调用存储过程 --> <select id="killByProcedure" statementType="CALLABLE"> CALL executeSeckill( #{ seckillId , jdbcType = BIGINT , mode= IN }, #{ phone ,jdbcType = BIGINT , mode= IN }, #{ killTime , jdbcType = TIMESTAMP , mode= IN }, #{ result , jdbcType = BIGINT , mode= OUT } ) </select>
而后,Service层从新写入一个方法SeckillExecution executeSeckillProcedure(int seckillId, String userPhone, String md5);(注意:在使用MapUtils时要注入commons-collections 3.2依赖)
public SeckillExecution executeSeckillProcedure(int seckillId, String userPhone, String md5) { if( md5==null || !md5.equals(getMD5(seckillId)) ){ return new SeckillExecution(seckillId,SeckillStateEnums.DATA_REWRITE); } Timestamp nowTime = new Timestamp(System.currentTimeMillis()); Map<String,Object> map = new HashMap<String,Object>(); map.put("seckillId",seckillId); map.put("phone",userPhone); map.put("killTime",nowTime); map.put("result", null); try{ seckillDao.killByProcedure(map); int result = MapUtils.getInteger(map,"result",-2); if(result == 1){ SuccessKilled sk = successKilledDao.queryByIdWithSeckill(seckillId,userPhone); return new SeckillExecution(seckillId,SeckillStateEnums.SUCCESS,sk); } else{ return new SeckillExecution(seckillId,SeckillStateEnums.stateOf(result)); } } catch (Exception e){ logger.error(e.getMessage(),e); return new SeckillExecution(seckillId,SeckillStateEnums.INNER_ERROR); } }
再者,在web-control层将调用方法改为executeSeckillProcedure,同时由于executeSeckillProcedure已经将重复秒杀,秒杀结束(无库存)合并到返回的SeckillExecution中,因此不用再捕获这两个异常(本来在service层要抛出这两个异常,是为了告诉Spring声明式事务该程序出错要进行事务回滚)
try{ SeckillExecution seckillExecution = seckillService.executeSeckillProcedure(seckillId,phone,md5); return new SeckillResult<SeckillExecution>(true,seckillExecution); } catch (Exception e){ logger.error(e.getMessage(),e); SeckillExecution seckillExecution = new SeckillExecution(seckillId, SeckillStateEnums.INNER_ERROR); return new SeckillResult<SeckillExecution>(true,seckillExecution); }
最后,集成测试web层:
可见秒杀成功,重复秒杀,秒杀结束都正常进行!