Lua腳本
lua是一個小巧的腳本語言,其設計目的是爲了通過靈活嵌入應用程序中從而爲應用程序提供靈活的擴展和定製功能,lua由標準c編寫而成,幾乎在所有操作系統和平臺上都可以編譯、運行,reids2.6版本後內嵌了對lua環境的支持,解決了長久以來不能高效地處理cas(check-and-set)命令的缺點,並且可以通過組合使用多個命令,輕鬆實現以前很難實現或者不能高效實現的模式。
其優點包含:
- 輕量級:其中只包含一個精簡的內核和最基本的庫,體積小、速度快,從而適合嵌入在別的程序裏
- 可擴展:Lua提供了非常易於使用的擴展接口和機制
- 減少網絡開銷:可以將多個請求通過腳本的形式一次發送,減少網絡時延和請求次數
- 原子性的操作:Redis會將整個腳本作爲一個整體執行,中間不會被其他命令插入。因此在編寫腳本的過程中無需擔心會出現競態條件,無需使用事務
在業務中的應用
1.創建Lua腳本
public class RedisLua {
public static final String STOCK_LUA;
static {
/**
* 扣減庫存Lua腳本(一次扣減一個庫存)
* KEYS[1]:商品數量key
* KEYS[2]:訂單key
* ARGV[1]:訂單信息
* -1:庫存不足
* -2:重複下單
* 大於等於0:剩餘庫存(扣減之後剩餘的庫存)
*/
StringBuilder sb = new StringBuilder();
sb.append("local goodsKey = KEYS[1];");
sb.append("local orderKey = KEYS[2];");
sb.append("local orderValue = ARGV[1];");
sb.append("local goodsNum = redis.call('get', goodsKey);");
sb.append("local order = redis.call('get', orderKey);");
sb.append("if goodsNum < '1' then");
sb.append(" return -1;");
sb.append("end;");
sb.append("if order == false then");
sb.append(" redis.call('set', orderKey , orderValue);");
sb.append(" return redis.call('decrBy', goodsKey, '1');");
sb.append("else");
sb.append(" return -2;");
sb.append("end;");
STOCK_LUA = sb.toString();
/**
* 扣減庫存Lua腳本(一次扣減多個庫存)
* KEYS[1]:商品數量key
* KEYS[2]:訂單key
* ARGV[1]:扣減數量
* ARGV[2]:訂單信息
* -1:庫存不足
* -2:重複下單
* 大於等於0:剩餘庫存(扣減之後剩餘的庫存)
*/
/*StringBuilder sb = new StringBuilder();
sb.append("local goodsKey = KEYS[1];");
sb.append("local orderKey = KEYS[2];");
sb.append("local decrNum = ARGV[1];");
sb.append("local orderValue = ARGV[2];");
sb.append("local goodsNum = redis.call('get', goodsKey);");
sb.append("local order = redis.call('get', orderKey);");
sb.append("if goodsNum < decrNum then");
sb.append(" return -1;");
sb.append("end;");
sb.append("if order == false then");
sb.append(" redis.call('set', orderKey , orderValue);");
sb.append(" return redis.call('decrBy', goodsKey, decrNum);");
sb.append("else");
sb.append(" return -2;");
sb.append("end;");
STOCK_LUA = sb.toString();*/
}
}
2.創建Redis操作工具類
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
@Service
public class RedisService {
@Autowired
private JedisPool jedisPool;
/**
* 獲取對象
*/
public <T> T get(String key,Class<T> clazz){
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
String sval = jedis.get(key);
//將String轉換爲Bean
T t = stringToBean(sval,clazz);
return t;
}finally {
if(jedis != null) {
jedis.close();
}
}
}
/**
* 設置對象
*/
public <T> boolean set(String key,T value){
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//將Bean轉換爲String
String s = beanToString(value);
if(s == null || s.length() <= 0) {
return false;
}
jedis.set(key, s);
return true;
}finally {
if(jedis != null) {
jedis.close();
}
}
}
/**
* 減少值
*/
public <T> Long decr(String key){
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//返回value減1後的值
return jedis.decr(key);
}finally {
if(jedis != null) {
jedis.close();
}
}
}
/**
* 將字符串轉換爲Bean對象
*/
@SuppressWarnings("unchecked")
public static <T> T stringToBean(String str,Class<T> clazz) {
if(str == null || str.length() == 0 || clazz == null) {
return null;
}
if(clazz == int.class || clazz == Integer.class) {
return ((T) Integer.valueOf(str));
}else if(clazz == String.class) {
return (T) str;
}else if(clazz == long.class || clazz == Long.class) {
return (T) Long.valueOf(str);
}else if(clazz == List.class) {
return JSON.toJavaObject(JSONArray.parseArray(str), clazz);
}else {
return JSON.toJavaObject(JSON.parseObject(str), clazz);
}
}
/**
* 將Bean對象轉換爲字符串類型
*/
public static <T> String beanToString(T value) {
if(value == null){
return null;
}
Class<?> clazz = value.getClass();
if(clazz == int.class || clazz == Integer.class) {
return ""+value;
}else if(clazz == String.class) {
return (String)value;
}else if(clazz == long.class || clazz == Long.class) {
return ""+value;
}else {
return JSON.toJSONString(value);
}
}
}
3.秒殺業務調用
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import cn.com.app.redis.RedisLua;
import cn.com.app.redis.RedisService;
@Controller
public class RedisLuaController implements InitializingBean {
@Autowired
private RedisService redisService;
@Autowired
private JedisPool jedisPool;
//內存標記,減少redis訪問
private HashMap<String, Boolean> localOverMap = new HashMap<String, Boolean>();
/**
* 系統初始化的時把商品庫存加入到緩存中
*/
@Override
public void afterPropertiesSet() throws Exception {
//設置默認商品庫存
redisService.set("goodsStock", "10");
//添加內存標記
localOverMap.put("goodsStock", false);
}
/**
* 請求秒殺,redis+Lua方式
*/
@RequestMapping(value="/miaoshalua")
@ResponseBody
public String miaoshalua(HttpServletRequest request,@RequestParam("userid")String userid){
boolean over = localOverMap.get("goodsStock");
if(over) {
System.out.println("秒殺結束");
return "秒殺結束";
}
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//腳本里的KEYS參數
List<String> keys = new ArrayList<>();
keys.add("goodsStock"); //redis中的商品庫存key
keys.add("order"+userid);//redis中的訂單key
//腳本里的ARGV參數
List<String> args = new ArrayList<>();
args.add("1");//扣減庫存數量
args.add(userid+"_"+UUID.randomUUID().toString());//redis中的訂單Value
long result = (long) jedis.eval(RedisLua.STOCK_LUA, keys, args);
if(result == -1){
System.out.println("庫存不足");
return "庫存不足";
}else if(result == -2){
System.out.println("重複下單");
return "重複下單";
}
System.out.println("秒殺成功,剩餘:" + result);
/**
* 數據庫操作減少庫存,下訂單,在一個事務中
*/
}finally {
if(jedis != null) {
jedis.close();
}
}
return "秒殺成功";
}
}