redis+lua實現高併發商品秒殺案例

用redis+lua不會出現商品賣超,減庫存問題,不用加鎖,只需要在lua腳本中把業務寫好,一切都是這麼簡單。redis的好處就是多路io複用,基於內存。存儲快速。

redis+Lua腳本
1、減少網絡開銷,如果一個功能需要多次請求redis,使用腳本完成同樣的操作只需要請求一次,減少了網絡往返
2、原子操作,redis會將lua腳本作爲一個整體執行,中間不會被其他命令插入,無需擔心競態條件,無需使用事務
3、複用,客戶端發送的腳本會永久存儲在redis中,其他客戶端可以複用這一腳本

我用的是單體服務進行跑批。999個商品一萬個線程。用了一分鐘(redis+lua執行時間+異步訂單寫入數據庫)tomcat服務拒絕3500次(單體),tomcat服務吐吞量在400/s,如果真正意義上的秒殺,單體服務肯定是不行的,服務肯定要考慮負載和集羣,分佈等,此次模擬只是測試。由於高併發的特點是瞬間用戶量大,對服務配置要求要高點。我的電腦配置是算差的了。所以真正線上商品秒殺要根據用戶量去選型配置。
好了今天就說到這裏。看下面的代碼吧。有問題可以隨時撩我。只要我在線

1、定義lua腳本
local productId = tostring(KEYS[1])
local uid = tostring(ARGV[1])
-- 成功函數
local function successFun(success, msg, data)
    success = success or 1
    msg = msg or ""
    data = data or {}
    return cjson.encode({success = success, msg = msg, data = data})
end
-- 錯誤函數
local function response(errno, msg, data)
    errno = errno or 0
    msg = msg or ""
    data = data or {}
    return cjson.encode({errno = errno, msg = msg, data = data})
end
-- 判斷用戶沒有搶過該商品
local log_key = "LOG_{" .. productId .. "}"
-- return log_key
local has_fetched = redis.call("sIsMember", log_key, uid)
if (has_fetched ~= 0) then
    return response(-1, "已經搶過該商品了")
end
local result = false

-- 遍歷商品所有批次
local quan_key = "QUAN_{" .. productId .. "}"
local param = productId.."@";
local product = redis.call("hgetall",param)
if product==nil then
   return response(-1, "商品數據不存在")
end
local nums = redis.call("hget",param,"num");
local n = tonumber(nums);
if (n<=0)then
  return response(-1, "暫無庫存")
end
redis.call("sAdd", log_key, uid)
local num = n-1;
local json = {};
json["id"] = productId;
json["num"] = n;
result = {uid = uid, pid = productId,  product = json}
--把人員訂單信息寫入redis
redis.call("rPush", "DB_QUEUE", cjson.encode(result))
---修改庫存
redis.call("hset", param, "num",(num))
redis.call('rPush',"user",cjson.encode(result))
if (result == false) then
    return response(-1, "商品已搶完")
else
    return successFun(1, "秒殺成功", result)
end

 

2、封裝redis工具

package com.bus.utils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;

/**
 * @author wwz
 * @date 2020-03-06
 * @descrption:
 */
@Component
public class RedisUtils {

    private final  Logger log = LoggerFactory.getLogger(this.getClass());
    private final  String expireTime = "50000";

    @SuppressWarnings("rawtypes")
    @Autowired
    private StringRedisTemplate stringRedisTemplateDemo;

    private DefaultRedisScript<String> getLockRedisScript;
    private DefaultRedisScript<String> releaseLockRedisScript;
    private DefaultRedisScript<String> realRedisScript;

    private StringRedisSerializer argsStringSerializer = new StringRedisSerializer();
    private StringRedisSerializer resultStringSerializer = new StringRedisSerializer();
    private StringRedisSerializer realStringSerializer = new StringRedisSerializer();

    private final String EXEC_RESULT = "1";
    @SuppressWarnings("unchecked")
    @PostConstruct
    private void init() {
        getLockRedisScript = new DefaultRedisScript<String>();
        getLockRedisScript.setResultType(String.class);
        releaseLockRedisScript = new DefaultRedisScript<String>();
        realRedisScript = new DefaultRedisScript<String>();
        releaseLockRedisScript.setResultType(String.class);
        realRedisScript.setResultType(String.class);

        // 初始化裝載 lua 腳本
        getLockRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("script/getLock.lua")));
        releaseLockRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("script/releaseLock.lua")));
        realRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("script/real.lua")));

    }

   
    /**
     * 原子操作
     * @param key
     * @param requestId
     * @param retryTimes
     * @return
     */
    public JSONObject set(String key, String requestId, int retryTimes) {
        try {
            int count = 0;
            while (true) {
                String result = stringRedisTemplateDemo.execute(realRedisScript, argsStringSerializer, realStringSerializer,
                        Collections.singletonList(key), requestId);
                JSONObject object = JSON.parseObject(result);
                log.debug("result:{},type:{}", result, result.getClass().getName());
                return object;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public boolean get(String key, String requestId) {
        String result = stringRedisTemplateDemo.execute(releaseLockRedisScript, argsStringSerializer, resultStringSerializer,
                Collections.singletonList(key), requestId);
        if (EXEC_RESULT.equals(result)) {
            return true;
        }
        return false;
    }


}

 

控制層調用

@RequestMapping("buy")
@ResponseBody
public Object orderBy(String productId){
    String requestId = UUID.randomUUID().toString();
    try{
        //Executors.newFixedThreadPool()
        JSONObject object = redisUtils.set(productId,requestId,0);
        if(object == null){
            return JsonResult.Fail("服務中斷,請稍後重試!");
        }
        String success = object.getString("success");
        if("1".equals(success)){
            taskExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    orderService.createOrder(object.getJSONObject("data"),requestId);
                }
            });
            return JsonResult.OK("恭喜你搶到了");
        }
        return JsonResult.Fail(object.getString("msg"));
    }catch (Exception e){
        e.printStackTrace();
        return JsonResult.Fail("服務中斷,請稍後重試!");
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章