用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 getLockRedisScript;
private DefaultRedisScript releaseLockRedisScript;
private DefaultRedisScript 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();
getLockRedisScript.setResultType(String.class);
releaseLockRedisScript = new DefaultRedisScript();
realRedisScript = new DefaultRedisScript();
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("服务中断,请稍后重试!");
}
}