Java高併發秒殺系統【觀後總結】

来源:https://www.cnblogs.com/Java3y/archive/2018/03/16/8586090.html
-Advertisement-
Play Games

項目簡介 在慕課網上發現了一個JavaWeb項目,內容講的是高併發秒殺,覺得挺有意思的,就進去學習了一番。 記錄在該項目中學到了什麼玩意.. 該項目源碼對應的gitHub地址(由觀看其視頻的人編寫,並非視頻源代碼): "https://github.com/codingXiaxw/seckill" ...


項目簡介

在慕課網上發現了一個JavaWeb項目,內容講的是高併發秒殺,覺得挺有意思的,就進去學習了一番。

記錄在該項目中學到了什麼玩意..

該項目源碼對應的gitHub地址(由觀看其視頻的人編寫,並非視頻源代碼):https://github.com/codingXiaxw/seckill

我結合其資料和觀看視頻的時候整理出從該項目學到了什麼...

項目Dao層

日誌記錄工具:



    <!--1.日誌 java日誌有:slf4j,log4j,logback,common-logging
        slf4j:是規範/介面
        日誌實現:log4j,logback,common-logging
        使用:slf4j+logback
    -->

Mybatis之前沒註意到的配置屬性:

使用jdbc的getGeneratekeys獲取自增主鍵值,這個屬性還是挺有用的。


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!--配置全局屬性-->
    <settings>
        <!--使用jdbc的getGeneratekeys獲取自增主鍵值-->
        <setting name="useGeneratedKeys" value="true"/>
        <!--使用列別名替換列名  預設值為true
        select name as title(實體中的屬性名是title) form table;
        開啟後mybatis會自動幫我們把表中name的值賦到對應實體的title屬性中
        -->
        <setting name="useColumnLabel" value="true"/>

        <!--開啟駝峰命名轉換Table:create_time到 Entity(createTime)-->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>

</configuration>

Mybatis返回的對象如果有關聯欄位,除了使用resultMap還有下麵這種方式(雖然我還是覺得resultMap會方便一點)


    <select id="queryByIdWithSeckill" resultType="SuccessKilled">

        <!--根據seckillId查詢SuccessKilled對象,並攜帶Seckill對象-->
        <!--如何告訴mybatis把結果映射到SuccessKill屬性同時映射到Seckill屬性-->
        <!--可以自由控制SQL語句-->
        SELECT
            sk.seckill_id,
            sk.user_phone,
            sk.create_time,
            sk.state,
            s.seckill_id "seckill.seckill_id",
            s.name "seckill.name",
            s.number "seckill",
            s.start_time "seckill.start_time",
            s.end_time "seckill.end_time",
            s.create_time "seckill.create_time"
        FROM success_killed sk
        INNER JOIN seckill s ON sk.seckill_id=s.seckill_id
        WHERE sk.seckill_id=#{seckillId}
        AND sk.user_phone=#{userPhone}
    </select>

資料庫連接池可能用到的屬性:


        <!--c3p0私有屬性-->
        <property name="maxPoolSize" value="30"/>
        <property name="minPoolSize" value="10"/>
        <!--關閉連接後不自動commit-->
        <property name="autoCommitOnClose" value="false"/>

        <!--獲取連接超時時間-->
        <property name="checkoutTimeout" value="1000"/>
        <!--當獲取連接失敗重試次數-->
        <property name="acquireRetryAttempts" value="2"/>

spring與Junit整合:


/**
 * Created by codingBoy on 16/11/27.
 * 配置spring和junit整合,這樣junit在啟動時就會載入spring容器
 */
@RunWith(SpringJUnit4ClassRunner.class)
//告訴junit spring的配置文件
@ContextConfiguration({"classpath:spring/spring-dao.xml"})
public class SeckillDaoTest {

    //註入Dao實現類依賴
    @Resource
    private SeckillDao seckillDao;


    @Test
    public void queryById() throws Exception {
        long seckillId=1000;
        Seckill seckill=seckillDao.queryById(seckillId);
        System.out.println(seckill.getName());
        System.out.println(seckill);
    }
}

Mybatis參數為一個以上時

之前在學習MyBatis的時候,如果參數超過了一個,那麼是使用Map集合來進行裝載的!

在這次教程中發現,可以不用Map集合(如果都是基本數據類型)!

例子:**使用@Param就行了!**


int reduceNumber(@Param("seckillId") long seckillId, @Param("killTime") Date killTime);

在XML文件中可以直接忽略parameterType了!

避免重覆插入數據時拋出異常

如果主鍵重覆插入數據的時候,Mybatis正常是會拋出異常的,我們又不希望它拋出異常,那麼我們可以這樣做:

寫ignore..

Service層

tdo

一個dto包作為傳輸層,dto和entity的區別在於:entity用於業務數據的封裝,而dto用於完成web和service層的數據傳遞。

對於dto這個概念,在之前我是接觸過一次的,但是沒有好好地實踐起來。這次看到了它的用法了。

我的理解是:Service與Web層數據傳遞數據的再包裝了一個對象而已。因為很多時候Service層返回的數據如果使用的是POJO,POJO很多的屬性是多餘的,還有一些想要的數據又包含不了。此時,dto又可以再一次對要傳輸的數據進行抽象,封裝想獲取的數據

定義多個異常對象

之前的異常對象都是針對整個業務的,其實還是可以細分多個異常類的出來的。比如“重覆秒殺”,”秒殺關閉“這些都是屬於秒殺的業務。

這樣做的好處就是看到拋出的異常就能夠知道是具體哪部分錯了。

對於視頻中在Service層就catch住了很多異常,我覺得可以在Service層直接拋出,在Controller也能拋出,直接使用統一異常處理器類來管理會更加方便!

提倡使用註解方式使用事務


我覺得就是代碼更加清晰吧,使用註解的話。

在視頻下麵還有同學說如果在Service中調用事務方法會有些坑,我暫時還沒遇到過。先存起來吧:

併發性上不去是因為當多個線程同時訪問一行數據時,產生了事務,因此產生寫鎖,每當一個獲取了事務的線程把鎖釋放,另一個排隊線程才能拿到寫鎖,QPS和事務執行的時間有密切關係,事務執行時間越短,併發性越高,這也是要將費時的I/O操作移出事務的原因。

關於同類中調用事務方法的時候有個坑,同學們需要註意下AOP切不到調用事務方法。事務不會生效,解決辦法有幾種,可以搜一下,找一下適合自己的方案。本質問題時類內部調用時AOP不會用代理調用內部方法。

“關於同類中調用事務方法的時候有個坑” 解決方案
1、如果是基於介面動態代理 是沒有問題的,直接使用介面調用
2、如果是基於class的動態代理 可以用 AopContext.currentProxy() 解決,註意剝離方法一定是public 修飾 !!

MD5暴露介面

其實我也在想MD5暴露出去的url是不是真的有用,也見到有人提問了。

https://www.imooc.com/qadetail/164058

回答者:

不能說沒作用,如果不加密,用戶截取了你的訪問地址,他看到了當前秒殺ID為1000,他完全可以推測出其他的秒殺地址,或者說他可以造出一批地址;視頻中秒殺在資料庫中判斷了秒殺時間,其他時間他自然是秒殺不到,但是對資料庫也有一定的衝擊,如果他用定時器或者迴圈秒殺軟體,你的系統承受力是個問題;另一方面對於一些還沒開始的秒殺,他模擬地址以後,完全可以用定時器一直訪問。加密以後由於他拿不到混淆碼,就只能通過點擊鏈接進行秒殺……

簡單理解:通過MD5加密以後,用戶在秒殺之前模擬不出真實的地址,還是有一定作用的。

枚舉類

在return new SeckillExecution(seckillId,1,"秒殺成功",successKilled);代碼中,我們返回的state和stateInfo參數信息應該是輸出給前端的,但是我們不想在我們的return代碼中硬編碼這兩個參數,所以我們應該考慮用枚舉的方式將這些常量封裝起來



public enum SeckillStatEnum {

    SUCCESS(1,"秒殺成功"),
    END(0,"秒殺結束"),
    REPEAT_KILL(-1,"重覆秒殺"),
    INNER_ERROR(-2,"系統異常"),
    DATE_REWRITE(-3,"數據篡改");

    private int state;
    private String info;

    SeckillStatEnum(int state, String info) {
        this.state = state;
        this.info = info;
    }

    public int getState() {
        return state;
    }


    public String getInfo() {
        return info;
    }


    public static SeckillStatEnum stateOf(int index)
    {
        for (SeckillStatEnum state : values())
        {
            if (state.getState()==index)
            {
                return state;
            }
        }
        return null;
    }
}

Web層開發技巧

Restful介面設計學習

之前就已經接觸過RESTful這樣的思想理念的,可是在第一個項目中是沒有用起來的。因為還是不大習慣,怕寫成不倫不類的RESTful介面,打算在第二個項目中將RESTful全部應用起來。




參考博文:http://kb.cnblogs.com/page/512047/

SpringMVC之前不知道的細節

**@DateTimeFormat註解對時間進行格式化!(這個我暫時沒有試驗)**


    <!--配置spring mvc-->
    <!--1,開啟springmvc註解模式
    a.自動註冊DefaultAnnotationHandlerMapping,AnnotationMethodHandlerAdapter
    b.預設提供一系列的功能:數據綁定,數字和日期的format@NumberFormat,@DateTimeFormat
    c:xml,json的預設讀寫支持-->
    <mvc:annotation-driven/>

    <!--2.靜態資源預設servlet配置-->
    <!--
        1).加入對靜態資源處理:js,gif,png
        2).允許使用 "/" 做整體映射
    -->
    <mvc:default-servlet-handler/>

返回統一格式的JSON

之前在Web層與Service中封裝了dto來進行這兩層的數據進行傳輸,而我們一般都是在Controller返回JSON給前端進行解析。

最好的做法就是將JSON的格式也統一化。這樣做就能夠很好地形成規範了!


//將所有的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;
    }
}

獲取JSON數據方式

之前獲取JSON都是使用object.properties的方式來獲取的,這次還看到了另一種方式:

JavaScript模塊化

之前在項目中寫JS代碼都是要什麼功能,寫到哪裡的。看了這次視頻,發現JS都可以模塊化!!!

JS模塊化起來可讀性還是比之前要好的,這是我之前沒有接觸過的,以後寫JS代碼就要註意了!

下麵貼上一段代碼來感受一下:


/**
 *  模塊化javaScript
 * Created by jianrongsun on 17-5-25.
 */
var seckill = {
    // 封裝秒殺相關的ajax的url
    URL: {
        now: function () {
            return "/seckill/time/now";
        },
        exposer: function (seckillId) {
            return "/seckill/" + seckillId + "/exposer";
        },
        execution: function (seckillId, md5) {
            return "/seckill/" + seckillId + "/" + md5 + "/execution";
        }
    },
    // 驗證手機號碼
    validatePhone: function (phone) {
        return !!(phone && phone.length === 11 && !isNaN(phone));
    },
    // 詳情頁秒殺業務邏輯
    detail: {
        // 詳情頁開始初始化
        init: function (params) {
            console.log("獲取手機號碼");
            // 手機號驗證登錄,計時交互
            var userPhone = $.cookie('userPhone');
            // 驗證手機號
            if (!seckill.validatePhone(userPhone)) {
                console.log("未填寫手機號碼");
                // 驗證手機控制輸出
                var killPhoneModal = $("#killPhoneModal");
                killPhoneModal.modal({
                    show: true,  // 顯示彈出層
                    backdrop: 'static',  // 靜止位置關閉
                    keyboard: false    // 關閉鍵盤事件
                });

                $("#killPhoneBtn").click(function () {
                    console.log("提交手機號碼按鈕被點擊");
                    var inputPhone = $("#killPhoneKey").val();
                    console.log("inputPhone" + inputPhone);
                    if (seckill.validatePhone(inputPhone)) {
                        // 把電話寫入cookie
                        $.cookie('userPhone', inputPhone, {expires: 7, path: '/seckill'});
                        // 驗證通過 刷新頁面
                        window.location.reload();
                    } else {
                        // todo 錯誤文案信息寫到前端
                        $("#killPhoneMessage").hide().html("<label class='label label-danger'>手機號碼錯誤</label>").show(300);
                    }
                });
            } else {
                console.log("在cookie中找到了電話號碼,開啟計時");
                // 已經登錄了就開始計時交互
                var startTime = params['startTime'];
                var endTime = params['endTime'];
                var seckillId = params['seckillId'];
                console.log("開始秒殺時間=======" + startTime);
                console.log("結束秒殺時間========" + endTime);
                $.get(seckill.URL.now(), {}, function (result) {
                    if (result && result['success']) {
                        var nowTime = seckill.convertTime(result['data']);
                        console.log("伺服器當前的時間==========" + nowTime);
                        // 進行秒殺商品的時間判斷,然後計時交互
                        seckill.countDown(seckillId, nowTime, startTime, endTime);
                    } else {
                        console.log('結果:' + result);
                        console.log('result' + result);
                    }
                });
            }

        }
    },
    handlerSeckill: function (seckillId, mode) {
        // 獲取秒殺地址
        mode.hide().html('<button class="btn btn-primary btn-lg" id="killBtn">開始秒殺</button>');
        console.debug("開始進行秒殺地址獲取");
        $.get(seckill.URL.exposer(seckillId), {}, function (result) {
            if (result && result['success']) {
                var exposer = result['data'];
                if (exposer['exposed']) {
                    console.log("有秒殺地址介面");
                    // 開啟秒殺,獲取秒殺地址
                    var md5 = exposer['md5'];
                    var killUrl = seckill.URL.execution(seckillId, md5);
                    console.log("秒殺的地址為:" + killUrl);
                    // 綁定一次點擊事件
                    $("#killBtn").one('click', function () {
                        console.log("開始進行秒殺,按鈕被禁用");
                        // 執行秒殺請求,先禁用按鈕
                        $(this).addClass("disabled");
                        // 發送秒殺請求
                        $.post(killUrl, {}, function (result) {
                            var killResult = result['data'];
                            var state = killResult['state'];
                            var stateInfo = killResult['stateInfo'];
                            console.log("秒殺狀態" + stateInfo);
                            // 顯示秒殺結果
                            mode.html('<span class="label label-success">' + stateInfo + '</span>');

                        });

                    });
                    mode.show();
                } else {
                    console.warn("還沒有暴露秒殺地址介面,無法進行秒殺");
                    // 未開啟秒殺
                    var now = seckill.convertTime(exposer['now']);
                    var start = seckill.convertTime(exposer['start']);
                    var end = seckill.convertTime(exposer['end']);
                    console.log("當前時間" + now);
                    console.log("開始時間" + start);
                    console.log("結束時間" + end);
                    console.log("開始倒計時");
                    console.debug("開始進行倒計時");
                    seckill.countDown(seckillId, now, start, end);
                }
            } else {
                console.error("伺服器端查詢秒殺商品詳情失敗");
                console.log('result' + result.valueOf());
            }
        });
    },
    countDown: function (seckillId, nowTime, startTime, endTime) {
        console.log("秒殺的商品ID:" + seckillId + ",伺服器當前時間:" + nowTime + ",開始秒殺的時間:" + startTime + ",結束秒殺的時間" + endTime);
        //  獲取顯示倒計時的文本域
        var seckillBox = $("#seckill-box");
        //  獲取時間戳進行時間的比較
        nowTime = new Date(nowTime).valueOf();
        startTime = new Date(startTime).valueOf();
        endTime = new Date(endTime).valueOf();
        console.log("轉換後的Date類型當前時間戳" + nowTime);
        console.log("轉換後的Date類型開始時間戳" + startTime);
        console.log("轉換後的Date類型結束時間戳" + endTime);
        if (nowTime < endTime && nowTime > startTime) {
            // 秒殺開始
            console.log("秒殺可以開始,兩個條件符合");
            seckill.handlerSeckill(seckillId, seckillBox);
        }
        else if (nowTime > endTime) {
            alert(nowTime > endTime);
            // console.log(nowTime + ">" + startTime);
            console.log(nowTime + ">" + endTime);

            // 秒殺結束
            console.warn("秒殺已經結束了,當前時間為:" + nowTime + ",秒殺結束時間為" + endTime);
            seckillBox.html("秒殺結束");
        } else {
            console.log("秒殺還沒開始");
            alert(nowTime < startTime);
            // 秒殺未開啟
            var killTime = new Date(startTime + 1000);
            console.log(killTime);
            console.log("開始計時效果");
            seckillBox.countdown(killTime, function (event) {
                // 事件格式
                var format = event.strftime("秒殺倒計時: %D天 %H時 %M分 %S秒");
                console.log(format);
                seckillBox.html(format);
            }).on('finish.countdown', function () {
                // 事件完成後回調事件,獲取秒殺地址,控制業務邏輯
                console.log("準備執行回調,獲取秒殺地址,執行秒殺");
                console.log("倒計時結束");
                seckill.handlerSeckill(seckillId, seckillBox);
            });
        }
    },
    cloneZero: function (time) {
        var cloneZero = ":00";
        if (time.length < 6) {
            console.warn("需要拼接時間");
            time = time + cloneZero;
            return time;
        } else {
            console.log("時間是完整的");
            return time;
        }
    },
    convertTime: function (localDateTime) {
        var year = localDateTime.year;
        var monthValue = localDateTime.monthValue;
        var dayOfMonth = localDateTime.dayOfMonth;
        var hour = localDateTime.hour;
        var minute = localDateTime.minute;
        var second = localDateTime.second;
        return year + "-" + monthValue + "-" + dayOfMonth + " " + hour + ":" + minute + ":" + second;
    }
};

高併發性能優化

前三篇已經做好了這個系統了,但是作為一個秒殺系統而言,它能支持的併發量是很低的。那我們現在要考慮怎麼調優。

分析

秒殺的地址介面可以藉助redis來進行優化,不用多次訪問資料庫。

秒殺操作是與資料庫的事務相關的,不能使用緩存來替代了。下麵給出的方案是需要修改源碼的,難度是比較難的。





下麵分析瓶頸究竟在哪:

  • Mysql執行單條的SQL語句其實是非常快的。
  • 主要是行級鎖事務的等待,網路的延遲和GC回收!






解決思路:

解決秒殺介面

對於秒殺介面而言,需要使用到Redis將數據進行緩存起來。那麼用戶就訪問就不用去訪問資料庫了,我們給Redis緩存的數據就好了。

這次使用Jedis來操作Redis.

還有值得 註意的地方:我們可以使用ProtostuffIOUtil來代替JDK的序列化,因為這個的序列化功能比JDK的要做得好很多!


package com.suny.dao.cache;

import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtostuffIOUtil;
import com.dyuproject.protostuff.runtime.RuntimeSchema;
import com.suny.entity.Seckill;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

/**
 * 操作Redis的dao類
 * Created by 孫建榮 on 17-5-27.下午4:44
 */
public class RedisDao {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private final JedisPool jedisPool;

    private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class);

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

    public Seckill getSeckill(long seckillId) {
        // redis操作業務邏輯
        try (Jedis jedis = jedisPool.getResource()) {
            String key = "seckill:" + seckillId;
            // 並沒有實現內部序列化操作
            //get->byte[]位元組數組->反序列化>Object(Seckill)
            // 採用自定義的方式序列化
            // 緩存獲取到
            byte[] bytes = jedis.get(key.getBytes());
            if (bytes != null) {
                // 空對象
                Seckill seckill = schema.newMessage();
                ProtostuffIOUtil.mergeFrom(bytes, seckill, schema);
                // seckill被反序列化
                return seckill;
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return null;
    }

    public String putSeckill(Seckill seckill) {
        //  set Object(Seckill) -> 序列化 -> byte[]
        try (Jedis jedis = jedisPool.getResource()) {
            String key = "seckill:" + seckill.getSeckillId();
            byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema,
                    LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
            // 超時緩存
            int timeout=60*60;
            return jedis.setex(key.getBytes(), timeout, bytes);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return null;
    }


}

        <!--導入連接redis的JAR包-->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>

        <!--添加序列化依賴-->
        <dependency>
            <groupId>com.dyuproject.protostuff</groupId>
            <artifactId>protostuff-core</artifactId>
            <version>1.1.1</version>
        </dependency>
        <dependency>
            <groupId>com.dyuproject.protostuff</groupId>
            <artifactId>protostuff-runtime</artifactId>
            <version>1.1.1</version>
        </dependency>

RedisDao並不受Mybatis的代理影響,於是是需要我們自己手動創建的。

最終,我們的service邏輯就會變成這樣子:

秒殺操作優化

再次回到我們的秒殺操作,其實需要優化的地方就是我們的GC和行級鎖等待的時間。

我們之前的邏輯是這樣的:先執行減庫存操作,再插入購買成功的記錄

其實,我們可以先插入成功購買的記錄,再執行減庫存的操作!

  • 那這兩者有啥區別呢???減庫存的操作會導致行級鎖的等待,而我們先進行insert的話,那麼就不會被行級鎖所干擾了。並且,我們這中兩個操作是在同一個事物中的,並不會出現“超賣”的情況!

關於先執行insert與先執行update的區別,兩個事務同時insert的情況下,沒有鎖競爭,執行速度會快,當兩個事務先update同一行數據,會有一個事務獲得行鎖,鎖在事務提交之前都不會釋放,所以讓鎖被持有的時間最短能提升效率

所以我們service層的邏輯可以改成這樣:

這不是最終的方案,如果為了性能的優化我們還可以將SQL在Mysql中運行,不受Spring的事務來管理。在Mysql使用存儲過程來進行提交性能



-- 秒殺執行儲存過程
DELIMITER $$ -- console ; 轉換為
$$
-- 定義儲存過程
-- 參數: in 參數   out輸出參數
-- row_count() 返回上一條修改類型sql(delete,insert,update)的影響行數
-- row_count:0:未修改數據 ; >0:表示修改的行數; <0:sql錯誤
CREATE PROCEDURE `seckill`.`execute_seckill`
  (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, create_time)
    VALUES (v_seckill_id, v_phone, 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(1003, 13502178891, now(), @r_result);
-- 獲取結果
SELECT @r_result;

Mybatis調用存儲過程其實和JDBC是一樣的:

在使用存儲過程的時候,我們需要4個參數,其實result是在存儲過程中被賦值的。我們可以通過MapUtils來獲取相對應的值。這是我之前沒有接觸過的。

最後,對於部署的系統架構應該是這樣子的:

總結

花了點時間看了該視頻教程,覺得還是學到了不少的東西的。之前沒有接觸過優化的相關問題,現在給我打開了思路,以及學到了不少的開發規範的問題,也是很贊的。如果是初學者的話是可以去學學的。

如果文章有錯的地方歡迎指正,大家互相交流。習慣在微信看技術文章,想要獲取更多的Java資源的同學,可以關註微信公眾號:Java3y


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 之前做過一個JavaScript版本的2048游戲,最近在學習C++,昨天晚上突然心血來潮,想用C++來實現,因為核心演算法已十分理解,所以兩個小時擼出來一個C++的簡易版本。 簡介 二維數組遍歷,C++基本數據類型,string類,控制結構,函數。 本方法不涉及指針以及面向對象思想,所以可作為 ...
  • pandas and numpy notebook 最近工作交接,整理電腦資料時看到了之前的基於Jupyter學習數據分析相關模塊學習筆記。想著拿出來分享一下,可是Jupyter導出來html文件,博客園不支持js註入,貼圖效果實在太差勁兒。所以只貼了內容,要是有需要文件原版(pdf、md、html ...
  • 文件 文件和文件夾 文件:文本文件、二進位文件 文件夾:(windows) G:\pythonWorkspace\python\study (linux/mac) /home/workspace/python 註意:文件夾路徑的斜杠linux與windows不同 windows下文件路徑:示例 跨平 ...
  • 利用反射擴展數組長度 思想:要擴展數組長度其實就是產生一個新的數組覆蓋舊的數組 備註: ...
  • 1、自定義一個工廠類,實現FactoryBean 交由spring管理public class JedisClusterFactory implements FactoryBean<JedisCluster> { private String hostAndPort; private JedisCl ...
  • 我們把變數從記憶體中變成可存儲或傳輸的過程稱之為序列化,在Python中叫pickling,在其他語言中也被稱之為serialization,marshalling,flattening等等,都是一個意思。 序列化之後,就可以把序列化後的內容寫入磁碟,或者通過網路傳輸到別的機器上。 反過來,把變數內容 ...
  • Python unicode轉義字元\u的處理 python還有更為專業的方法來解決unicode轉義字元問題,那就是unicode escape編碼。 s = s2.decode("unicode escape") 就可以了 ...
  • 創建了一個一維向量和三行散列的矩陣 註意:這裡要求數據是同一結構,shape函數作用:幾行幾列 取值: 修改矩陣中的值: 這裡把5和7的值改成了10 強轉類型: 把int型轉為str型 其他操作: 矩陣初始化: 創建矩陣: 運算: 排序: 特別註意: 讀取txt文件: ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...