Java高并发秒杀优化

高并发优化分析

  1. 高并发发生在哪里
    红色区域都是高并发发生的点
    在这里插入图片描述

  2. 为什么要单独获取系统时间
    因为detail页和他的静态化资源都放在CDN上,所以访问的时候就不再请求我们的服务器,也就不能拿到系统时间,所以要有一个接口来返回系统时间
    在这里插入图片描述
    什么是CDN
    在这里插入图片描述

  3. 获取秒杀地址接口分析
    这个没法使用CDN,因为CDN对应的资源是不宜变化的,而获取秒杀接口的返回里面的数据(开始时间,结束时间)在变化

他适合用redis等服务的缓存
在这里插入图片描述

  1. 秒杀操作优化分析
    ① 无法使用CDN
    ② 后端缓存困难:库存问题,要使用Mysql保证事务
    ③ 一行数据竞争:热点商品操作

为什么不用Mysql处理
因为Mysql低效,这里的低效的原因是java客户端执行这些操作时候和数据库的通信有网络延迟和GC,而update这些操作不同用户线程又是串行执行的,会等待

在这里插入图片描述
在这里插入图片描述

行级锁是在commit或rollback才释放,我们优化就是要减少行级锁持有时间,也就是说可以把客户端逻辑放在MySql服务器端,避免网络延迟和GC影响

在这里插入图片描述

并发优化

在这里插入图片描述
这里可以改变insert和update的顺序,达到行级锁持有时间变短的目的(insert 和 update两个依旧组成事务,如果update不成功会回滚)

 @Override
    @Transactional
    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);
            //userPhone和seckillId作为联合主键避免重复秒杀
            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, SeckillStaEnum.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());
        }
    }

redis后端缓存优化

因为要频繁的访问后端获取秒杀暴露接口,那么可以把他放到redis缓存起来

  1. 引入java访问redis的客户端
<!--    redis客户端:jedis-->
      <dependency>
          <groupId>redis.clients</groupId>
          <artifactId>jedis</artifactId>
          <version>2.7.3</version>
      </dependency>
  1. 缓存控制不应该放在service层,而应该放在dao层,dao数据访问对象就是用来放数据库存储和其他数据访问的包

  2. 我们想① 通过redis访问一个缓存的对象,② 缓存里没有的时候往里放一个

  3. 我们要实现序列化,因为redis并没有实现内部序列化操作,从redis里面读出来的二进制数组,我们要通过反序列化来转化为对象
    对于序列化来说,直接implements Serializable,但是这是高并发优化,使用protostuff更高效,使用自定义序列化

  4. 引入对应依赖

<!--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>
  1. 取是从redis中取出字节数组,反序列化为java对象

  2. 放是把java对象序列化为byte[]数组,放入

package org.seckill.dao.cache;

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

import java.util.concurrent.ExecutionException;

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){
        try{
            Jedis jedis = jedisPool.getResource();
            try{
                String key = "sckill:"+seckillId;
                //并没有实现哪部序列化操作
                //采用自定义序列化
                //protostuff: pojo.
                byte[] bytes =jedis.get(key.getBytes());
                //获取到
                if(bytes!=null){
                    //反序列化
                    Seckill seckill = schema.newMessage();
                    ProtostuffIOUtil.mergeFrom(bytes,seckill,schema);
                    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;
    }

}

  1. 注入
<!--redisDao-->
    <bean id="redisDao" class="org.seckill.dao.cache.RedisDao">
        <constructor-arg index="0" value="localhost"/>
        <constructor-arg index="1" value="6379"/>
    </bean>
  1. service中使用
@Override
    public Exposer exportSeckillUrl(long seckillId) {
        Seckill seckill = redisDao.getSeckill(seckillId);
        if(seckill == null){
            seckill = seckillDao.queryById(seckillId);
            if(seckill == null){
                return new Exposer(false,seckillId);
            }else {
                redisDao.putSeckill(seckill);
            }
        }
        if(seckill == null){//差不到秒杀记录
            return new Exposer(false,seckillId);
        }
        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);
    }

事务SQL在MySql端执行

存储过程优化事务行级锁的持有时间

发布了62 篇原创文章 · 获赞 0 · 访问量 1162

猜你喜欢

转载自blog.csdn.net/weixin_43907800/article/details/104011816