Java高并发秒杀API之高并发优化

Posted twoheads

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java高并发秒杀API之高并发优化相关的知识,希望对你有一定的参考价值。

---恢复内容开始---

第1章 秒杀系统高并发优化分析

 

 

 

 

1.为什么要单独获得系统时间

访问cdn这些静态资源不用请求系统服务器

而CDN上没有系统时间,需要单独获取,获取系统时间不用优化,只是new了一个日期对象返回,java访问一次内存(cacheline)的时间大概为10ns,即一秒可可访问一亿次

倒计时放在js端,在浏览器中,不会对服务器端造成影响,也不用优化

 

 2.秒杀地址接口分析

秒杀未开启,秒杀开启,秒杀结束,秒杀地址返回的数据不同,不是静态的,无法使用CDN缓存

但它适合使用redis等服务器端缓存

 

 超时穿透即当缓存超时后,请求穿透缓存直接到达mysql

主动更新即mysql更新后,主动更新到redis

 

3.秒杀操作优化分析

 

 涉及到减库存,无法使用后端缓存,必须通过mysql的事务来维持一致性

 

 4.其他方案分析

 

MQ即消息队列

 

普遍认为mysql低效,但经过测试mysql的QPS很高。

每秒查询率QPS(Query Per Second)

但由于事务及行级锁的存在,update成为了一个串行的操作

 

 

 可能会出现GC,新生代GC会暂停所有事务产生约几十毫秒延迟

Minor GC都会触发(stop-the-world)

除了GC所需的线程外,其他线程都将停止工作,中断了的线程直到GC任务结束才继续它们的任务

 

优化分析:

行级锁在commit或rollback之后释放

优化方向->减少行级锁的持有时间

 

 

 

如果出现GC锁的释放时间又会延长约50ms,并发越高GC也约多

 

 

 update影响记录数即update返回值,若为0则失败

 

修改源码不现实,腾讯曾经做过

用户点击了秒杀按钮,会先禁用按钮,防止不停发送请求,将请求拦截在前端,减轻后端负载

 

第2章 redis后端缓存优化编码

 

用redis优化地址暴露接口

 由于地址暴露接口是根秒杀单的时间来计算是否开启秒杀,是否结束,以及是否在秒杀中,所以不方便作为固定的内容放在CDN中作为缓存,它要放在服务器端,通过服务器端的逻辑去控制。由于这各接口调用也比较频繁,我们不希望它频繁访问数据库

 

原来在官网上可以下载的windows版本的,现在官网以及没有下载地址,只能在github上下载,官网只提供linux版本的下载

redis在windows下安装过程:http://www.cnblogs.com/M-LittleBird/p/5902850.html

我们不去做linux下javaweb环境搭建以及项目部署,目前学习的重点是Java以及Java WEB的相关知识,不是细枝末节的平台、IDE等工具。所以采用windows版的redis

 

在org.myseckill.dao下新建文件夹cache,在其中新建RedisDao如下

public class RedisDao {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    
    private final JedisPool jedisPool;
    //构造方法
    public RedisDao(String id,int port) {
        jedisPool = new JedisPool(id,port);
    }
    
    //只需要知道这个对象是什么class,内部有一个schema描述这个class是什么结构
    //.class是字节码文件,代表这个类的字节码对象,通过反射可以知道字节码文件对应对象有哪些属性和方法。序列化的本质:通过字节码和字节码对应的对象有哪些属性,把字节码的数据传递给那些属性
    private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class);
    
    //不用访问DB直接通过redis拿到Seckill对象
    public Seckill getSeckill(long seckillId) {
        //redis操作逻辑
        try {
            //JedisPool相当于数据库连接池,Jedis相当于数据库的Connection
            Jedis jedis = jedisPool.getResource();
            try {
                String key = "seckill:" + seckillId;
                //redis或它原生的jedis并没有实现内部序列化操作,不像memcached内部做了序列化
                //典型的缓存访问逻辑:get->得到一个二进制数组byte[](无论是什么对象或图片或文字存储都是二进制数组)->反序列化得到Object(Seckill)
                //高并发里面容易被忽视的一个点,序列化问题,jdk自带的序列化机制serializable效率比较低
                //采用自定义序列化,使用开源社区的方案,pom.xml中导入protostuff的依赖
                //protostuff把一个对象转化为二进制数组,传入redis当中。只需要知道这个对象是什么class,内部有一个schema描述这个class是什么结构。要求该对象为pojo,即有getter setter方法的普通java对象
                byte[] bytes = jedis.get(key.getBytes());
                //缓存中获取到
                if(bytes != null) {
                    Seckill seckill = schema.newMessage(); //Seckill的空对象
                    ProtostuffIOUtil.mergeFrom(bytes, seckill, schema);   //把字节数组的数据传入空对象中
                    //Seckill被反序列化
                    return seckill;
                }
            } finally {
                jedis.close();
            }
            
        } catch (Exception e) {
            logger.error(e.getMessage(),e);
        }
        return null;
    }
    
    //当缓存没有时将Seckill放入redis缓存中
    public String putSeckill(Seckill seckill) {
        //把Seckill对象序列化为字节数组放入redis
        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);  //String类型的result,如果错误会返回错误信息,如果成功会返回ok

                return result;
            }finally {
                jedis.close();
            }
        }catch (Exception e) {
            logger.error(e.getMessage(),e);
        }

        return null;
    }

}

pom.xml中导入相关依赖:

    <!-- redis客户端:Jedis -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.7.3</version>
    </dependency>
    <!-- protolstuff序列化依赖 -->
    <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>
   </dependencies>

spring-dao.xml中注入该bean

    <!-- redisDao -->
    <bean id="redisDao" class="org.myseckill.dao.cache.RedisDao">
       <!-- 构造方法注入 -->
       <constructor-arg index="0" value="localhost"/>
       <constructor-arg index="1" value="6379"/> 
    </bean>

 

 

 RedisDao的测试类:

@RunWith(SpringJUnit4ClassRunner.class)
//告诉junit spring的配置文件
@ContextConfiguration({"classpath:spring/spring-dao.xml"})
public class RedisDaoTest {
    
    private long id = 1001;
    
    @Autowired
    private RedisDao redisDao;
    
    @Autowired
    private SeckillDao seckillDao;

    @Test
    public void testSeckill() {
        //全局测试 get and put
        Seckill seckill = redisDao.getSeckill(id);
        if(seckill == null) {
            seckill = seckillDao.queryById(id);
            if(seckill != null) {
                String result = redisDao.putSeckill(seckill);
                System.out.println(result);
                seckill = redisDao.getSeckill(id);
                System.out.println(seckill);
            }
        }
        
    }

}

输出

OK
Seckill{seckillId=1001, name=\'800元秒杀ipad\', number=200, startTime=Mon Mar 26 00:00:00 CST 2018, endTime=Sun Apr 15 00:00:00 CST 2018, createTime=Fri Dec 29 23:04:08 CST 2017}

 

修改SeckillServiceImpl,加入缓存优化并测试

    @Autowired
    private RedisDao redisDao;

@Override
    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);
            }

        }

 

最后查看redis,发现缓存中已有数据

 

 

第3章 并发优化

1.简单优化

 

网络延迟是指update或insert操作返回结果到java客户端进行逻辑判断的延迟(下一节用存储过程在mysql本地执行科避免)

 只有update操作需要获取行级锁

insert操作冲突的概率很小(重复秒杀时),可以看做并行的

 

调换update和insert的位置

 

先update的情况下,第一个事务到update锁住了,其他的全都在等,第一个update执行完,还要再去执行insert,所以持有锁的时间时间相当于是update+insert
先insert的情况下,insert并行,前一个事务到update锁住了,其他的在执行insert,所以持有锁的时间就是只有一个update

更新库存发现更新失败(此时影响结果行数为0)会回滚事务,清除掉前面插入的购买明细,所以不存在超卖问题 

 

 2.深度优化

 利用存储过程,将事务SQL打包到在MySQL端执行

存储过程说白了就是一堆 SQL 的合并。中间加了点逻辑控制。

但是存储过程处理比较复杂的业务时比较实用。 比如说,一个复杂的数据操作。如果你在前台处理的话。可能会涉及到多次数据库连接。但如果你用存储过程的话。就只有一次。从响应时间上来说有优势。

 insert和update的逻辑比较简单,我这里并没有使用存储过程

 

优势主要体现在: 1.存储过程只在创造时进行编译,以后每次执行存储过程都不需再重新编译,而一般 SQL 语句每执行一次就编译一次,所以使用存储过程可提高数据库执行速度。 2.当对数据库进行复杂操作时(如对多个表进行Update,Insert,Query,Delete时),可将此复杂操作用存储过程封装起来与数据库提供的事务处理结合一起使用。这些操作,如果用程序来完成,就变成了一条条的 SQL 语句,可能要多次连接数据库。而换成存储,只需要连接一次数据库就可以了。

 

第4章 系统部署架构

 

 

 

 

 

 

第5章 课程总结


数据层技术回顾:
数据库设计和实现;Mybatis理解与使用技巧;Mybatis整合Spring技巧
业务层技术回顾:
业务接口设计和封装(站在使用者的角度设计);SpringIOC配置技巧;Spring声明式事务使用与理解
WEB技术回顾:
前端交互设计过程,Restful接口设计,SpringMVC使用技巧,Bootstrap和JS的使用
并发优化:
系统瓶颈点分析;事务,锁,网路延迟理解;前端,CDN,缓存等理解使用;集群化部署

 

---恢复内容结束---

以上是关于Java高并发秒杀API之高并发优化的主要内容,如果未能解决你的问题,请参考以下文章

SSM实战——秒杀系统之高并发优化

01 整合IDEA+Maven+SSM框架的高并发的商品秒杀项目之业务分析与DAO层

Java开发之高并发必备篇——线程基础

Java开发之高并发必备篇——线程基础

(百度云百度网盘)11Java秒杀系统方案优化 高性能高并发实战

Java高并发秒杀API之业务分析与DAO层