1.關於lua
Lua腳本可以調用大部分的Redis命令,Redis運行開發者通過編寫腳本傳入Redis,一次性執行多條命令。
使用Lua腳本的好處可以參考pipeline:Redis: pipeline基本原理以及Jedis和Redisson的實現示例
- 提升性能:減少多個命令在I/O上的耗時。
- 原子操作:一個lua腳本內的多個命令的執行時原子操作。
- 腳本複用:lua腳本會加載到redis內存中,可以被多次使用。
2.搶紅包實現
2.1.原理簡析
通過Redis中的兩個數據結構實現搶紅包邏輯:
- 某個紅包的子紅包List:存放着每個紅包的金額。數據結構:List, Key=rp-{紅包ID}, value=紅包金額
- 已搶到紅包的用戶Hash:存放已經搶到紅包的用戶及金額。數據結構:Hash, Key=rp-gain-{紅包ID}, hashKey={用戶ID}, hashValue={紅包金額}
搶紅包的基本邏輯:
- 查看用戶Hash中是否存在用戶,如果存在,則返回0,代表
之前搶到過
;否則繼續。 - 查詢紅包List中的剩餘紅包個數,如果爲0,則返回2,代表
紅包已搶完
;否則繼續。 - 從紅包List中pop一個紅包,和用戶信息一起放在用戶Hash中,並且返回1,代表
本次已搶到
。
2.2.lua腳本
-- 搶紅包的lua腳本
-- KEYS[1] = 紅包隊列Key
-- KEYS[2] = 搶到紅包的用戶Hash Key
-- ARGV[1] = 用戶ID
-- 如果用戶已經搶到過紅包,則返回0
if redis.call('hexists',KEYS[2],ARGV[1]) ~= 0 then
return 0
end
-- 如果用戶沒搶到過,先查看紅包數量是否足夠
if redis.call('llen',KEYS[1]) ~= 0 then
-- 如果仍剩餘紅包,則取出一個紅包
local money = redis.call('rpop',KEYS[1])
-- 將用戶信息和紅包金額保存
redis.call('hset',KEYS[2],ARGV[1],money)
return 1
end
-- 如果無紅包了,則返回2表示紅包已經搶完
return 2
2.3.Java代碼
Redis工具類:RedisUtil
提供Redis連接池的初始化、連接獲取與連接釋放。
/**
* <p>Redis工具類</P>
*
* @author hanchao
*/
public class RedisUtil {
/**
* Redis連接池
*/
private static JedisPool jedisPool;
static {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(1024);
config.setMaxIdle(200);
config.setMaxWaitMillis(10000);
config.setTestOnBorrow(true);
jedisPool = new JedisPool(config, "127.0.0.1", 6379, 10000, null, 0);
}
/**
* 獲取Jedis實例
*/
public synchronized static Jedis getConnection() {
try {
if (jedisPool != null) {
return jedisPool.getResource();
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 釋放jedis資源
*/
public static void close(final Jedis jedis) {
if (jedis != null) {
jedis.close();
}
}
}
客戶端:RedisLuaDemo
主要流程:
- 清空紅包List和用戶Hash的緩存數據。這一步只是爲了方便測試。
- 僞造紅包List數據:隨機生成金額,並記錄總金額。
- 加載搶紅包的Lua腳本,加載之後,redis返回這個腳本的sha加密值。
- 創建多個線程進行搶紅包。
- 查詢Redis,展示搶紅包結果,對比實際紅包被搶總金額。
/**
* <p>搶紅包</P>
* 1.某個紅包的子紅包隊列:存放着每個紅包的金額。數據結構:List, Key=rp-{紅包ID}, value=紅包金額
* 2.已搶到紅包的用戶散列:存放已經搶到紅包的用戶及金額。數據結構:Hash, Key=rp-gain-{紅包ID}, hashKey={用戶ID}, hashValue={紅包金額}
*
* @author hanchao
*/
@Slf4j
public class RedisLuaDemo {
/**
* 搶紅包的lua腳本
* KEYS[1] = 紅包隊列Key
* KEYS[2] = 搶到紅包的用戶Hash Key
* ARGV[1] = 用戶ID
*/
private static final String TEST_SCRIPT = "" +
//如果用戶已經搶到過紅包,則返回0
" if redis.call('hexists',KEYS[2],ARGV[1]) ~= 0 then " +
" return 0 " +
" end " +
//如果用戶沒搶到過,先查看紅包數量是否足夠
" if redis.call('llen',KEYS[1]) ~= 0 then " +
//如果仍剩餘紅包,則取出一個紅包
" local money = redis.call('rpop',KEYS[1]) " +
//將用戶信息和紅包金額保存
" redis.call('hset',KEYS[2],ARGV[1],money) " +
" return 1 " +
" end " +
//如果無紅包了,則返回2表示紅包已經搶完
" return 2 ";
public static void main(String[] args) throws InterruptedException {
//紅包List Key
String redPacketKey = "rp-list-9527";
//搶到紅包的用戶hash Key
String gainUserKey = "rp-user-map-9527";
//紅包分發總金額
Integer sum = 0;
//紅包實際被搶總金額
AtomicReference<Integer> total = new AtomicReference<>(0);
//門閂一:以便所有線程同時開始運行
CountDownLatch switchLatch = new CountDownLatch(1);
//門閂二:以便所有線程都能運行完成
CountDownLatch countLatch = new CountDownLatch(10);
//線程池
ExecutorService executorService = Executors.newCachedThreadPool();
Jedis jedis = null;
try {
jedis = RedisUtil.getConnection();
if (Objects.nonNull(jedis)) {
//先清空
jedis.del(redPacketKey);
jedis.del(gainUserKey);
//預先生成5個紅包
for (int i = 0; i < 5; i++) {
int money = RandomUtils.nextInt(100, 150);
sum += money;
//存入緩存
jedis.lpush(redPacketKey, String.valueOf(money));
}
List<String> redPacketList = jedis.lrange(redPacketKey, 0, jedis.llen(redPacketKey));
log.info("共生成{}元的紅包,金額分別爲:{}", sum, redPacketList);
//腳本加載之後生成的sha編碼
String scriptSha = jedis.scriptLoad(TEST_SCRIPT);
//腳本執行所需的Key列表,也可以在腳本中直接寫死
List<String> keyList = Lists.newArrayList(redPacketKey, gainUserKey);
log.info("=============開始搶紅包=============");
//有7個人搶紅包,有些人搶了2次
for (int i = 0; i < 10; i++) {
//有些人手快,搶了多次
int finalUserId = i % 7;
//進行搶紅包
executorService.submit(new GrabRedPacketTask(switchLatch, countLatch, finalUserId, scriptSha, keyList));
}
switchLatch.countDown();
executorService.shutdown();
countLatch.await();
//顯示搶紅包情況
log.info("=============搶紅包結束=============");
Map<String, String> gainUserMap = jedis.hgetAll(gainUserKey);
gainUserMap.forEach((userId, money) -> {
log.info("UserId:{},money:{}", userId, money);
Integer now = total.get();
total.compareAndSet(now, now + Integer.parseInt(money));
});
log.info("共生成{}元的紅包,實際被搶紅包總額{}元。", sum, total.get());
}
} finally {
//關閉redis
RedisUtil.close(jedis);
}
}
}
搶紅包線程:GrabRedPacketTask
通過Redis執行Lua腳本一般需要三個參數:
- 腳本的sha值,用於定位腳本。
- keyList,Key參數列表,在腳本中通過KEYS[1]、KEYS[2] … KEYS[n-1]來獲取參數。
- argsList,其他參數列表,在腳本中通過ARGV[1]、ARGV[2] … ARGV[n-1]來獲取參數。
/**
* <p>搶紅包線程</P>
*
* @author hanchao
*/
@Slf4j
@AllArgsConstructor
public class GrabRedPacketTask implements Runnable {
/**
* 門閂一:以便所有線程同時開始運行
*/
private CountDownLatch switchLatch;
/**
* 門閂二:以便所有線程都能運行完成
*/
private CountDownLatch countLatch;
/**
* 用戶ID
*/
private Integer userId;
/**
* 搶紅包腳本sha
*/
private String scriptSha;
/**
* 搶紅包腳本執行所需的Key列表
*/
private List<String> keyList;
/**
* 搶紅包
*/
@Override
public void run() {
try {
switchLatch.await();
} catch (InterruptedException e) {
log.error("error");
}
//腳本執行所需的其他參數列表,也可以在腳本中直接寫死
List<String> argList = Lists.newArrayList(String.valueOf(userId));
Jedis jedis = null;
try {
//搶紅包
jedis = RedisUtil.getConnection();
if (Objects.nonNull(jedis)) {
//這裏的腳本利用的是其他地方已經加載好的
String result = jedis.evalsha(scriptSha, keyList, argList).toString();
switch (Integer.parseInt(result)) {
case 0:
log.info("UserId:{} 已經搶到了紅包,不能再搶.", userId);
break;
case 1:
log.info("UserId:{} 眼疾手快,搶到了紅包 !!!", userId);
break;
case 2:
log.info("UserId:{} 手慢,沒搶到紅包.", userId);
break;
default:
log.info("Illegal Result: {}", result);
break;
}
}
} finally {
countLatch.countDown();
RedisUtil.close(jedis);
}
}
}
運行結果:
INFO - 共生成607元的紅包,金額分別爲:[115, 137, 101, 145, 109]
INFO - =============開始搶紅包=============
INFO - UserId:0 眼疾手快,搶到了紅包 !!!
INFO - UserId:2 眼疾手快,搶到了紅包 !!!
INFO - UserId:1 眼疾手快,搶到了紅包 !!!
INFO - UserId:0 已經搶到了紅包,不能再搶.
INFO - UserId:6 眼疾手快,搶到了紅包 !!!
INFO - UserId:5 眼疾手快,搶到了紅包 !!!
INFO - UserId:2 已經搶到了紅包,不能再搶.
INFO - UserId:4 手慢,沒搶到紅包.
INFO - UserId:3 手慢,沒搶到紅包.
INFO - UserId:1 已經搶到了紅包,不能再搶.
INFO - =============搶紅包結束=============
INFO - UserId:0,money:109
INFO - UserId:1,money:101
INFO - UserId:2,money:145
INFO - UserId:5,money:115
INFO - UserId:6,money:137
INFO - 共生成607元的紅包,實際被搶紅包總額607元。
3.注意事項
- 類似於pipeline,不要在lua腳本中執行耗時長的命令或者過多的命令,以防阻塞其他命令。
- lua腳本會加載到redis內存中,節點重啓,則腳本消失。