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

四 高并发优化

1.分析

这里写图片描述
1.详情页 部署到cdn上,这样用户访问的是cdn不是服务器了。
用户在上网时通过运营商访问最近的城域网,城域网访问主干网。
2.获取系统时间 不用优化
访问一次内存大概 10ns
无法使用cdn,适合服务器端缓存redis等(单台一秒10万qps,还可以做集群)
一致性维护非常低
3.秒杀地址优化
请求地址->访问redis–(超时穿透/主动刷新)->访问mysql
4.秒杀操作的优化分析
无法使用cdn
后端缓存困难,库存问题
一行数据竞争,热点商品
其他方案分析
这里写图片描述
运维成本高(nosql不稳定等),开发成本高(开发需要知道事务回滚等)
幂等性难保证,重复秒杀

mysql update同一条数据 ,4万qps

A :先update 后insert
B 先update 等待事务 ,A释放锁后update,insert

gc(新生代gc(暂停所有java代码,几十毫秒 )和老一代gc)
执行update –网络延迟/gc –>insert–网络延迟/gc –>commit/rollback
优化方向减少锁持有时间
同城机房(0.5~2ms)max(1000qps)
异地机房更长

如何判断update成功
1.自身没有报错2.客户端确认更新成功
优化思路
把客户端的业务逻辑放到mysql服务端

两种解决方案
1.定制sql方案update /*+[auto_commit]*/需要修改mysql源码
自动进行update为1 -> commit ,为0 -> rollback
2.存储过程

2.redis 后端优化

官网下载redis

https://redis.io/download 

安装完成后
redis-server服务器启动
redis-cli 客户端启动
引入jedis依赖

   <dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>2.7.3</version>
    </dependency>

redis指令 get key ,set key value

redisDao

package org.seckill.dao.cache;

import org.seckill.entity.Seckill;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtostuffIOUtil;
import com.dyuproject.protostuff.runtime.RuntimeSchema;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public class RedisDao {
    private final JedisPool jedisPool;
    private  final Logger logger = LoggerFactory.getLogger(RedisDao.class);
    private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class);

    public RedisDao(String ip,int port){
        jedisPool = new JedisPool(ip,port);
    }

    public Seckill getSeckill(long seckillId){
        try {
            Jedis jedis = jedisPool.getResource();
            try {
                String key = "seckill:"+seckillId;
                //没有实现 内部序列化
                //get -> byte[] -> 反序列化 ->Object(seckill)
                //采用自定义序列化
                // protostuff:pojo(有get,set方法)
                byte [] bytes = jedis.get(key.getBytes());
                if(bytes != null){
                    //创建一个空对象
                    Seckill seckill = schema.newMessage();
                    ProtostuffIOUtil.mergeFrom(bytes, seckill, schema);
                    //被反序列
                    return seckill;//比java 原生的压缩了1/10~1/5  压缩速度 差2个数量级
                }
            }finally{
                jedis.close();
            }
        } catch (Exception e) {
            logger.error(e.getMessage());
        }
        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;//小时
                String result = jedis.setex(key.getBytes(), timeout, bytes);
                return result;
            } finally{
                jedis.close();
            }
        } catch (Exception e) {
            logger.error(e.getMessage());
        }
        return null;
    }

}

key存放seckill:id value存放序列化对象
所以需要protostuff(性能更好)serializable(性能较低)
添加依赖

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

单元测试
RedisDaoTest

package org.seckill.dao;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.seckill.dao.cache.RedisDao;
import org.seckill.entity.Seckill;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:spring/spring-dao.xml"})
public class RedisDaoTest {

    @Autowired
    private RedisDao redisDao;

    private long id = 1002;
    @Autowired
    private SeckillDao seckillDao;

    @Test
    public void testSeckill() throws Exception{
        //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);
            }
        }
    }


    @Test
    public void testGetSeckill() throws Exception{
        Seckill seckill  = redisDao.getSeckill(id);
        System.out.println(seckill);
    }
    @Test
    public void testputSeckill() throws Exception{

    }

}
3.并发优化

原来的逻辑 update->insert->commit/rollback
修改为insert->update->commit/rollback
减少锁持有的时间
存储过程

--- 秒杀执行的存储过程
--;
DELIMITER $$  --console ; 转换为 $$
CREATE PROCEDURE `SECKILL`.`execute_seckill`
--参数 in 输入参数 out 输出参数
-- row_count():返回上一条修改类型sql(d,i,u)的影响行数
--row-count() = 0 未修改数据  >0 表示修改的行数 <0 sql错误/未执行
(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,create_time)
    values(v_seckill_id,v_phone,0,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(1001,13934131331,now(),@r_result)

select @r_result

SeckillDao添加方法

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

SeckillDao.xml添加

    <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>

service添加


    //执行秒杀操作by存储过程
        SeckillExecution excuteSeckillProcedure(long seckillId,long userPhone,String md5) throws RepeatKillException,seckillCloseException,SeckillException; 

实现类

public SeckillExecution excuteSeckillProcedure(long seckillId, long userPhone, String md5)
            throws RepeatKillException, seckillCloseException, SeckillException {
        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);
    try {
        seckilldao.killByProcedure(map);
        int result = MapUtils.getInteger(map, "result",-2);
        if(result==1){
            SuccessKilled 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());
        return new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
    }
    }

系统用到哪些服务
cdn
webserver : nginx(集群化,http服务器,给后端服务器做反向代理)+jetty
redis服务器端缓存
mysql mysql事务,保证数据的一致性和完整性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值