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("服务中断,请稍后重试!");
    }
}
发布了71 篇原创文章 · 获赞 22 · 访问量 11万+

猜你喜欢

转载自blog.csdn.net/saygood999/article/details/104801408