为了账号安全,请及时绑定邮箱和手机立即绑定

慕课网《Java高并发秒杀API之高并发优化》学习总结

标签:
Java

慕课网《Java高并发秒杀API之高并发优化》学习总结

时间:2017年08月25日星期五
说明:本文部分内容均来自慕课网。@慕课网:http://www.imooc.com
教学源码:无
学习源码:https://github.com/zccodere/study-imooc
第一章:优化分析
1-1 优化分析(上)

学习本门课程前请先学习前置课程《Java高并发秒杀API之业务分析与DAO层》和《Java高并发秒杀API之Service层》和《Java高并发秒杀API之web层》

首先搞清楚高并发发生在哪?

图片描述

详情页

图片描述

CDN的理解

CDN(内容分发网络)加速用户获取数据的系统
部署在离用户最近的网络节点上
命中CDN不需要访问后端服务器
互联网公司自己搭建或租用

秒杀地址接口分析

无法使用CDN缓存
适合服务端缓存redis等
一致性维护成本低

秒杀地址接口优化

图片描述

秒杀操作优化分析

无法使用CDN缓存
后端缓存困难:库存问题
一行数据竞争:热点商品
1-2 优化分析(下)

秒杀其他方案分析

图片描述

成本分析

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

Java控制事务行为分析

图片描述

瓶颈分析

图片描述

优化分析

行级锁在Commit之后释放
优化方向减少行级锁持有时间

如何判断Update更新库存成功

两个条件
    Update自身没报错
    客户端确认Update影响记录数
优化思路
    把客户端逻辑放到MySQL服务端,避免网络延迟和GC影响

如何放到MySQL服务端:两种解决方案

定制SQL方案:update /*+[auto_commit]*/,需要修改MySQL源码
使用存储过程:整个事务在MySQL端完成

优化总结

前端控制:暴露接口,按钮防重复
动静态数据分离:CDN缓存,后端缓存
事务竞争优化:减少事务锁时间
第二章:缓存优化
2-1 后端缓存

代码编写

1.编写RedisDao类

package com.myimooc.seckill.dao.cache;

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

/**
 * @describe Redis数据访问对象
 * @author zc
 * @version 1.0 2017-08-26
 */
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操作逻辑
        try {
            Jedis jedis = jedisPool.getResource();
            try{
                String key = "seckill:"+seckillId;
                // 并没有实现内部序列化操作
                // get->byte[] ->反序列化 ->Object(Seckill)
                // 采用自定义序列化
                // 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){
            logger.error(e.getMessage(),e);
        }
        return null;
    }

    public String putSeckill(Seckill seckill){
        // set Object(Seckill) -> 序列化 ->byte[]
        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){
            logger.error(e.getMessage(),e);
        }
        return null;
    }
}

2.修改SeckillServiceImpl类 输出秒杀接口地址方法

    /**
     * 秒杀开启时输出秒杀接口地址,否则输出系统时间和秒杀时间
     *
     * @param seckillId
     */
    public Exposer exportSeckillUrl(long seckillId) {
        // 优化点:缓存优化:超时的基础上维护一致性
        //1:访问redis
        Seckill seckill = redisDao.getSeckill(seckillId);
        if(null == seckill){
            //2:访问数据库
            seckill = seckillDao.queryById(seckillId);
            if(null == seckill){
                return new Exposer(false,seckillId);
            }else {
                //3:放入redis
                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 = this.getMD5(seckillId);

        return new Exposer(true,md5,seckillId);
    }
第三章:并发优化
3-1 简单优化

回顾事务执行

图片描述

简单优化

图片描述

3-2 深度优化

事务SQL在MySQL端执行(存储过程)

1.编写存储过程

-- 秒杀执行存储过程

DELIMITER $$ -- console ; 转换为 $$

-- 定义存储过程
-- 参数:in 输入参数;out 输出参数
-- row_count():返回上一条修改类型sql的影响行数
-- row_count():0 未修改数据;>0 修改的行数;<0 sql错误或未执行修改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,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 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 ;

set @r_result=-3;
-- 执行存储过程
call execute_seckill(1003,13521542111,new(),@r_result);
select @r_result

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

2.修改SeckillDao.xml

    <!-- mybatis调用存存储过程 -->
    <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>

3.修改SeckillServiceImpl类

    /**
     * 执行秒杀操作,存储过程
     *
     * @param seckillId
     * @param userPhone
     * @param md5
     */
    public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5){
        if(md5 == null || !md5.equals(getMD5(seckillId))){
            return new SeckillExecution(seckillId,SeckillStatEnum.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);
        // 执行存储过程,result被赋值
        try {
            seckillDao.killByProcedure(map);
            // 获取result
            int result = MapUtils.getInteger(map,"result",-2);
            if(result == 1){
                SuccessSeckilled sk = successKilledDao.queryByIdWithSeckill(seckillId,userPhone);
                return new SeckillExecution(seckillId,SeckillStatEnum.SUCCESS,sk);
            }else{
                return new SeckillExecution(seckillId,SeckillStatEnum.stateOf(result));
            }
        }catch (Exception e){
            logger.error(e.getMessage(),e);
            return new SeckillExecution(seckillId,SeckillStatEnum.INNER_ERROR);
        }
    }
第四章:系统部署
4-1 部署架构

系统可能用到哪些服务

CDN(内容分发网络)
WebServer:Nginx+Jetty
Redis:服务器端缓存
MySQL:数据存储服务

大型系统部署架构

图片描述

可能参与的角色

开发:前端+后端
测试
DBA
运维
第五章:课程总结
5-1 课程总结

数据层技术回顾

数据库设计和实现
MyBatis理解和使用技巧
MyBatis整合Spring技巧

业务层技术回顾

业务接口设计和封装
SpringIOC配置技巧
Spring声明式事务使用和理解

WEB层技术回顾

Restful接口运用
SpringMVC使用技巧
前端交互分析过程
Bootstrap和js使用

并发优化

系统瓶颈点分析
事务、锁、网络延迟理解
前端CDN、缓存等理解使用
集群化部署
点击查看更多内容
12人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
138
获赞与收藏
532

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消