精华内容
下载资源
问答
  • 列举Java高并发面试题附答案解析
    千次阅读
    2021-03-17 21:45:05

    Java高并发面试题是程序员面试过程中的必修课,只有熟练掌握这些技术要点,在我们的学习中才会脱颖而出,在这里,达内石家庄Java培训老师作深入解答。

    Java并发面试题附答案

    1. 什么是原子操作?在Java Concurrency API中有哪些原子类(atomic classes)?

    原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。

    int++并不是一个原子操作,所以当一个线程读取它的值并加1时,另外一个线程有可能会读到之前的值,这就会引发错误。

    为了解决这个问题,必须保证增加操作是原子的,在JDK1.5之前我们可以使用同步技术来做到这一点。到JDK1.5,java.util.concurrent.atomic包提供了int和long类型的装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。

    2. Java Concurrency API中的Lock接口(Lock interface)是什么?对比同步它有什么优势?

    Lock接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

    它的优势有:

    · 可以使锁更公平

    · 可以使线程在等待锁的时候响应中断

    · 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间

    · 可以在不同的范围,以不同的顺序获取和释放锁

    3. 什么是Executors框架?

    Executor框架同java.util.concurrent.Executor 接口在Java 5中被引入。Executor框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。

    无限制的创建线程会引起应用程序内存溢出。所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用Executors框架可以非常方便的创建一个线程池。

    4. 什么是阻塞队列?如何使用阻塞队列来实现生产者-消费者模型?

    java.util.concurrent.BlockingQueue的特性是:当队列是空的时,从队列中获取或删除元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞。

    阻塞队列不接受空值,当你尝试向队列中添加空值的时候,它会抛出NullPointerException。阻塞队列的实现都是线程安全的,所有的查询方法都是原子的并且使用了内部锁或者其他形式的并发控制。BlockingQueue 接口是java collections框架的一部分,它主要用于实现生产者-消费者问题。

    5. 什么是Callable和Future?

    Java 5在concurrency包中引入了java.util.concurrent.Callable 接口,它和Runnable接口很相似,但它可以返回一个对象或者抛出一个异常。

    Callable接口使用泛型去定义它的返回类型。Executors类提供了一些有用的方法去在线程池中执行Callable内的任务。由于Callable任务是并行的,我们必须等待它返回的结果。java.util.concurrent.Future对象为我们解决了这个问题。在线程池提交Callable任务后返回了一个Future对象,使用它我们可以知道Callable任务的状态和得到Callable返回的执行结果。Future提供了get()方法让我们可以等待Callable结束并获取它的执行结果。

    6. 什么是FutureTask?

    FutureTask是Future的一个基础实现,我们可以将它同Executors使用处理异步任务。通常我们不需要使用FutureTask类,单当我们打算重写Future接口的一些方法并保持原来基础的实现是,它就变得非常有用。我们可以仅仅继承于它并重写我们需要的方法。

    7.什么是并发容器的实现?

    Java集合类都是快速失败的,这就意味着当集合被改变且一个线程在使用迭代器遍历集合的时候,迭代器的next()方法将抛出ConcurrentModificationException异常。

    并发容器支持并发的遍历和并发的更新。主要的类有ConcurrentHashMap, CopyOnWriteArrayList 和CopyOnWriteArraySet。

    8. Executors类是什么?

    Executors为Executor,ExecutorService,ScheduledExecutorService,ThreadFactory和Callable类提供了一些工具方法。

    Executors可以用于方便的创建线程池。

    更多相关内容
  • java高并发秒杀api源码

    热门讨论 2016-11-29 23:01:35
    java高并发秒杀api源码
  • DougLee可扩展的网络服务事件驱动Reactor模式基础版多线程版其他变体java.io包中分阻塞IOAPI一览Web服务器,分布式对象系统等等它们的共同特点Read请求解码请求报文业务处理编码响应报文发送响应实际应用中每一个...
  • Java高并发秒杀API(四)之高并发优化

    万次阅读 多人点赞 2017-10-06 17:07:54
    Java高并发秒杀API(四)之高并发优化1. 高并发优化分析 关于并发 并发性上不去是因为当多个线程同时访问一行数据时,产生了事务,因此产生写锁,每当一个获取了事务的线程把锁释放,另一个排队线程才能拿到写锁,QPS...

    Java高并发秒杀API(四)之高并发优化

    1. 高并发优化分析

    关于并发

    并发性上不去是因为当多个线程同时访问一行数据时,产生了事务,因此产生写锁,每当一个获取了事务的线程把锁释放,另一个排队线程才能拿到写锁,QPS(Query Per Second每秒查询率)和事务执行的时间有密切关系,事务执行时间越短,并发性越高,这也是要将费时的I/O操作移出事务的原因。

    在本项目中高并发发生在哪?

    高并发发生的地方

    在上图中,红色的部分就表示会发生高并发的地方,绿色部分表示对于高并发没有影响。

    为什么需要单独获取系统时间?

    这是为了我们的秒杀系统的优化做铺垫。比如在秒杀还未开始的时候,用户大量刷新秒杀商品详情页面是很正常的情况,这时候秒杀还未开始,大量的请求发送到服务器会造成不必要的负担。

    我们将这个详情页放置到CDN中,这样用户在访问该页面时就不需要访问我们的服务器了,起到了降低服务器压力的作用。而CDN中存储的是静态化的详情页和一些静态资源(css,js等),这样我们就拿不到系统的时间来进行秒杀时段的控制,所以我们需要单独设计一个请求来获取我们服务器的系统时间。

    详情页

    CDN(Content Delivery Network)的理解

    CDN

    获取系统时间不需要优化

    因为Java访问一次内存(Cacheline)大约10ns,1s=10亿ns,也就是如果不考虑GC,这个操作1s可以做1亿次。

    秒杀地址接口分析

    • 无法使用CDN缓存,因为CDN适合请求对应的资源不变化的,比如静态资源、JavaScript;秒杀地址返回的数据是变化的,不适合放在CDN缓存;
    • 适合服务端缓存:Redis等,1秒钟可以承受10万qps。多个Redis组成集群,可以到100w个qps. 所以后端缓存可以用业务系统控制。

    秒杀地址接口优化

    秒杀地址接口优化

    秒杀操作优化分析

    • 无法使用cdn缓存
    • 后端缓存困难: 库存问题
    • 一行数据竞争:热点商品

    大部分写的操作和核心操作无法使用CDN,也不可能在缓存中减库存。你在Redis中减库存,那么用户也可能通过缓存来减库存,这样库存会不一致,所以要通过mysql的事务来保证一致性。

    比如一个热点商品所有人都在抢,那么会在同一时间对数据表中的一行数据进行大量的update set操作。

    行级锁在commit之后才释放,所以优化方向是减少行级锁的持有时间。

    延迟问题很关键

    • 同城机房网络(0.5ms~2ms),最高并发性是1000qps。
    • Update后JVM -GC(垃圾回收机制)大约50ms,最高并发性是20qps。并发性越高,GC就越可能发生,虽然不一定每次都会发生,但一定会发生。
    • 异地机房,比如北京到上海之间的网络延迟,进过计算大概13~20ms。

      网络延迟计算

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

    有两个条件:

    1. update自身没报错;
    2. 客户端确认update影响记录数

    优化思路:

    • 把客户端逻辑放到MySQL服务端,避免网络延迟和GC影响

    如何把客户端逻辑放到MySQL服务端

    有两种方案:

    1. 定制SQL方案,在每次update后都会自动提交,但需要修改MySQL源码,成本很高,不是大公司(BAT等)一般不会使用这种方法。
    2. 使用存储过程:整个事务在MySQL端完成,用存储过程写业务逻辑,服务端负责调用。

    接下来先分析第一种方案

    秒杀方案1

    秒杀方案1成本分析

    根据上图的成本分析,我们的秒杀系统采用第二种方案,即使用存储过程。

    优化总结

    • 前端控制

    暴露接口,按钮防重复(点击一次按钮后就变成灰色,禁止重复点击按钮)

    • 动静态数据分离

    CDN缓存,后端缓存

    • 事务竞争优化

    减少事务行级锁的持有时间

    2. Redis后端缓存优化编码

    关于CDN的说明

    由于不同公司提供的CDN的接口暴露不同,不同的公司租用的机房调用的API也不相同,所以慕课网的视频中并没有对CDN的使用过程进行讲解。

    2.1 下载安装Redis

    前往官网下载安装Stable版本的Redis,安装后可以将安装目录添加到系统变量Path里以方便使用,我使用的是Windows系统的Redis,懒得去官网下载的可以点这里下载

    安装后,运行redis-server.exe启动服务器成功,接着运行redis-cli.exe启动客户端连接服务器成功,说明Redis已经安装成功了。

    为什么使用Redis

    Redis属于NoSQL,即非关系型数据库,它是key-value型数据库,是直接在内存中进行存取数据的,所以有着很高的性能。

    利用Redis可以减轻MySQL服务器的压力,减少了跟数据库服务器的通信次数。秒杀的瓶颈就在于跟数据库服务器的通信速度(MySQL本身的主键查询非常快)

    2.2 在pom.xml中配置Redis客户端

    <!--添加Redis依赖 -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.7.3</version>
    </dependency>
    

    Jedis

    Redis有很多客户端,我们的项目是用Java语言写的,自然选择对应Java语言的客户端,而官网最推荐我们的Java客户端是Jedis,在pom.xml里配置了Jedis依赖就可以使用它了,记得要先开启Redis的服务器,Jedis才能连接到服务器。

    由于Jedis并没有实现内部序列化操作,而Java内置的序列化机制性能又不高,我们是一个秒杀系统,需要考虑高并发优化,在这里我们采用开源社区提供的更高性能的自定义序列化工具protostuff。

    2.3 在pom.xml中配置protostuff依赖

    <!--prostuff序列化依赖 -->
    <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>
    

    关于序列化和反序列化

    序列化是处理对象流的机制,就是将对象的内容进行流化,可以对流化后的对象进行读写操作,也可以将流化后的对象在网络间传输。反序列化就是将流化后的对象重新转化成原来的对象。

    在Java中内置了序列化机制,通过implements Serializable来标识一个对象实现了序列化接口,不过其性能并不高。

    2.4 使用Redis优化地址暴露接口

    原本查询秒杀商品时是通过主键直接去数据库查询的,选择将数据缓存在Redis,在查询秒杀商品时先去Redis缓存中查询,以此降低数据库的压力。如果在缓存中查询不到数据再去数据库中查询,再将查询到的数据放入Redis缓存中,这样下次就可以直接去缓存中直接查询到。

    以上属于数据访问层的逻辑(DAO层),所以我们需要在dao包下新建一个cache目录,在该目录下新建RedisDao.java,用来存取缓存。

    RedisDao

    public class RedisDao {
        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操作逻辑
            try {
                Jedis jedis = jedisPool.getResource();
                try {
                    String key = "seckill:" + seckillId;
                    // 并没有实现哪部序列化操作
                    // 采用自定义序列化
                    // protostuff: pojo.
                    byte[] bytes = jedis.get(key.getBytes());
                    // 缓存重获取到
                    if (bytes != null) {
                        Seckill seckill = schema.newMessage();
                        ProtostuffIOUtil.mergeFrom(bytes, seckill, schema);
                        // seckill被反序列化
    
                        return seckill;
                    }
                } finally {
                    jedis.close();
                }
            } catch (Exception e) {
    
            }
            return null;
        }
    
        public String putSeckill(Seckill seckill) {
            try {
                Jedis jedis = jedisPool.getResource();
                try {
                    String key = "seckill:" + seckill.getSeckillId();
                    byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema,
                            LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
                    // 超时缓存
                    int timeout = 60 * 60;// 1小时
                    String result = jedis.setex(key.getBytes(), timeout, bytes);
    
                    return result;
                } finally {
                    jedis.close();
                }
            } catch (Exception e) {
    
            }
    
            return null;
        }
    }
    

    注意

    使用protostuff序列化工具时,被序列化的对象必须是pojo对象(具备setter/getter)

    在spring-dao.xml中手动注入RedisDao

    由于RedisDao和MyBatis的DAO没有关系,MyBatis不会帮我们自动实现该接口,所以我们需要在spring-dao.xml中手动注入RedisDao。由于我们在RedisDao是通过构造方法来注入ip和port两个参数的,所以需要配置,如果不配置这个标签,我们需要为ip和port提供各自的setter和getter(注入时可以没有getter)。

    在这里我们直接把value的值写死在标签里边了,实际开发中需要把ip和port参数的值写到配置文件里,通过读取配置文件的方式读取它们的值。

    <!--redisDao -->
    <bean id="redisDao" class="com.lewis.dao.cache.RedisDao">
        <constructor-arg index="0" value="localhost" />
        <constructor-arg index="1" value="6379" />
    </bean>
    

    修改SeckillServiceImpl

    使用注解注入RedisDao属性

     @Autowired
     private RedisDao redisDao;
    

    修改exportSeckillURI()

    public Exposer exportSeckillUrl(long seckillId) {
        // 优化点:缓存优化:超时的基础上维护一致性
        // 1.访问redis
    
        Seckill seckill = redisDao.getSeckill(seckillId);
        if (seckill == null) {
            // 2.访问数据库
            seckill = seckillDao.queryById(seckillId);
            if (seckill == null) {// 说明查不到这个秒杀产品的记录
                return new Exposer(false, seckillId);
            } else {
                // 3.放入redis
                redisDao.putSeckill(seckill);
            }
        }
    
        // 若是秒杀未开启
        Date startTime = seckill.getStartTime();
        Date endTime = seckill.getEndTime();
        // 系统当前时间
        Date nowTime = new Date();
        if (startTime.getTime() > nowTime.getTime() || endTime.getTime() < nowTime.getTime()) {
            return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime());
        }
    
        // 秒杀开启,返回秒杀商品的id、用给接口加密的md5
        String md5 = getMD5(seckillId);
        return new Exposer(true, md5, seckillId);
    }
    

    2.5 测试类RedisDaoTest

    通过IDE工具快速生成测试类RedisDaoTest,新写一个testSeckill(),对getSeckill和putSeckill方法进行全局测试。

    @RunWith(SpringJUnit4ClassRunner.class)
    // 告诉junit spring的配置文件
    @ContextConfiguration({ "classpath:spring/spring-dao.xml" })
    public class RedisDaoTest {
        private final Logger logger = LoggerFactory.getLogger(this.getClass());
    
        private long id = 1001;
    
        @Autowired
        private RedisDao redisDao;
    
        @Autowired
        private SeckillDao seckillDao;
    
        @Test
        public void testSeckill() {
    
            Seckill seckill = redisDao.getSeckill(id);
            if (seckill == null) {
                seckill = seckillDao.queryById(id);
                if (seckill != null) {
                    String result = redisDao.putSeckill(seckill);
                    logger.info("result={}", result);
                    seckill = redisDao.getSeckill(id);
                    logger.info("seckill={}", seckill);
                }
            }
        }
    
    }
    

    如果测试通过了,会输出result={}OK以及id为1001的商品信息,如果输出的都是null,那说明你没有开启Redis服务器,所以在内存中没有存取到缓存。

    为什么不用Redis的hash来存储对象?

    第一:通过Jedis储存对象的方式有大概三种

    1. 本项目采用的方式:将对象序列化成byte字节,最终存byte字节;
    2. 对象转hashmap,也就是你想表达的hash的形式,最终存map;
    3. 对象转json,最终存json,其实也就是字符串

    第二:其实如果你是平常的项目,并发不高,三个选择都可以,这种情况下以hash的形式更加灵活,可以对象的单个属性,但是问题来了,在秒杀的场景下,三者的效率差别很大。

    第三:结果如下

    10w数据时间内存占用
    存json10s14M
    存byte6s6M
    存jsonMap10s20M
    存byteMap4s4M
    取json7s
    取byte4s
    取jsonmap7s
    取bytemap4s

    第四:你该说了,bytemap最快啊,为啥不用啊,因为项目用了超级强悍的序列化工具啊,以上测试是基于java的序列化,如果改了序列化工具,你可以测试下。

    以上问答源自慕课网的一道问答

    教学视频中张老师对于Redis暴露接口地址的补充

    1. redis事务与RDBMS事务有本质区别,详情见http://redis.io/topics/transactions
    2. 关于spring整合redis。原生Jedis API已经足够清晰。笔者所在的团队不使用任何spring-data整合API,而是直接对接原生Client并做二次开发调优,如Jedis,Hbase等。
    3. 这里使用redis缓存方法用于暴露秒杀地址场景,该方法存在瞬时压力,为了降低DB的primary key QPS,且没有使用库存字段所以不做一致性维护。
    4. 跨数据源的严格一致性需要2PC支持,性能不尽如人意。线上产品一般使用最终一致性去解决,这块相关知识较多,所以没有讲。
    5. 本课程的重点其实不是SSM,只是一个快速开发的方式。重点根据业务场景分析通信成本,瓶颈点的过程和优化思路。
    6. 初学者不要纠结于事务。事务可以降低一致性维护难度,但扩展性灵活性存在不足。技术是死的,人是活的。比如京东抢购使用Redis+LUA+MQ方案,就是一种技术反思。

    3. 秒杀操作——并发优化

    3.1 简单优化

    回顾事务执行

    回顾事务执行

    sql语句的简单优化

    简单优化

    优化SeckillServiceImpl的executeSeckill()

    用户的秒杀操作分为两步:减库存、插入购买明细,我们在这里进行简单的优化,就是将原本先update(减库存)再进行insert(插入购买明细)的步骤改成:先insert再update。

    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");// 秒杀数据被重写了
        }
        // 执行秒杀逻辑:减库存+增加购买明细
        Date nowTime = new Date();
    
        try {
    
            // 否则更新了库存,秒杀成功,增加明细
            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 {
                    // 秒杀成功,得到成功插入的明细记录,并返回成功秒杀的信息 commit
                    SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
                    return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);
                }
            }
        } catch (SeckillCloseException e1) {
            throw e1;
        } catch (RepeatKillException e2) {
            throw e2;
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            // 将编译期异常转化为运行期异常
            throw new SeckillException("seckill inner error :" + e.getMessage());
        }
    
    }
    

    为什么要先insert再update

    首先是在更新操作的时候给行加锁,插入并不会加锁,如果更新操作在前,那么就需要执行完更新和插入以后事务提交或回滚才释放锁。而如果插入在前,更新在后,那么只有在更新时才会加行锁,之后在更新完以后事务提交或回滚释放锁。

    在这里,插入是可以并行的,而更新由于会加行级锁是串行的。

    也就是说是更新在前加锁和释放锁之间两次的网络延迟和GC,如果插入在前则加锁和释放锁之间只有一次的网络延迟和GC,也就是减少的持有锁的时间。

    这里先insert并不是忽略了库存不足的情况,而是因为insert和update是在同一个事务里,光是insert并不一定会提交,只有在update成功才会提交,所以并不会造成过量插入秒杀成功记录。

    3.2 深度优化

    前边通过调整insert和update的执行顺序来实现简单优化,但依然存在着Java客户端和服务器通信时的网络延迟和GC影响,我们可以将执行秒杀操作时的insert和update放到MySQL服务端的存储过程里,而Java客户端直接调用这个存储过程,这样就可以避免网络延迟和可能发生的GC影响。另外,由于我们使用了存储过程,也就使用不到Spring的事务管理了,因为在存储过程里我们会直接启用一个事务。

    3.2.1 写一个存储过程procedure,然后在MySQL控制台里执行它

    -- 秒杀执行储存过程
    DELIMITER $$ -- 将定界符从;转换为$$
    -- 定义储存过程
    -- 参数: in输入参数   out输出参数
    -- row_count() 返回上一条修改类型sql(delete,insert,update)的影响行数
    -- row_count:0:未修改数据 ; >0:表示修改的行数; <0:sql错误
    CREATE PROCEDURE `seckill`.`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 IGNORE INTO success_killed
        (seckill_id, user_phone, state)
        VALUES (v_seckill_id, v_phone, 0);
        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 end_time > v_kill_time
                AND start_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;
          END IF;
        END IF;
      END;
    $$
    -- 储存过程定义结束
    -- 将定界符重新改为;
    DELIMITER ;
    
    -- 定义一个用户变量r_result
    SET @r_result = -3;
    -- 执行储存过程
    CALL execute_seckill(1003, 13502178891, now(), @r_result);
    -- 获取结果
    SELECT @r_result;
    

    注意点

    CREATE PROCEDURE `seckill`.`execute_seckill`
    

    上边这句语句的意思是为一个名为seckill的数据库定义一个名为execute_seckill的存储过程,如果你在连接数据库后使用了这个数据库(即use seckill;),那么这里的定义句子就不能这样写了,会报错(因为存储过程是依赖于数据库的),改成下边这样:

    CREATE PROCEDURE `execute_seckill`
    

    row_count()

    存储过程中,row_count()函数用来返回上一条sql(delete,insert,update)影响的行数。

    根据row_count()返回值,可以进行接下来的流程判断:

    0:未修改数据;

    >0: 表示修改的行数;

    <0: 表示SQL错误或未执行修改SQL

    3.2.2 修改源码以调用存储过程

    SeckillDao里添加调用存储过程的方法声明

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

    接着在SeckillDao.xml里添加该方法对应的sql语句

    <!--调用储存过程 -->
    <select id="killByProcedure" statementType="CALLABLE">
        CALL execute_seckill(
            #{seckillId,jdbcType=BIGINT,mode=IN},
            #{phone,jdbcType=BIGINT,mode=IN},
            #{killTime,jdbcType=TIMESTAMP,mode=IN},
            #{result,jdbcType=INTEGER,mode=OUT}
        )
    </select>
    

    SeckillService接口里添加一个方法声明

    /**
     * 调用存储过程来执行秒杀操作,不需要抛出异常
     * 
     * @param seckillId 秒杀的商品ID
     * @param userPhone 手机号码
     * @param md5 md5加密值
     * @return 根据不同的结果返回不同的实体信息
     */
    SeckillExecution executeSeckillProcedure(long seckillId,long userPhone,String md5);
    

    为什么这个方法不需要抛出异常?

    原本没有调用存储过程的执行秒杀操作之所以要抛出RuntimException,是为了让Spring事务管理器能够在秒杀不成功的时候进行回滚操作。而现在我们使用了存储过程,有关事务的提交或回滚已经在procedure里完成了,前面也解释了不需要再使用到Spring的事务了,既然如此,我们也就不需要在这个方法里抛出异常来让Spring帮我们回滚了。

    SeckillServiceImpl里实现这个方法

    我们需要使用到第三方工具类,所以在pom.xml里导入commons-collections工具类

    <!--导入apache工具类-->
    <dependency>
        <groupId>commons-collections</groupId>
        <artifactId>commons-collections</artifactId>
        <version>3.2.2</version>
    </dependency>
    

    在接口的实现类里对executeSeckillProcedure进行实现

    @Override
    public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) {
        if (md5 == null || !md5.equals(getMD5(seckillId))) {
            return new SeckillExecution(seckillId, SeckillStatEnum.DATE_REWRITE);
        }
        Date killTime = new Date();
        Map<String, Object> map = new HashMap<>();
        map.put("seckillId", seckillId);
        map.put("phone", userPhone);
        map.put("killTime", killTime);
        map.put("result", null);
        // 执行储存过程,result被复制
        seckillDao.killByProcedure(map);
        // 获取result
        int result = MapUtils.getInteger(map, "result", -2);
        if (result == 1) {
            SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
            return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);
        } else {
            return new SeckillExecution(seckillId, SeckillStatEnum.stateOf(result));
        }
    }
    

    接着对该方法进行测试,在原本的SeckillServiceTest测试类里添加测试方法

    @Test
    public void executeSeckillProcedure(){
        long seckillId = 1001;
        long phone = 13680115101L;
        Exposer exposer = seckillService.exportSeckillUrl(seckillId);
        if (exposer.isExposed()) {
            String md5 = exposer.getMd5();
            SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId, phone, md5);
            logger.info("execution={}", execution);
        }
    }
    

    经过测试,发现没有问题,测试通过。然后我们需要把Controller里的执行秒杀操作改成调用存储过程的方法。

        @RequestMapping(value = "/{seckillId}/{md5}/execution",
                method = RequestMethod.POST,
                produces = {"application/json;charset=UTF-8"})
        @ResponseBody
        public SeckillResult<SeckillExecution> execute(@PathVariable("seckillId") Long seckillId,
                                                       @PathVariable("md5") String md5,
                                                       @CookieValue(value = "userPhone",required = false) Long userPhone)
        {
            if (userPhone==null)
            {
                return new SeckillResult<SeckillExecution>(false,"未注册");
            }
    
            try {
                //这里改为调用存储过程
    //            SeckillExecution execution = seckillService.executeSeckill(seckillId, userPhone, md5);
                SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId, userPhone, md5);
                return new SeckillResult<SeckillExecution>(true, execution);
            }catch (RepeatKillException e1)
            {
                SeckillExecution execution=new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL);
                return new SeckillResult<SeckillExecution>(true,execution);
            }catch (SeckillCloseException e2)
            {
                SeckillExecution execution=new SeckillExecution(seckillId, SeckillStatEnum.END);
                return new SeckillResult<SeckillExecution>(true,execution);
            }
            catch (Exception e)
            {
                SeckillExecution execution=new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
                return new SeckillResult<SeckillExecution>(true,execution);
            }
        }
    

    存储过程优化总结

    1. 存储过程优化:事务行级锁持有的时间
    2. 不要过度依赖存储过程
    3. 简单的逻辑依赖存储过程
    4. QPS:一个秒杀单6000/qps

    经过简单优化和深度优化之后,本项目大概能达到一个秒杀单6000qps(慕课网视频中张老师说的),这个数据对于一个秒杀商品来说其实已经挺ok了,注意这里是指同一个秒杀商品6000qps,如果是不同商品不存在行级锁竞争的问题。

    3.3 系统部署架构

    系统可能用到的服务

    CDN:放置一些静态化资源,或者可以将动态数据分离。一些js依赖直接用公网的CDN,自己开发的一些页面也做静态化处理推送到CDN。用户在CDN获取到的数据不需要再访问我们的服务器,动静态分离可以降低服务器请求量。比如秒杀详情页,做成HTML放在cdn上,动态数据可以通过ajax请求后台获取。

    Nginx:作为http服务器,响应客户请求,为后端的servlet容器做反向代理,以达到负载均衡的效果。

    Redis:用来做服务器端的缓存,通过Jedis提供的API来达到热点数据的一个快速存取的过程,减少数据库的请求量。

    MySQL:保证秒杀过程的数据一致性与完整性。

    智能DNS解析+智能CDN加速+Nginx并发+Redis缓存+MySQL分库分表

    大型系统部署架构

    大型系统部署架构,逻辑集群就是开发的部分。

    1. Nginx做负载均衡
    2. 分库分表:在秒杀系统中,一般通过关键的秒杀商品id取模进行分库分表,以512为一张表,1024为一张表。分库分表一般采用开源架构,如阿里巴巴的tddl分库分表框架。
    3. 统计分析:一般使用hadoop等架构进行分析

    在这样一个架构中,可能参与的角色如下:

    项目角色

    本节结语

    至此,关于该SSM实战项目——Java高并发秒杀API已经全部完成,感谢观看本文。

    项目笔记相关链接

    项目源码

    项目视频教程链接

    这是慕课网上的一个免费项目教学视频,名为Java高并发秒杀API,一共有如下四节课程,附带视频传送门(在视频中老师是用IDEA,本文用的是Eclipse)

    高并发的相关推荐

    1. java系统高并发解决方案(转载)
    展开全文
  • 基于SpringBoot实现Java高并发之秒杀系统 技术栈 后端: SpringBoot-2.x + Redis-4.x 前端: Bootstrap + Jquery 测试环境 IDEA + Maven-10.13 + Tomcat8 + JDK8 启动说明 启动前,请配置好 application.yml ...
  • 将课程代码整理,包括数据库,测试没有问题
  • Java高并发秒杀API

    2018-03-16 10:21:04
    代码包含业务分析和DAO层,Service层,Web层和高并发优化!
  • JAVA 并发框架 API 一览 一执行框程序 (Executor) 最常见的用法就是用 Executors 来构造相关的线程池用 CompletionService 来分离生产任务和已经完成的任务 生产者 submit 执行的任务使用者 take 已完成的任务并按照...
  • SSM实战项目——Java高并发秒杀API,详细流程+学习笔记
  • Java高并发秒杀项目源码 项目介绍 此秒杀项目前端使用了Bootstrap、jQuery, 后端主要使用了Spring、SpringMVC、Mybatis, 数据库使用了MySQL, 缓存使用了Redis。同时使用了RESTful的风格,整个项目自底向上开发, 由...
  • SSM实战项目——Java高并发秒杀API 本文包括了项目的完整流程+开发过程中遇到的各种坑的总结+学习笔记和问题扩展,如果觉得README太长,我在blog里进行了分章, 这是完成后的项目,(阿里云到期了orz) 项目截图 ...
  • SSM实战项目——Java高并发秒杀API源码,包含了sql语句,这是个Maven项目
  • Java高并发秒杀API(三)之Web层

    千次阅读 2017-10-05 18:03:54
    Java高并发秒杀API(三)之Web层1. 设计前的分析 Web层内容相关前端交互设计 Restful规范 SpringMVC Bootstrap + jQuery 前端页面流程 详情页流程逻辑 为什么要获取标准系统时间(服务器的时间) 用户可能处在不同...

    Java高并发秒杀API(三)之Web层


    1. 设计前的分析

    Web层内容相关

    • 前端交互设计
    • Restful规范
    • SpringMVC
    • Bootstrap + jQuery

    前端页面流程

    前端页面流程

    详情页流程逻辑

    详情页流程逻辑

    为什么要获取标准系统时间(服务器的时间)

    用户可能处在不同时区,用户的电脑的系统时间可能不同。

    Restful规范

    Restful规范是一种优雅的URI表达方式:/模块/资源/{标识}/集合1/···

    GET -> 查询操作

    POST -> 添加/修改操作(用于非幂等操作)

    PUT -> 修改操作(用于幂等操作)

    DELETE -> 删除操作

    怎么实现Restful接口

    • @RequestMapping(value = “/path”,method = RequestMethod.GET)
    • @RequestMapping(value = “/path”,method = RequestMethod.POST)
    • @RequestMapping(value = “/path”,method = RequestMethod.PUT)
    • @RequestMapping(value = “/path”,method = RequestMethod.DELETE)

    非幂等操作和幂等操作

    幂等性(idempotency)意味着对同一URL的多个请求应该返回同样的结果。在Restful规范中,GET、PUT、DELETE是幂等操作,只有POST是非幂等操作。

    POST和PUT都可以用来创建和更新资源,二者的区别就是前者用于非幂等操作,后者用于幂等操作。

    简单来说,使用POST方法请求创建一个资源,如果将这条请求重复发送N次,就会创建出N个资源;而如果用GET方法请求创建一个资源,就算重复发送该请求N次,也只会创建一个资源(就算第一次请求创建出来的资源)。

    附:《幂等和高并发在电商系统中的使用》

    秒杀API的URL设计

    秒杀API的URL设计

    @RequestMapping的映射技巧

    注解映射技巧

    请求方法细节处理

    1. 请求参数绑定
    2. 请求方法限制
    3. 请求转发和重定向
    4. 数据模型赋值
    5. 返回json数据
    6. Cookie访问

    2. 整合配置SpringMVC框架

    2.1 配置web.xml

    <web-app xmlns="http://java.sun.com/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
                          http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
             version="3.0"
             metadata-complete="true">
        <!--用maven创建的web-app需要修改servlet的版本为3.0 -->
        <!--配置DispatcherServlet -->
        <servlet>
            <servlet-name>seckill-dispatcher</servlet-name>
            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
            <!-- 配置SpringMVC 需要配置的文件 spring-dao.xml,spring-service.xml,spring-web.xml 
                MyBatis -> Spring -> SpringMVC -->
            <init-param>
                <param-name>contextConfigLocation</param-name>
                <param-value>classpath:spring/spring-*.xml</param-value>
            </init-param>
        </servlet>
        <servlet-mapping>
            <servlet-name>seckill-dispatcher</servlet-name>
            <!--默认匹配所有请求 -->
            <url-pattern>/</url-pattern>
        </servlet-mapping>
    </web-app>
    

    注意

    • 这里的Servlet版本是3.0,对应Tomcat7.0版本
    • 由于我们的配置文件都是以spring-开头命名的,所以可以用通配符*一次性全部加载
    • url-pattern设置为/,这是使用了Restful的规范;在使用Struts框架时我们配置的是*.do之类的,这是一种比较丑陋的表达方式

    2.2 在src/main/resources/spring包下建立spring-web.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"
           xmlns:mvc="http://www.springframework.org/schema/mvc"
           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.xsd
            http://www.springframework.org/schema/mvc
            http://www.springframework.org/schema/mvc/spring-mvc.xsd">
    
        <!--配置spring mvc-->
        <!--1,开启springmvc注解模式
        a.自动注册DefaultAnnotationHandlerMapping,AnnotationMethodHandlerAdapter
        b.默认提供一系列的功能:数据绑定,数字和日期的format@NumberFormat,@DateTimeFormat
        c:xml,json的默认读写支持-->
        <mvc:annotation-driven/>
    
        <!--2.静态资源默认servlet配置-->
        <!--
            1).加入对静态资源处理:js,gif,png
            2).允许使用 "/" 做整体映射
        -->
        <mvc:default-servlet-handler/>
    
        <!--3:配置JSP 显示ViewResolver-->
        <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
            <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
            <property name="prefix" value="/WEB-INF/jsp/"/>
            <property name="suffix" value=".jsp"/>
        </bean>
    
        <!--4:扫描web相关的controller-->
        <context:component-scan base-package="com.lewis.web"/>
    </beans>
    

    3. Controller设计

    Controller中的每一个方法都对应我们系统中的一个资源URL,其设计应该遵循Restful接口的设计风格。

    3.1 在java包下新建com.lewis.web包,在该包下新建SeckillController.java

    @Controller
    @RequestMapping("/seckill")//url:模块/资源/{}/细分
    public class SeckillController
    {
        @Autowired
        private SeckillService seckillService;
    
        @RequestMapping(value = "/list",method = RequestMethod.GET)
        public String list(Model model)
        {
            //list.jsp+mode=ModelAndView
            //获取列表页
            List<Seckill> list=seckillService.getSeckillList();
            model.addAttribute("list",list);
            return "list";
        }
    
        @RequestMapping(value = "/{seckillId}/detail",method = RequestMethod.GET)
        public String detail(@PathVariable("seckillId") Long seckillId, Model model)
        {
            if (seckillId == null)
            {
                return "redirect:/seckill/list";
            }
    
            Seckill seckill=seckillService.getById(seckillId);
            if (seckill==null)
            {
                return "forward:/seckill/list";
            }
    
            model.addAttribute("seckill",seckill);
    
            return "detail";
        }
    
        //ajax ,json暴露秒杀接口的方法
        @RequestMapping(value = "/{seckillId}/exposer",
                        method = RequestMethod.GET,
                        produces = {"application/json;charset=UTF-8"})
        @ResponseBody
        public SeckillResult<Exposer> exposer(@PathVariable("seckillId") Long seckillId)
        {
            SeckillResult<Exposer> result;
            try{
                Exposer exposer=seckillService.exportSeckillUrl(seckillId);
                result=new SeckillResult<Exposer>(true,exposer);
            }catch (Exception e)
            {
                e.printStackTrace();
                result=new SeckillResult<Exposer>(false,e.getMessage());
            }
    
            return result;
        }
    
        @RequestMapping(value = "/{seckillId}/{md5}/execution",
                method = RequestMethod.POST,
                produces = {"application/json;charset=UTF-8"})
        @ResponseBody
        public SeckillResult<SeckillExecution> execute(@PathVariable("seckillId") Long seckillId,
                                                       @PathVariable("md5") String md5,
                                                       @CookieValue(value = "userPhone",required = false) Long userPhone)
        {
            if (userPhone==null)
            {
                return new SeckillResult<SeckillExecution>(false,"未注册");
            }
    
            try {
                SeckillExecution execution = seckillService.executeSeckill(seckillId, userPhone, md5);
                return new SeckillResult<SeckillExecution>(true, execution);
            }catch (RepeatKillException e1)
            {
                SeckillExecution execution=new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL);
                return new SeckillResult<SeckillExecution>(true,execution);
            }catch (SeckillCloseException e2)
            {
                SeckillExecution execution=new SeckillExecution(seckillId, SeckillStatEnum.END);
                return new SeckillResult<SeckillExecution>(true,execution);
            }
            catch (Exception e)
            {
                SeckillExecution execution=new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
                return new SeckillResult<SeckillExecution>(true,execution);
            }
        }
    
        //获取系统时间
        @RequestMapping(value = "/time/now",method = RequestMethod.GET)
        @ResponseBody
        public SeckillResult<Long> time()
        {
            Date now=new Date();
            return new SeckillResult<Long>(true,now.getTime());
        }
    }
    

    注意

    SpringMVC在处理Cookie时有个小问题:如果找不到对应的Cookie会报错,所以设置为required=false,将Cookie是否存在的逻辑判断放到代码中来判断。

    关于异常的捕捉

    Service层中的抛出异常是为了让Spring能够回滚,Controller层中捕获异常是为了将异常转换为对应的Json供前台使用,缺一不可。

    3.2 在dto包下新建一个SeckillResult

    //将所有的ajax请求返回类型,全部封装成json数据
    public class SeckillResult<T> {
    
        //请求是否成功
        private boolean success;
        private T data;
        private String error;
    
        public SeckillResult(boolean success, T data) {
            this.success = success;
            this.data = data;
        }
    
        public SeckillResult(boolean success, String error) {
            this.success = success;
            this.error = error;
        }
    
        public boolean isSuccess() {
            return success;
        }
    
        public void setSuccess(boolean success) {
            this.success = success;
        }
    
        public T getData() {
            return data;
        }
    
        public void setData(T data) {
            this.data = data;
        }
    
        public String getError() {
            return error;
        }
    
        public void setError(String error) {
            this.error = error;
        }
    }
    

    注意

    SeckillResult是一个VO类(View Object),属于DTO层,用来封装json结果,方便页面取值;在这里,将其设计成泛型,就可以和灵活地往里边封装各种类型的对象。

    这里的success属性不是指秒杀执行的结果,而是指页面是否发送请求成功,至于秒杀之后是否成功的这个结果则是封装到了data属性里。

    4. 基于Bootstrap开发页面

    由于项目的前端页面都是由Bootstrap开发的,所以需要先去下载Bootstrap或者是使用在线的CDN服务。而Bootstrap又是依赖于jQuery的,所以需要先引入jQuery。

    4.1 在webapp下建立resources目录,接着建立script目录,建立seckill.js

    //存放主要交互逻辑的js代码
    // javascript 模块化(package.类.方法)
    
    var seckill = {
    
        //封装秒杀相关ajax的url
        URL: {
            now: function () {
                return '/seckill/seckill/time/now';
            },
            exposer: function (seckillId) {
                return '/seckill/seckill/' + seckillId + '/exposer';
            },
            execution: function (seckillId, md5) {
                return '/seckill/seckill/' + seckillId + '/' + md5 + '/execution';
            }
        },
    
        //验证手机号
        validatePhone: function (phone) {
            if (phone && phone.length == 11 && !isNaN(phone)) {
                return true;//直接判断对象会看对象是否为空,空就是undefine就是false; isNaN 非数字返回true
            } else {
                return false;
            }
        },
    
        //详情页秒杀逻辑
        detail: {
            //详情页初始化
            init: function (params) {
                //手机验证和登录,计时交互
                //规划我们的交互流程
                //在cookie中查找手机号
                var userPhone = $.cookie('userPhone');
                //验证手机号
                if (!seckill.validatePhone(userPhone)) {
                    //绑定手机 控制输出
                    var killPhoneModal = $('#killPhoneModal');
                    killPhoneModal.modal({
                        show: true,//显示弹出层
                        backdrop: 'static',//禁止位置关闭
                        keyboard: false//关闭键盘事件
                    });
    
                    $('#killPhoneBtn').click(function () {
                        var inputPhone = $('#killPhoneKey').val();
                        console.log("inputPhone: " + inputPhone);
                        if (seckill.validatePhone(inputPhone)) {
                            //电话写入cookie(7天过期)
                            $.cookie('userPhone', inputPhone, {expires: 7, path: '/seckill'});
                            //验证通过  刷新页面
                            window.location.reload();
                        } else {
                            //todo 错误文案信息抽取到前端字典里
                            $('#killPhoneMessage').hide().html('<label class="label label-danger">手机号错误!</label>').show(300);
                        }
                    });
                }
    
                //已经登录
                //计时交互
                var startTime = params['startTime'];
                var endTime = params['endTime'];
                var seckillId = params['seckillId'];
                $.get(seckill.URL.now(), {}, function (result) {
                    if (result && result['success']) {
                        var nowTime = result['data'];
                        //时间判断 计时交互
                        seckill.countDown(seckillId, nowTime, startTime, endTime);
                    } else {
                        console.log('result: ' + result);
                        alert('result: ' + result);
                    }
                });
            }
        },
    
        handlerSeckill: function (seckillId, node) {
            //获取秒杀地址,控制显示器,执行秒杀
            node.hide().html('<button class="btn btn-primary btn-lg" id="killBtn">开始秒杀</button>');
    
            $.get(seckill.URL.exposer(seckillId), {}, function (result) {
                //在回调函数种执行交互流程
                if (result && result['success']) {
                    var exposer = result['data'];
                    if (exposer['exposed']) {
                        //开启秒杀
                        //获取秒杀地址
                        var md5 = exposer['md5'];
                        var killUrl = seckill.URL.execution(seckillId, md5);
                        console.log("killUrl: " + killUrl);
                        //绑定一次点击事件
                        $('#killBtn').one('click', function () {
                            //执行秒杀请求
                            //1.先禁用按钮
                            $(this).addClass('disabled');//,<-$(this)===('#killBtn')->
                            //2.发送秒杀请求执行秒杀
                            $.post(killUrl, {}, function (result) {
                                if (result && result['success']) {
                                    var killResult = result['data'];
                                    var state = killResult['state'];
                                    var stateInfo = killResult['stateInfo'];
                                    //显示秒杀结果
                                    node.html('<span class="label label-success">' + stateInfo + '</span>');
                                }
                            });
                        });
                        node.show();
                    } else {
                        //未开启秒杀(浏览器计时偏差)
                        var now = exposer['now'];
                        var start = exposer['start'];
                        var end = exposer['end'];
                        seckill.countDown(seckillId, now, start, end);
                    }
                } else {
                    console.log('result: ' + result);
                }
            });
    
        },
    
        countDown: function (seckillId, nowTime, startTime, endTime) {
            console.log(seckillId + '_' + nowTime + '_' + startTime + '_' + endTime);
            var seckillBox = $('#seckill-box');
            if (nowTime > endTime) {
                //秒杀结束
                seckillBox.html('秒杀结束!');
            } else if (nowTime < startTime) {
                //秒杀未开始,计时事件绑定
                var killTime = new Date(startTime + 1000);//todo 防止时间偏移
                seckillBox.countdown(killTime, function (event) {
                    //时间格式
                    var format = event.strftime('秒杀倒计时: %D天 %H时 %M分 %S秒 ');
                    seckillBox.html(format);
                }).on('finish.countdown', function () {
                    //时间完成后回调事件
                    //获取秒杀地址,控制现实逻辑,执行秒杀
                    console.log('______fininsh.countdown');
                    seckill.handlerSeckill(seckillId, seckillBox);
                });
            } else {
                //秒杀开始
                seckill.handlerSeckill(seckillId, seckillBox);
            }
        }
    };
    

    脚本文件的技巧

    使用Json来讲JavaScript模块化(类似于Java的package),不要将js都写成一堆,不易维护,页不方便阅读。

    特殊说明

    由于本人的Eclipse内嵌的Tomcat设置的原因,我需要在URL里的所有路径前加上/seckill(我的项目名)才可以正常映射到Controller里对应的方法,如下

    //封装秒杀相关ajax的url
    URL: {
        now: function () {
            return '/seckill/seckill/time/now';
        },
        exposer: function (seckillId) {
            return '/seckill/seckill/' + seckillId + '/exposer';
        },
        execution: function (seckillId, md5) {
            return '/seckill/seckill/' + seckillId + '/' + md5 + '/execution';
        }
    },
    

    如果有同学在后边测试页面时找不到路径,可以将这里的路径里的/seckill删掉

    4.2 编写页面

    WEB-INF目录下新建一个jsp目录,在这里存放我们的jsp页面,为了减少工作量,也为了方便,将每个页面都会使用到的头部文件和标签库分离出来,放到common目录下,在jsp页面中静态包含这两个公共页面就行了。

    关于jsp页面请从源码中拷贝,实际开发中前端页面由前端工程师完成,但是后端工程师也应该了解jQuery和ajax,想要了解本项目的页面是如何实现的请观看慕课网的Java高并发秒杀API之Web层

    静态包含和动态包含的区别

    静态包含会直接将页面包含进来,最终只生成一个Servlet;而动态包含会先将要包含进来的页面生成Servlet后再包含进来,最终会生成多个Servlet。

    存在的坑

    在页面里不要写成<script/>,这样写会导致后边的js加载不了,所以要写成<script></script>

    EL表达式

    startTime是Date类型的,通过${startTime.time}来将Date转换成long类型的毫秒值。

    4.3 测试页面

    先clean下Maven项目,接着编译Maven项目(-X compile命令),然后启动Tomcat,在浏览器输入http://localhost:8080/seckill/seckill/list,成功进入秒杀商品页面;输入http://localhost:8080/seckill/seckill/1000/detail成功进入详情页面。

    配置使用jquery countdown插件

    1.pom.xml

    <dependency>
    
      <groupId>org.webjars.bower</groupId>
    
      <artifactId>jquery.countdown</artifactId>
    
      <version>2.1.0</version>
    
     </dependency>
    

    2.页面

       <script src="${pageContext.request.contextPath}/webjars/jquery.countdown/2.1.0/dist/jquery.countdown.min.js"></script>
    

    其他问题

    关于显示NaN天 NaN时 NaN分 NaN秒的问题,原因是new Date(startTime + 1000),startTime 被解释成一个字符串了。

    解决办法:

    1. new Date(startTime-0 + 1000);
    2. new Date(Number(startTime) + 1000);

    关于分布式环境下的几个问题以及慕课网张老师的回答

    • 根据系统标准时间判断,如果分布式环境下各机器时间不同步怎么办?同时发起的两次请求,可能一个活动开始,另一个提示没开始。

    后端服务器需要做NTP时间同步,如每5分钟与NTP服务同步保证时间误差在微妙级以下。时间同步在业务需要或者活性检查场景很常见(如hbase的RegionServer)

    • 如果判断逻辑都放到后端,遇到有刷子,后端处理这些请求扛不住了怎么办?可能活动没开始,服务器已经挂掉了。

    秒杀开启判断在前端和后端都有,后端的判断比较简单取秒杀单做判断,这块的IO请求是DB主键查询很快,单DB就可以抗住几万QPS,后面也会加入redis缓存为DB减负。

    • 负载均衡问题,比如根据地域在nginx哈希,怎样能较好的保证各机器秒杀成功的尽量分布均匀呢。

    负载均衡包括nginx入口端和后端upstream服务,在入口端一般采用智能DNS解析请求就近进入nginx服务器。后端upstgream不建议采用一致性hash,防止请求不均匀。后端服务无状态可以简单使用轮训机制。nginx负载均衡本身过于简单,可以使用openresty自己实现或者nginx之后单独架设负载均衡服务如Netflix的Zuul等。

    对于流量爆增的造成后端不可用情况,这门课程(Java高并发秒杀API)并没有做动态降级和弹性伸缩架构上的处理,后面受慕课邀请会做一个独立的实战课,讲解分布式架构,弹性容错,微服务相关的内容,到时会加入这方面的内容。

    本节结语

    至此,关于Java高并发秒杀API的Web层的开发与测试已经完成,接下来进行对该秒杀系统进行高并发优化,详情可以参考下一篇文章。

    上一篇文章:Java高并发秒杀API(二)之Service层

    下一篇文章:Java高并发秒杀API(四)之高并发优化

    展开全文
  • Java高并发秒杀API之业务分析与DAO层 Java高并发秒杀API之web层 Java高并发秒杀API之Service层 Java高并发秒杀API之高并发优化 其实这几个流程也就是开发的流程,首先从DAO层开始开发,从后往前开发,开始Coding吧! ...
  • Java高并发编程详解 PDF 下载

    千次阅读 2021-02-12 18:03:32
    深入理解volatile关键字 第12章 volatile关键字的介绍 12.1 初识volatile关键字 12.2 机器硬件CPU 12.3 Java内存模型 12.4 本章总结 第13章 深入volatile关键字 13.1 并发编程的三个重要特性 13.2 JMM如何保证三大...

    推荐序一

    推荐序二

    推荐序三

    推荐序四

    前言

    第一部分 多线程基础

    第1章 快速认识线程

    1.1 线程的介绍

    1.2 快速创建并启动一个线程

    1.3 线程的生命周期详解

    1.4 线程的start方法剖析:模板设计模式在Thread中的应用

    1.5 Runnable接口的引入以及策略模式在Thread中的使用

    1.6 本章总结

    第2章 深入理解Thread构造函数

    2.1 线程的命名

    2.2 线程的父子关系

    2.3 Thread与ThreadGroup

    2.4 Thread与Runnable

    2.5 Thread与JVM虚拟机栈

    2.6 守护线程

    2.7 本章总结

    第3章 Thread API的详细介绍

    3.1 线程sleep

    3.2 线程yield

    3.3 设置线程的优先级

    3.4 获取线程ID

    3.5 获取当前线程

    3.6 设置线程上下文类加载器

    3.7 线程interrupt

    3.8 线程join

    3.9 如何关闭一个线程

    3.10 本章总结

    第4章 线程安全与数据同步

    4.1 数据同步

    4.2 初识synchronized关键字

    4.3 深入synchronized关键字

    4.4 This Monitor和Class Monitor的详细介绍

    4.5 程序死锁的原因以及如何诊断

    4.6 本章总结

    第5章 线程间通信

    5.1 同步阻塞与异步非阻塞

    5.2 单线程间通信

    5.3 多线程间通信

    5.4 自定义显式锁BooleanLock

    5.5 本章总结

    第6章 ThreadGroup详细讲解

    6.1 ThreadGroup与Thread

    6.2 创建ThreadGroup

    6.3 复制Thread数组和ThreadGroup数组

    6.4 ThreadGroup操作

    6.5 本章总结

    第7章 Hook线程以及捕获线程执行异常

    7.1 获取线程运行时异常

    7.2 注入钩子线程

    7.3 本章总结

    第8章 线程池原理以及自定义线程池

    8.1 线程池原理

    8.2 线程池实现

    8.3 线程池的应用

    8.4 本章总结

    第二部分 Java ClassLoader

    第9章 类的加载过程

    9.1 类的加载过程简介

    9.2 类的主动使用和被动使用

    9.3 类的加载过程详解

    9.4 本章总结

    第10章 JVM类加载器

    10.1 JVM内置三大类加载器

    10.2 自定义类加载器

    10.3 本章总结

    第11章 线程上下文类加载器

    11.1 为什么需要线程上下文类加载器

    11.2 数据库驱动的初始化源码分析

    11.3 本章总结

    第三部分 深入理解volatile关键字

    第12章 volatile关键字的介绍

    12.1 初识volatile关键字

    12.2 机器硬件CPU

    12.3 Java内存模型

    12.4 本章总结

    第13章 深入volatile关键字

    13.1 并发编程的三个重要特性

    13.2 JMM如何保证三大特性

    13.3 volatile关键字深入解析

    13.4 本章总结

    第14章 7种单例设计模式的设计

    14.1 饿汉式

    14.2 懒汉式

    14.3 懒汉式+同步方法

    14.4 Double-Check

    14.5 Volatile+Double-Check

    14.6 Holder方式

    14.7 枚举方式

    14.8 本章总结

    第四部分 多线程设计架构模式

    第15章 监控任务的生命周期

    15.1 场景描述

    15.2 当观察者模式遇到Thread

    15.3 本章总结

    第16章 Single Thread Execution设计模式

    16.1 机场过安检

    16.2 吃面问题

    16.3 本章总结

    第17章 读写锁分离设计模式

    17.1 场景描述

    17.2 读写分离程序设计

    17.3 读写锁的使用

    17.4 本章总结

    第18章 不可变对象设计模式

    18.1 线程安全性

    18.2 不可变对象的设计

    18.3 本章总结

    第19章 Future设计模式

    19.1 先给你一张凭据

    19.2 Future设计模式实现

    19.3 Future的使用以及技巧总结

    19.4 增强FutureService使其支持回调

    19.5 本章总结

    第20章 Guarded Suspension设计模式

    20.1 什么是Guarded Suspension设计模式

    20.2 Guarded Suspension的示例

    20.3 本章总结

    第21章 线程上下文设计模式

    21.1 什么是上下文

    21.2 线程上下文设计

    21.3 ThreadLocal详解

    21.4 使用ThreadLocal设计线程上下文

    21.5 本章总结

    第22章 Balking设计模式

    22.1 什么是Balking设计

    22.2 Balking模式之文档编辑

    22.3 本章总结

    第23章 Latch设计模式

    23.1 什么是Latch

    23.2 CountDownLatch程序实现

    23.3 本章总结

    第24章 Thread-Per-Message设计模式

    24.1 什么是Thread-Per-Message模式

    24.2 每个任务一个线程

    24.3 多用户的网络聊天

    24.4 本章总结

    第25章 Two Phase Termination设计模式

    25.1 什么是Two Phase Termination模式

    25.2 Two Phase Termination的示例

    25.3 知识扩展

    25.4 本章总结

    第26章 Worker-Thread设计模式

    26.1 什么是Worker-Thread模式

    26.2 Worker-Thread模式实现

    26.3 本章总结

    第27章 Active Objects设计模式

    27.1 接受异步消息的主动对象

    27.2 标准Active Objects模式设计

    27.3 通用Active Objects框架设计

    27.4 本章总结

    第28章 Event Bus设计模式

    28.1 Event Bus设计

    28.2 Event Bus实战——监控目录变化

    28.3 本章总结

    第29章 Event Driven设计模式

    29.1 Event-Driven Architecture基础

    29.2 开发一个Event-Driven框架

    29.3 Event-Driven的使用

    29.4 本章总结

    展开全文
  • 秒杀----- Java高并发秒杀API之业务分析与DAO层原始码和整理的笔记seckill是项目源码note是整理的笔记
  • Java高并发--消息队列

    2021-02-12 11:33:15
    Java高并发--消息队列举个例子:在购物商城下单后,希望购买者能收到短信或者邮件通知。有一种做法时在下单逻辑执行后调用短信发送的API,如果此时服务器响应较慢、短信客户端出现问题等诸多原因购买者不能正常收到...
  • SSM实战项目——Java高并发秒杀API

    千次阅读 2020-11-16 17:50:15
    https://blog.csdn.net/lewky_liu/article/details/78154502
  • SSM高并发秒杀、红包API设计 maven初始化项目 # 其中archetypeCatalog=internal表示不从远程获取archetype的分类 # groudId对应包名,artifactId对应项目名 # -X指定maven以DEBUG方式运行,输出详细信息。之前的没有...
  • Java高并发秒杀API(一)之业务分析与DAO层 本SSM实战项目使用了Maven进行依赖管理,如果有不清楚Maven是什么的可以参考这篇文章 1. 创建Maven项目和依赖 1.1 创建项目前需要先安装Maven,并设置好环境...
  • Java高并发秒杀API之业务分析与DAO层 第一课代码,不用那么辛苦的敲代码了
  • Java高并发书籍推荐

    千次阅读 2020-05-04 01:45:39
    1.Java并发编程实战 (java并发的圣经) 2.多处理器编程的艺术(并发编程的各种算法,java实现,有点难度) 3.并发的艺术 (多核处理器的共享...6.Java 7并发编程实战手册 (java中的并发编程实践,属于API工具书,...
  • 主要是讲解java并发的教学视频,想学java多线程,这套视频足矣。
  • java秒杀项目源码
  • Java高并发核心编程.卷2,多线程、锁、JMM、JUC、高并发设计模式》 目录 第1章 多线程原理与实战 1.2 无处不在的进程和线程 1.2.1 进程的基本原理 1.2.2 线程的基本原理 1.2.3 进程与线程的区别 1.3 创建...
  • 一个整合SSM框架的高并发和商品秒杀项目,学习目前较流行的Java框架组合实现高并发秒杀API 项目环境的搭建 操作系统 : Ubuntu 16.04 IDE :IntelliJ IDEA 2019.2.5 x64 用Eclipse也一样的,工具时靠人用的 JDK : JDK...
  • 每个服务运行在其独立的进程中,服务与服务间采用轻量级的通信机制互相沟通(通常是基于HTTP的RESTful API)。每个服务都围绕着具体业务进行构建,并且能够被独立地部署到生产环境、类生产环境等。另外,应尽量避免...
  • 部分主要阐述 Thread 的基础知识,详细介绍线程的 API 使用、线程安全、线程间数据通信,以及如何保护共享资源等内容,它是深入学习多线程内容的基础。 第二部分引入了 ClassLoader,这是因为 ClassLoader 与线程...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 167,868
精华内容 67,147
关键字:

java高并发api

java 订阅