五、高并发秒杀系统高并发优化

优化后完整代码下载:https://download.csdn.net/download/qq_34288630/10473394

前面的四篇博客已经将基本的饿秒杀系统完成,本人按照教程一步一步敲代码、测试完成。现在将学习如何在上面的基础上对这个秒杀系统进行优化。

这篇将学习一下内容:
1、高并发系统优化思路分析
2、高并发优化技巧

  • 动静态资源分离:
    CND(内容分发网络):缓存静态资源
    Redis:缓存动态分离
  • 并发优化:
    SQL优化降低行级锁持有时间
    存储过程优化降低行级锁持有时间

3、集群化部署

一、优化分析

结合该高并发系统考虑,哪些是可能出现高并发点呢?
这里写图片描述

上图中,所有红色部分都可能出现高并发的点。

1、为什么单独获取系统时间
在详情页,可能出现用户大量刷新的情况,此时系统应该部署在CDN节点上,此时要做一个静态化的处理,当再次刷新时他获取CDN静态资源(css/js/picture),但是,时间要保持实时的,所以要单独的做处理,单独从服务器系统获取时间,这也是为什么要在详情页单独获取系统时间了。

2、CDN是什么
简介:CDN(内容发布网络),是一个加速用户获取数据的系统;既可以是静态资源,又可以是动态资源,这取决于我们的决策策略,大部分视频加速都依赖于CDN,比如:优酷,爱奇艺等,据此加速;

原理:CDN部署在距离用户最近的网络节点上,用户上网的时候通过网络运营商(电信,长城等)访问距离用户最近的要给城域网网络节点上,然后通过城域网跳到主干网上,主干网则根据访问IP找到访问资源所在的服务器,但是,很大一部分内容在上一层节点已经找到,此时不用往下继续找,直接返回所访问的资源即可。减小了服务器的负担,一般互联网公司都会建立自己的CDN机群或者租用CDN

3、获取系统时间不用优化
获取系统时间的操作不用优化,因为访问一次内存Cacheline大约10ns,1秒内可以做很大数量级的时间获取操作,所以不用什么优化。

4、秒杀地址(Redis缓存技术)

对于秒杀地址暴露的接口可以做缓存呢?

秒杀接口是无法缓存在CDN当中的,因为CDN适合缓存不容易变化的资源,通常是静态资源,比如css/jquery资源,每一个URL对应一个不变的内容,秒杀接口地址每次发生变化的,不适合放在CDN缓存。

但是适合放在服务器端做缓存(后端缓存),比如redis等,下一次访问的时候直接去服务器端缓存里面查找,如果服务器端缓存有了就直接拿走,没有的话再做正常的数据访问处理;另外一个原因就是,一致性维护成本很低。

秒杀地址接口的优化策略:
请求地址,先访问redis,如果redis缓存中没有所需要的资源或者访问超时,则直接进入mysql获取系统资源,将获取的内容更新在redis中(策略:超时穿透,主动更新)。

5、秒杀操作
(1)、秒杀操作分析

  • 秒杀操作分析
    对于这种操作,是无法使用CDN优化的,另外,也不适合在后端缓存,因为缓存了其他数据,可能会出现不一致的情况。
    秒杀数据操作的一个困难的点就是一行数据大量用户出现竞争的情况,同时出现大量的update操作,这样该如何优化呢?

(架构+维护点)

设计一个原子计数器(redis/NoSQL来实现)用来记录用户的行为(用分布式MQ实现这个消息队列,即把消息放在MQ当中),然后后端服务器消费此消息并落地(用Mysql实现,落地:记录购买者,能抗住很大的访问量)
但是这个技术有自己的弱点,就是成本方面:

运维成本和稳定性:NoSQL、MQ等;开发成本在数据一致性和回滚方案等;幂等性难以保证:重复秒杀的问题;不适合新手的架构。

  • 为什么不用Mysql来解决秒杀操作
    因为Mysql执行update的减库存比较低效,一条update操作的压力测试结果可以抗住4wQPS,也就是说,一个商品在一秒内。可以被买4W次;

看一下Java控制事务的行为分析:
(执行库存减1操作)
Update table set num=num-1 where id=10 andnum>0,紧接着会进行一个inser购买明细的操作,然后commit/rollback;

然后第二个用户Updatetable set num=num-1 where id=10 and num>0,紧接着等待行锁,获得锁lock,来继续执行,然后后面的用户……

这样下来的话,整个秒杀操作可以说是一种串行化的执行序列

(2)、分析瓶颈所在

update减库存————》insert购买明细———–》commit/rollback:这个过程都存在网络延迟和GC;并非Java和sql本身慢,而是Java和通信比较慢;

所以,Java执行时间+网络延迟时间+GC=这行操作的执行时间(大概在2ms。1秒有500次操作,对于秒杀系统来说这个性能呈指数级下降,并不好)。

(3)、优化思路分析

我们知道行级锁是在commit之后释放的,那么我们优化的方向就是减少行级锁的持有时间。

同城机房需要花0.5-2msmax(1000qps),update之后JVM-GC(50ms) max(20qps);

异地机房一次(北京上海之间额一次update Sql需要20ms。

如何判断update更新库存成功?

两个条件:——Update自身不报错,客户端确认影响记录数

优化思路:

把客户端逻辑放在Mysql服务端,避免网络延迟和GC影响。

那么,如何把逻辑放在Mysql服务端呢?

(4)、两种优化解决方案

  • 定制SQL方案:update/+[auto_commit]/,需要修改Mysql源码;这样可以在SQL执行完之后,直接在服务端完成commit,不用客户端逻辑判断之后来执行是否commit/rollback。 但是这个增加了修改Mysql源码的成本(不推荐)。
  • 使用存储过程:整个事务在MySQL端完成(把整个热点执行放在一个过程当中一次性完成,只需要返回执行的整个结果就行了,这样可以避免网络延迟和GC干扰)。

    6、优化分析总结
    前端控制:暴露接口(动静态数据分离)

按钮防重复(避免重复请求)

动静态数据分离:CDN缓存,后端缓存(redis技术实现的查询)。

事务竞争优化:减少事务锁时间(用Mysql来解决)。

二、Redis后端缓存优化

1、Redis安装

Redis在通常情况下都是使用机群来维护缓存,此处用一个Redis缓存为例。

此处应用的目的:使用redis优化地址接口,暴露接口。

若想使用Redis作为服务端的缓存机制,则应该首先在服务端安装Redis

安装:http://www.runoob.com/redis/redis-tutorial.html

2、优化编码

第一:pom.xml文件引入Redis在java环境下的客户端Jedis。

<!--高并发优化:Redis在java环境中的客户端Jedis -->  
    <dependency>  
        <groupId>redis.clients</groupId>  
        <artifactId>jedis</artifactId>  
        <version>2.7.3</version>  
    </dependency>  
    <!-- protostuff序列化依赖,写自己的序列化方式-->  
    <dependency>  
        <groupId>com.dyuproject.protostuff</groupId>  
        <artifactId>protostuff-core</artifactId>  
        <version>1.0.8</version>  
    </dependency>  
    <dependency>  
        <groupId>com.dyuproject.protostuff</groupId>  
        <artifactId>protostuff-runtime</artifactId>  
        <version>1.0.8</version>  
    </dependency>  

第二:添加一个对象序列化的缓存类RedisDao.java:

为什么要使用对象序列化?

序列化的目的是将一个实现Serializable接口的对象转换成一个字节序列,可以把该字节序列保存起来(例如保存到一个文件里),以后可以随时将该字节序列恢复为原来的对象。

序列化的对象占原有空间的十分之一,压缩速度可以达到两个数量级,同时节省了CPU

Redis缓存对象时需要将其序列化,而何为序列化,实际就是将对象以字节形式存储,这样,不管对象的属性是字符创、整形还是图片、视频等二进制类型

都可以将其保存在字节数组中。对象序列化后便可以持久化保存或者网络传输。需要还原对象时候,只需将字节数组再反序列化即可。

package dao.cache;

import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtostuffIOUtil;
import com.dyuproject.protostuff.runtime.RuntimeSchema;
import entity.Seckill;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

/**
 * @Author:peishunwu
 * @Description:
 * @Date:Created  2018/6/11
 *
 * 注意:往Readis中放入的对象一定要序列化之后再放入,
 * 序列化的目的是将一个实现Serializable接口的对象转换成一个字节序列,可以。把该字节序列保存起来
 * (例如:保存在一个文件里)以后随时将该字节序列恢复原来的对象。
 *序列化的对象占原来空间的十分之一,压缩速度可以达到两个数量级,同时节省了CPU
 *
 * Readis缓存对象时需要将对象序列化,而何为序列化,实际上就是将对象以字节存储,这样不管对象的属性是字符串、整形还是图片、视频等二进制类型,
 * 都可以将其保存在字节数组中。对象序列化后便可以持久化保存或者网络传输。需要还原对象时,只需要将字节数组再反序列化即可。
 *
 * 因为要在项目中用到,所以要添加@Service, 把这个做成一个服务
 * 因为要初始化连接池JedisPool,所以要implements  InitializingBean并调用默认的
 * afterPropertiesSet()方法
 *
 */
@Service
public class RedisDao {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private final JedisPool jedisPool;

    public RedisDao(String ip, int port) {
        //一个简单的配置
        jedisPool = new JedisPool(ip,port);
    }

    private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class);

    public Seckill getSeckill(long seckillId){
        //缓存Redis操作逻辑,而不应该放在Service下,因为这是数据访问层的逻辑
        try{
            Jedis jedis = jedisPool.getResource();
            try{
                String key = "seckill:"+seckillId;
                //并没有实现内部序列化操作
                //get->byte[]->反序列化-》Object(Seckill)
                //采用自定义序列化方式
                //采用自定义的序列化,在pom.xml文件中引入两个依赖protostuff:pojo
                byte[] bytes = jedis.get(key.getBytes());
                //重新获取缓存
                if(bytes != null){
                    Seckill seckill = schema.newMessage();
                    //将bytes按照从Seckill类创建的模式架构scheam反序列化赋值给对象seckill
                    ProtostuffIOUtil.mergeFrom(bytes,seckill,schema);
                    return seckill;
                }
            }finally {
                jedis.close();
            }
        }catch (Exception e){
            logger.error(e.getMessage(),e);
        }
        return null;
    }


    public String putSeckill(Seckill seckill){
        //set Object (Seckill)---》序列化----》bytes[]
        try{
            Jedis jedis = jedisPool.getResource();
            try {
                String key = "seckill:"+seckill.getSeckillId();
                //protostuff工具
                //将seckill对象序列化成字节数组
                byte[] bytes = ProtostuffIOUtil.toByteArray(seckill,schema,
                        LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
                //缓存时间+key标记+对象序列化结果==》放入缓存jedis缓存池,返回结果result(OK/NO)
                int timeout = 60*60;
                String result = jedis.setex(key.getBytes(),timeout,bytes);
                return result;
            }finally {
                jedis.close();
            }
        }catch (Exception e){
            logger.error(e.getMessage(),e);
        }
        return null;
    }
}

第三:配置文件对象注入再spring-dao.xml文件中为序列化缓存类添加注入参数,用于自动实例化对象;

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans.xsd
         http://www.springframework.org/schema/context
         http://www.springframework.org/schema/context/spring-context-3.0.xsd" >

    <!-- 配置整合Mybatis的过程 -->
    <!-- 1:配置数据库相关参数  properties的属性:${url}         -->
    <context:property-placeholder location="classpath:jdbc.properties"/> <!-- 加载配置参数的文件所在地 -->

    <!-- 2:数据库连接池 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!-- 配置连接池属性 -->
        <!-- c3p0连接池的基本属性 -->
        <property name="driverClass" value="${driver}"/>
        <property name="jdbcUrl" value="${url}"/>
        <property name="user" value="${username}"/>
        <property name="password" value="${password}"/>

        <!--连接池的私有属性根据高并发应用场景 -->
        <property name="maxPoolSize" value="30"/><!--连接池最多保留30个对象 -->
        <property name="minPoolSize" value="10"/>

        <!-- 关闭连接后不自动commit -->
        <property name="autoCommitOnClose" value="false"/>
        <!-- 获取连接超时时间 -->
        <property name="checkoutTimeout" value="6000"/>
        <!-- 当前获取连接失败重试次数 -->
        <property name="acquireRetryAttempts" value="2"/>
    </bean>


    <!-- 使用框架趋势:约定大于配置,将相应的文件放在对应包下,通过已配置项可以自动扫描 -->
    <!-- 3:配置 SqlSessionFactory对象  真正的整合配置-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!--注入数据库连接池  -->
        <property name="dataSource" ref="dataSource"/>
        <!-- 配置Mybatis全局配置文件:mybatis-config.xml -->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <!-- 扫描entity包 使用别名org.seckill.entity -->
        <property name="typeAliasesPackage" value="entity"/>
        <!-- 扫描Sql配置文件:mapper需要的xml文件 -->
        <property name="mapperLocations" value="classpath:mapper/*.xml"/>
    </bean>

    <!-- 4: 配置扫描Dao接口包,动态实现Dao接口,注入到spring容器中-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!--注入SqlSessionFactory 使用sqlSessionFactoryBeanName可以在用的时候再找sqlSessionFactory,防止提前初始化-->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
        <!-- 给出需要扫描Dao接口包-->
        <property name="basePackage" value="dao"/>
    </bean>


    <!--高并发优化模块-->
    <bean id="redisDao" class="dao.cache.RedisDao">

        <constructor-arg index="0" value="localhost"/>
        <constructor-arg index="1" value="6379"/>
    </bean>
</beans>

这里写图片描述

第四:编写测试类

package dao;

import dao.cache.RedisDao;
import entity.Seckill;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * @Author:peishunwu
 * @Description:
 * @Date:Created  2018/6/11
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:spring/spring-dao.xml"})
public class RedisDaoTest {

    private long seckillId = 1001;
    @Autowired
    private RedisDao redisDao;

    @Autowired
    private SeckillDao seckillDao;
    @Test
    public void testKill()throws Exception{
        //get  and  put

        //从缓存中获取
        Seckill seckill = redisDao.getSeckill(seckillId);
        if(seckill == null){//缓存中没有就从数据库查询
            seckill = seckillDao.queryById(seckillId);
            if(seckill !=null){
                String result = redisDao.putSeckill(seckill);//缓存序列化对象
                System.out.println("放入缓存结果:"+result);
                seckill = redisDao.getSeckill(seckillId);
                System.out.println(seckill);
            }
        }else {
            System.out.println("从缓存中获取成功:"+seckill);
        }

    }
}

开始测试!
打开一个 cmd 窗口 使用cd命令切换目录到 C:\redis 运行 redis-server.exe redis.windows.conf 。
这里写图片描述

运行测试类!
这里写图片描述

打开redis可视化工具连接本地redis 查看缓存
RedisDesktopManager:

这里写图片描述

测试结果正常!

第五:进一步修改服务端Redis中间件的服务;才能用服务端的缓存
修改SeckillServiceImpl.java

 /**
     * 高并发优化后
     * @param seckillId
     * @return
     */
    @Override
    public Exposer exportSeckillUrl(long seckillId) {
        //优化点:缓存优化(用Redis缓存起来,降低数据库访问压力)
        //通过超时性来维护一致性
        /**
         *
         get from cache
             if null
                get db
             else
                put db
         *
         */
       //1:访问redis
        Seckill seckill = redisDao.getSeckill(seckillId);
        if(seckill == null){
             seckill = seckillDao.queryById(seckillId);
            if(seckill == null){
                return new Exposer(false,seckillId);
            }else {
                redisDao.putSeckill(seckill);
            }
        }
        Date startTime = seckill.getStartTime();
        Date endTime = seckill.getEndTime();
        //系统当前时间
        Date nowTime = new Date();
        if(nowTime.getTime() < startTime.getTime() || nowTime.getTime()>endTime.getTime()){
            return new Exposer(false,seckillId,nowTime.getTime(),startTime.getTime(),endTime.getTime());
        }
        String md5 = getMD5(seckillId);
        return new Exposer(true,md5,seckillId);
    }

三、并发优化

1、优化分析
这一部分主要是针对秒杀进行并发优化的;秒杀操作是作为一个事务来执行的。

前面已经分析过了:Update减库存——–》insert购买明细———-》commit/rollback:这个事务作为一个原子,里面两个过程都存在网络延迟和GC。
这里写图片描述

改为:

原来的流程:
第一阶段:秒杀开始先Update更新库存,根据结果记录数量决定是否插入明细。这个过程中存在网络延迟,数据库事务持有行级锁。

第二阶段:根据插入insert的结果,最后执行commit/rollback,这个阶段也存在网络延迟,数据库事务持有行级锁。

最终:行级锁经历了两次的java代码执行+网络延迟+GC

方案一:SQL执行顺序调整

第一阶段:先插入Insert明细(同时根据主键判断了是否重复秒杀),根据返回结果判断如果不是重复秒杀则表明插入成功,然后进入第二阶段;该阶段虽然存在网络延迟但是没有持有行级锁;

第二阶段:直接拿到行级锁,然后更新Update库存,最后根据返回结果决定commit/rollback;

该阶段持有网络延迟并且持有行级锁。

最终:行级锁经历了一次的java代码执行+网络延迟+GC;这种策略将只在最后的更新操作中持有行级锁,降低了commit/rollback的持有时间,访问速度提高到了原来的2倍。

方案二:服务端使用存储过程

这种策略直接在服务端使用存储过程将两个阶段insert和update操作直接绑定在一起,这样行级锁commit/rollback的持有在mysql端就执行完成结束了,然后通过网络返回结果。
最终:该策略相比于方案一,屏蔽掉了所有的网络延迟,大大的提高了访问速度,可以让mysql获得更高的QPS,所以可以把它叫做深度优化

2、SQL调整优化编码实现
方案一:利用sql顺序的调整减掉一半的行级锁持有时间,在Service实现类SeckillServiceImpl中调整:

 //秒杀是否成功,成功:减库存,增加明细;失败:抛出异常,事务回滚
    /*使用注解控制事务方法的优点:
    1、开发团队达成一致约定,明确标注事务方法的编程风格
    2、保证事务方法的执行时间尽可能短,不要穿插其他网络操作(RPC/HTTP请求),或者剥离到事务方法外部
    3、不是所有的方法都需要事务,如只有一条修改操作或只读操作不需要事务控制*/
    @Transactional
    @Override
    public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException {
        if(md5 == null || !md5.equals(getMD5(seckillId))){
            throw new SeckillException("秒杀数据被重写了 (seckill data rewrite)");//秒杀数据被重写了
            //return new SeckillExecution(seckillId,SeckillStateEnum.DATA_REWRITE);
        }
        //执行秒杀逻辑:减库存+增加购买明细
        Date nowTime = new Date();
        try{

            //高并发优化前
            /*//减库存
            int updateCount = seckillDao.reduceNumber(seckillId,nowTime);
            if(updateCount <= 0){
                //没有更新库存记录,说明秒杀结束
                throw new SeckillCloseException("说明秒杀结束(seckill is closed)");
                //return new SeckillExecution(seckillId,SeckillStateEnum.END);
            }else {
                //否则更新库存成功,秒杀成功,增加明细
                int insertCount = successKilledDao.insertSuccessKilled(seckillId,userPhone);
               //看是否该明细被重复插入,即用户是否重复秒杀
                if(insertCount <= 0){
                    throw new RepeatKillException("重复秒杀(seckill repeated)");
                    //return new SeckillExecution(seckillId,SeckillStateEnum.REPEAT_KILL);
                }else{
                    //秒杀成功,得到成功插入的明细记录,并返回秒杀信息
                    SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId,userPhone);
                    return new SeckillExecution(seckillId,SeckillStateEnum.SUCCESS);
                }
            }*/
            //高并发优化后

            //增加明细
            int insertCount = successKilledDao.insertSuccessKilled(seckillId,userPhone);
            if(insertCount <= 0){
                throw new RepeatKillException("重复秒杀(seckill repeated)");
            }else{
                //减库存
                int updateCount = seckillDao.reduceNumber(seckillId,nowTime);
                if(updateCount <= 0){
                    //没有更新库存记录,说明秒杀结束 ----rollback
                    throw new SeckillCloseException("seckill is closed");
                }else{
                    //秒杀成功,得到成功插入的明细记录,并返回秒杀信息
                    SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId,userPhone);
                    return new SeckillExecution(seckillId,SeckillStateEnum.SUCCESS);
                }
            }
        }catch (SeckillCloseException e1){
            throw e1;
        }catch (RepeatKillException e2){
            throw e2;
        }catch (Exception e){
            logger.error(e.getMessage(),e);
            //所以编译期异常转化为运行期异常
            throw new SeckillException(""+e.getMessage());
        }
    }

3、深度优化

(1)编写sql语句,建立存储过程:

--使用存储过程执行秒杀  
DELIMITER$$ -- console;转换为$$;定义换行符:表示  

-- 定义存储过程  
-- 参数:in 输入参数;out 输出参数  
--row_count():返回上一条修改类型sql(delete,insert,update)的影响行数。  
--row_count():0:未修改数据;>0:表示修改数据的行数;<0:sql错误/未执行修改sql。  
CREATE PROCEDURE execute_seckill(in v_seckill_id bigint,in v_phone bigint,  
                                                in v_kill_time timestamp,out r_result int)  
BEGIN  
    DECLARE insert_count INT DEFAULT 0;  

    START TRANSACTION ;  

    INSERT ignoresuccess_killed(seckill_id,user_phone,create_time)  
    VALUES(v_seckill_id,v_phone,v_kill_time); -- 先插入购买明细  

    SELECT ROW_COUNT() INTO insert_count;  
    IF(insert_count = 0) THEN  
      ROLLBACK ;  
      SET r_result = -1;   -- 重复秒杀  
    ELSEIF(insert_count < 0) THEN  
      ROLLBACK ;  
      SET r_result = -2;   -- 内部错误  
    ELSE  -- 已经插入购买明细,接下来要减少库存  
      update seckill  
      set number = number -1  
      WHERE seckill_id = v_seckill_id  
             AND start_time < v_kill_time  
             AND end_time > v_kill_time  
             AND number > 0;  

      select ROW_COUNT() INTO insert_count;  
      IF (insert_count = 0)  THEN  
        ROLLBACK ;  
        SET r_result = 0;   -- 库存没有了,代表秒杀已经关闭  
      ELSEIF (insert_count < 0) THEN  
        ROLLBACK ;  
        SET r_result = -2;   -- 内部错误  
      ELSE  
        COMMIT ;    -- 秒杀成功,事务提交  
        SET r_result = 1;   -- 秒杀成功返回值为1  
      END IF;  
    END IF;  
  END  
$$  

-- 测试  
DELIMITER;-- 把DELIMITER重新定义还原成分号;  

SET @r_result =-3;  
-- 执行存储过程  
CALLexecute_seckill(1003,18864598658,now(),@r_result);  
-- 获取结果  
select @r_result;  


drop procedure execute_seckill; -- 删除存储过程

按照上述的SQL语句在mysql数据库查询中执行,创建数据库的存储过程execute_seckill,然后用下面的语句执行存储过程测试。

使用存储过程:

  • 使用存储过程优化:降低事务行级锁持有时间;
  • 但是不要过过度依赖存储过程,要是根据实际需求而定;
  • 简单逻辑可以应用存储过程
  • QPS得到提升,一个秒杀单可以接近6000/qps

(2)Service调用Procedure实现

第一:Mybatis在SeckillDao.java接口中, 添加调用存储过程的方法声明:

 /**
     * 秒杀操作优化:使用存储过程执行秒杀
     * @param paramMap
     */
    void killByProcedure(Map<String,Object> paramMap);

第二步,(Mybatis)在SeckillDao.xml配置文件当中,编写SQL语句,带入参数,调用存储过程:

<!--秒杀操作优化存储部分 -->  
   <!-- mybatis调用存储过程 id和接口中的方法想偶同,传入参数-->  
   <select id="killByProcedure"statementType="CALLABLE">  
   callexecute_seckill(  
   #{seckillId,jdbcType=BIGINT,mode=IN},  
   #{phone,jdbcType=BIGINT,mode=IN},  
   #{killTime,jdbcType=TIMESTAMP,mode=IN},  
   #{result,jdbcType=INTEGER,mode=OUT}  
    )  
   </select>   

第三步,在SeckillService.java接口中声明方法executeSeckillProcedure:

/**
     * 执行秒杀操作  通过存储过程
     * @param seckillId
     * @param userPhone
     * @param md5
     * @return
     * @throws SeckillException
     * @throws RepeatKillException
     * @throws SeckillCloseException
     */
    SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5)throws SeckillException, RepeatKillException, SeckillCloseException;

第四步,在SeckillServiceImpl.java这个实现类中实现上述定义的方法,在Java客户端调用存户过程:

/**
     * 通过java客户端调用存储过程
     * @param seckillId
     * @param userPhone
     * @param md5
     * @return
     * @throws SeckillException
     * @throws RepeatKillException
     * @throws SeckillCloseException
     */
    @Override
    public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException {
        if(md5 == null || !md5.equals(getMD5(seckillId))){
            return new SeckillExecution(seckillId,SeckillStateEnum.DATA_REWRITE);
        }
        //执行秒杀逻辑:减库存+增加购买明细
        Date killTime = new Date();
        Map<String,Object> map = new HashMap<String,Object>();
        map.put("seckillId",seckillId);
        map.put("phone",userPhone);
        map.put("killTime",killTime);
        map.put("result",null);
        //执行存储过程
        try{
            seckillDao.killByProcedure(map);
            //获取result
            //此处pom.xml,中引入MapUtil用于获取集合的值
            int result = MapUtils.getInteger(map,"result",-2);
            if (result == 1){
                SuccessKilled sk = successKilledDao.queryByIdWithSeckill(seckillId,userPhone);
                return new SeckillExecution(seckillId, SeckillStateEnum.SUCCESS,sk);
            }else {
                return new SeckillExecution(seckillId, SeckillStateEnum.stateOf(result));
            }

        }catch(Exception e)
        {
            logger.error(e.getMessage(),e);
            return new SeckillExecution(seckillId, SeckillStateEnum.INNER_ERROR);
        }
    }

第六步,在SeckillServiceTest.java类中编写测试方法:

@Test
    public void executeSeckillProcedureTest()
    {
        long seckillId=1001;
        long phone=13476191899l;
        Exposer exposer=seckillService.exportSeckillUrl(seckillId);

        if(exposer.isExposed())
        {
            String md5=exposer.getMd5();
            SeckillExecution execution=seckillService.executeSeckillProcedure(seckillId, phone, md5);
            logger.info(execution.getStateInfo());
            System.out.println(execution.getStateInfo());

        }
    }

4 总结

数据层

数据库技术:数据库设计和实现

Mybatis理解和使用技巧:和数据表对应的entity—–Dao接口–—Dao接口配置sql语句的文件。

Mybatis和Spring的整合技巧:包扫描/对象的注入

业务层技术回顾

站在使用者的角度上进行业务接口设计和封装

SpringIOC配置技巧:注入

Spring声明式事务的使用和理解

Web技术回顾

Restful接口的运用:post/get

Spring MVC的使用技巧

前端交互分析过程

Bootstrap和JS的使用:使用现有的格式,使用模块/对象类似的划分。

并发优化

系统优化点的分析和抽取

事务、锁、网络延迟理解

前端,CDN,缓存等理解和使用

集群化部署

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值