1. 程式人生 > >Java高併發秒殺API之高併發優化(四)

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事務,保證資料的一致性和完整性