1. 程式人生 > >基於本地redis、protostuff序列化對於資料層的優化及java中對於泛型的使用

基於本地redis、protostuff序列化對於資料層的優化及java中對於泛型的使用

此次對於redis、protostuff的應用是在一個高併發的秒殺系統中實現的。

在高併發的秒殺系統的優化中主要有以下幾個方面:

1.對於獲取秒殺地址的介面的優化
每次獲取秒殺介面我們都要訪問資料庫,在高併發的系統中我們可以使用redis快取進行優化,不需要每次都訪問資料庫,從而減小資料庫的壓力。而同時使用Protostuff對於需要傳遞的資料進行序列化,這樣傳遞的資料量就會大大減小。從而減少併發時間。

2.對於秒殺操作的優化
秒殺操作是和資料庫事務有關的,在秒殺操作中我們主要執行insert和update兩條語句。
而對同一條資料進行update操作的時候,mysql存在行級鎖等待的情況,一條update語句必須等待前一天update執行完之後才可以拿到lock鎖。
所以這裡優化主要針對的是行級鎖事務的等待,網路的延遲和GC回收!

這次我們主要講的是對於秒殺地址的優化。

實體類Seckill,記錄了秒殺商品id,商品name,商品庫存,開始時間結束時間,建立時間。

public class Seckill
{
    private long seckillId;
    private String name;
    private int number;
    private Date startTime;
    private Date endTime;
    private Date createTime;

    public long getSeckillId() {
        return
seckillId; } public void setSeckillId(long seckillId) { this.seckillId = seckillId; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getNumber() { return number; } public
void setNumber(int number) { this.number = number; } public Date getStartTime() { return startTime; } public void setStartTime(Date startTime) { this.startTime = startTime; } public Date getEndTime() { return endTime; } public void setEndTime(Date endTime) { this.endTime = endTime; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } @Override public String toString() { return "Seckill{" + "seckillId=" + seckillId + ", name='" + name + '\'' + ", number=" + number + ", startTime=" + startTime + ", endTime=" + endTime + ", createTime=" + createTime + '}'; } }

實體類Exposer,存放了是否秒殺,MD5加密,當前系統時間,秒殺開始時間、秒殺結束時間等資訊

/**
 * Created by yliu on 18/03/27.
 * 暴露秒殺地址(介面)DTO
 */
public class Exposer {

    //是否開啟秒殺
    private boolean exposed;

    //加密措施
    private String md5;

    private long seckillId;

    //系統當前時間(毫秒)
    private long now;

    //秒殺的開啟時間
    private long start;

    //秒殺的結束時間
    private long end;

//建構函式。秒殺成功則返回true,已經經過md5加密後的地址,以及商品id,表示該商品已經開啟秒殺
    public Exposer(boolean exposed, String md5, long seckillId) {
        this.exposed = exposed;
        this.md5 = md5;
        this.seckillId = seckillId;
    }

//建構函式。若失敗則返回false,可以返回商品id和系統時間、開始時間、結束時間、判斷秒殺何時開啟
    public Exposer(boolean exposed, long seckillId,long now, long start, long end) {
        this.exposed = exposed;
        this.seckillId=seckillId;
        this.now = now;
        this.start = start;
        this.end = end;
    }
//建構函式 
    public Exposer(boolean exposed, long seckillId) {
        this.exposed = exposed;
        this.seckillId = seckillId;
    }

    public boolean isExposed() {
        return exposed;
    }

    public void setExposed(boolean exposed) {
        this.exposed = exposed;
    }

    public String getMd5() {
        return md5;
    }

    public void setMd5(String md5) {
        this.md5 = md5;
    }

    public long getSeckillId() {
        return seckillId;
    }

    public void setSeckillId(long seckillId) {
        this.seckillId = seckillId;
    }

    public long getNow() {
        return now;
    }

    public void setNow(long now) {
        this.now = now;
    }

    public long getStart() {
        return start;
    }

    public void setStart(long start) {
        this.start = start;
    }

    public long getEnd() {
        return end;
    }

    public void setEnd(long end) {
        this.end = end;
    }

    @Override
    public String toString() {
        return "Exposer{" +
                "exposed=" + exposed +
                ", md5='" + md5 + '\'' +
                ", seckillId=" + seckillId +
                ", now=" + now +
                ", start=" + start +
                ", end=" + end +
                '}';
    }
}

實體類:秒殺結果類,裡面存放了請求是否成功,成功則返回一個泛型T的資料,失敗則返回錯誤資訊:

/**
 * Created by yliu on 18/03/28.
 */
//將所有的ajax請求返回型別,全部封裝成json資料
public class SeckillResult<T> {

    //請求是否成功
    private boolean success;
    private T data;
    private String error;

    public SeckillResult(boolean success, T data) {
        this.success = success;
        this.data = data;
    }

    public SeckillResult(boolean success, String error) {
        this.success = success;
        this.error = error;
    }

    public boolean isSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public String getError() {
        return error;
    }

    public void setError(String error) {
        this.error = error;
    }
}

RedisDao中定義了put、get redis中資料的方法:


/**
 * Created by yliu on 18/3/27.
 */
public class RedisDao {
    private final JedisPool jedisPool;//redis連線池

    //redisDao的構造器,傳入redis的地址和埠,實現連線池的初始化
    public RedisDao(String ip, int port) {
        jedisPool = new JedisPool(ip, port);
    }

    //Protostuff序列化所要傳入的引數
    private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class);


    public Seckill getSeckill(long seckillId) {
        //redis操作邏輯
        try {
            Jedis jedis = jedisPool.getResource();
            try {
                String key = "seckill:" + seckillId;
                //並沒有實現哪部序列化操作
                //採用自定義序列化 protostuff: pojo.
                //通過key得到存放在redis中經過序列化後的Seckill類,經過序列化後存放的是位元組型別的資料
                byte[] bytes = jedis.get(key.getBytes());
                //快取重獲取到
                if (bytes != null) {
                    Seckill seckill=schema.newMessage();
                    ProtostuffIOUtil.mergeFrom(bytes,seckill,schema);
                    //反序列化得到Seckill 並返回

                    return seckill;
                }
            }finally {
            //最後不要忘記關閉redis連線
                jedis.close();
            }
        }catch (Exception e) {

        }
        return null;
    }

    public String putSeckill(Seckill seckill) {
        try {
            Jedis jedis = jedisPool.getResource();
            try {
                String key = "seckill:" + seckill.getSeckillId();
                //將seckill類序列化成位元組,大小為預設的大小
                byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema,
                        LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
                //超時快取
                int timeout = 60 * 60;//1小時
//將序列化後的位元組存放在redis中,存放時間為1小時,返回string型別的result,根據返回的結果可判斷存放是否成功
                String result = jedis.setex(key.getBytes(),timeout,bytes);

                return result;
            }finally {
                jedis.close();
            }
        }catch (Exception e) {

        }

        return null;
    }
}

暴露秒殺介面地址的方法,會返回一個Exposer的型別:

//加入一個混淆字串(秒殺介面)的salt,為了我避免使用者猜出我們的md5值,值任意給,越複雜越好
private final String salt="shsdssljdd'l.";
public Exposer exportSeckillUrl(long seckillId) {
        //優化點:快取優化:超時的基礎上維護一致性
        //1。首先訪問redis,如果redis中存放了秒殺介面的資料,則直接從redis中取出,如果沒有存放,則從資料庫中取出資料,並存放到redis中,方便下次獲取秒殺介面。

        Seckill seckill = redisDao.getSeckill(seckillId);
        if (seckill == null) {
            //2.訪問資料庫
            seckill = seckillDao.queryById(seckillId);
            if (seckill == null) {//說明查不到這個秒殺產品的記錄
            //返回商品id 和false表示該商品未開啟秒殺
                return new Exposer(false, seckillId);
            }else {
                //3,放入redis
                redisDao.putSeckill(seckill);
            }

        }


        //根據返回的seckill 類中存放的秒殺開啟時間判斷是否開啟秒殺
        Date startTime=seckill.getStartTime();
        Date endTime=seckill.getEndTime();
        //系統當前時間
        Date nowTime=new Date();
        if (startTime.getTime()>nowTime.getTime() || endTime.getTime()<nowTime.getTime())
        {
        //系統當前時間比開始時間小,或者超過結束時間,說明沒有開啟秒殺,返回false及其他引數
            return new Exposer(false,seckillId,nowTime.getTime(),startTime.getTime(),endTime.getTime());
        }

        //秒殺開啟,返回秒殺商品的id、用給介面加密的md5
        String md5=getMD5(seckillId);
        return new Exposer(true,md5,seckillId);
    }

//對秒殺商品的id進行加密,通過與salt鹽值的拼接,返回複雜的md5
    private String getMD5(long seckillId)
    {
        String base=seckillId+"/"+salt;
        //呼叫spring中自帶的md5工具
        String md5= DigestUtils.md5DigestAsHex(base.getBytes());
        return md5;
    }

控制器controller中

@RequestMapping(value = "/{seckillId}/exposer",
                    method = RequestMethod.GET,
                    produces = {"application/json;charset=UTF-8"})
    @ResponseBody
    public SeckillResult<Exposer> exposer(@PathVariable("seckillId") Long seckillId)
    {
    //返回型別是seckillResult並設定了泛型Exposer,則在實體類中seckillResult所定義的泛型變數data是Exposer型別的資料
        SeckillResult<Exposer> result;
        try{
       //根據秒殺商品id返回商品exposer類變數,中存放了是否開啟秒殺,秒殺開啟時間,md5等資料
            Exposer exposer=seckillService.exportSeckillUrl(seckillId);
     //在seckillResult定義了建構函式
     /* public SeckillResult(boolean success, T data) {
        this.success = success;
        this.data = data;
    } */
            result=new SeckillResult<Exposer>(true,exposer);//請求成功 返回秒殺商品的資訊,即是否開啟秒殺等資訊
        }catch (Exception e)
        {
        //請求錯誤,丟擲異常,並返回錯誤資訊
            e.printStackTrace();
            result=new SeckillResult<Exposer>(false,e.getMessage());
        }

        return result;
    }

在不確定返回引數型別的時候,可以使用泛型,等返回引數確定之後,再給其繫結型別。