1. 程式人生 > >分散式商城秒殺實現

分散式商城秒殺實現


秒殺工程的演變,在一致性要求下面對高併發和高速讀寫

秒殺

頁面倒計時

<script th:inline="javascript">
    //搶購URL
    function kill(){
        location.href="/kill/gokill?id=" + [[${kill.id}]] + "&gnumber=1";
    }

    //js的定時器
    // setTimeout(function(){}, 1000);  隔1秒後,執行該方法一次,僅僅只會執行一次
    // setInterval(function(){}, 1000); 每隔1秒都會執行一次
	//使用setInterval+遞迴解決使用者第一秒顯示00的問題
	
    //實現倒計時
    function time(){
        //秒殺開始的時間
        var begin = new Date([[${kill.starttime}]]);
        //獲得當前時間
        var now = new Date();//獲取時間伺服器 - ajax

        //計算相差多久
        var howtime = begin - now;

        if(howtime > 0){
            //計算倒計時
            var day = format(parseInt(howtime/1000/60/60/24));
            var hour = format(parseInt(howtime/1000/60/60%24));
            var min = format(parseInt(howtime/1000/60%60));
            var second = format(parseInt(howtime/1000%60));

            var showtime = day + "天" + hour + "時" + min + "分" + second + "秒";
            $("#span_id").html(showtime);

            setTimeout(function(){
                time();
            }, 1000);

        } else {
            //秒殺開始
            // alert("秒殺開始");
            $("#btn1").attr("disabled",false);
        }
    }
    //呼叫方法
    time();
    
    // setInterval(function(){
    //     time();
    // }, 1000);
    //格式化數字顯示為 00:00:00
    function format(number){
        if(number < 10){
            return "0" + number;
        }
        return number;
    }
</script>

秒殺核心service的演變

一.簡單的service邏輯:
1.查詢kill庫存
2.根據庫存判斷是否秒殺成功
3.成功則生成訂單

@Override
@Transactional
public int kill(Integer id, Integer gnumber, int uid) {

    //先查詢kill庫存
    Kill kill = killDao.queryKillById(id);
    //庫存不足直接返回0
    if(kill.getSave() <= 0){
        return 0;
    }
    //庫存足夠
    //減庫存
    killDao.updateSave(id,gnumber);
    //插入訂單
    Orders orders = new Orders();
    orders.setOrdertime(new Date());
    orders.setOrderid(UUID.randomUUID().toString());
    orders.setUid(uid);

    orderDao.insertOrder(orders);
    return 1;
}

存在問題

秒殺一個高併發和高速讀寫的場景,我們並不能簡單的操作資料庫
1.解決資料一致性,因為高併發多執行緒訪問產生的超買情況
2.解決資料庫的高速讀寫問題,減小資料的負載

問題解決

演變:

1.業務層共享資源加鎖

1.1悲觀鎖的思想

1.1.1 程式碼同步

我們直接在方法上新增synchronized,使用同步程式碼塊,或者互斥鎖,也就是簡單的給業務層的共享資源上鎖

確實可以保證我們的資料一致性,但是帶來的是伺服器處理請求能力的大幅度下降

1.1.2 資料庫使用排它鎖加鎖

<select id="queryKillById">
    select save from t_kill where id = #{id}
</select>

我們在查詢秒殺庫存的時候新增for update,也就是我們的排它鎖,一旦我們查詢了該條資料,其他事務不可以新增任何鎖,也就是我們在對庫存做操作的時候只要事務沒有提交一定可以保證其他事務無法訪問該資料

保證了資料一致性,但仍然是鎖機制肯定我們不希望得到效能下降的結果

1.2樂觀鎖的思想

我們需要知道樂觀鎖並不是鎖機制
樂觀鎖只是一種思想,並不是鎖,我們是通過version或者編碼等實現
1.2.1 通過新增欄位version

每次查詢庫存的時候攜帶版本號,然後每次扣減庫存的時候判斷版本號是否一致,一旦不一致就不允許提交

<select id="queryKillById">
    select save,version from t_kill where id = #{id}
</select>
<update id="updateSave">
  update t_kill 
	  set save = save - #{gnumber} 
	  where id = #{id} and version = #{version}
</update>
響應比悲觀鎖的實現肯定快一點,但是還有一個明顯的缺點,導致了執行緒放棄購買,我們的商品肯定會出現剩餘

1.2.2 通過庫存判斷

也是樂觀鎖的思想,只不過我們不需要維護version欄位,只要我們在扣減庫存的時候對save欄位進行判斷即可,因為insert,delete,update是預設自帶排他鎖的,我們可以保證在執行的時候該資料其他事務無法訪問

<select id="queryKillById">
    select save from t_kill where id = #{id}
</select>
<update id="updateSave">
  update t_kill 
	  set save = save - #{gnumber} 
	  where id = #{id} and save >= gnumber
 </update>

引入Redis

業務邏輯

將資料庫的資料先存放在redis中,然後我們直接對redis進行指令碼操作,將需要新增的訂單資訊也存放到redis中然後判斷save<=0在將快取中的資料添加回資料庫中

編寫lua指令碼保證原子性操作
--key=kill+id
local id = KEYS[1]
--需要購買的商品數量
local number = tonumber(ARGV[1])
--獲得庫存
local save = tonumber(redis.call('get','kill'..id))

if save == null or save <= 0 then
    --購買失敗 庫存不足或者沒有該商品
    return 2
end

--此時庫存的扣減
save = save - number
--設定庫存回到快取
redis.call('set','kill'..id),save)
--快取訂單資料
redis.call('rpush','order'..id,ARGV[2])
-- 秒殺成功 返回結果
if save == 0 then
    -- 0 : 表示最後一次,秒殺結束
    --此時需要將快取中的資料寫回資料庫
    return 0
else
    return 1
end
快取lua指令碼
@Autowired
private RedisTemplate redisTemplate;
private RedisConnection connection;
//lua指令碼的位置
private String luaPath;
private String luaKey;
@PostConstruct
public void init(){
    connection = redisTemplate.getConnectionFactory().getConnection();
    luaPath = this.getClass().getResource("/static/lua/kill.lua").getPath();
    try {
        luaKey = connection.scriptLoad(FileUtils.readFileToByteArray(new File(luaPath)));
    } catch (IOException e) {
        e.printStackTrace();
    }
}
主要業務方法
@Override
@Transactional
public int kill(Integer id, Integer gnumber, int uid) {
Orders orders = new Orders();
orders.setOrdertime(new Date());
orders.setOrderid(UUID.randomUUID().toString());
orders.setUid(uid);
//執行快取的lua指令碼
Long result = connection.evalSha(luaKey, ReturnType.INTEGER, 1,
        (id + "").getBytes(),
        (gnumber + "").getBytes(),
        new Gson().toJson(orders).getBytes());
if(result == 0){
    //秒殺結束
    //為了避免最後一個使用者等待一個將資料寫回資料庫的時間,我們使用非同步的service方法進行寫回
    saveReturnData(id);
}
return result.intValue();//0,1成功 2失敗
}
非同步方法將redis中的資料寫回資料庫
啟動主方法上新增@EnableAsync註解開啟非同步Service
@Override
@Async
@Transactional
public void saveReturnData(Integer id) {
    //通過id獲得資料
    Long size = redisTemplate.opsForList().size("order" + id);
    List<String> ordersStringList = redisTemplate.opsForList().range("order" + id, 0, size);
    List<Orders> ordersList = new ArrayList<>();
    for (int i = 0; i < ordersStringList.size(); i++) {
        Orders orders = new Gson().fromJson(ordersStringList.get(i), Orders.class);
        ordersList.add(orders);
    }
    //批量插入訂單
    orderDao.insertOrderList(ordersList);
    //將庫存設為0
    killDao.updateSave(id,0);
    //刪除redis中的資料
    redisTemplate.delete("kill" + id);
    redisTemplate.delete("order" + id);
}