一.原理
1.redis事務
基本原理爲樂觀鎖,多個client對操作的key進行watch,一旦有一個client進行了exec,那麼其它client的exec就會失效。其實現原理可參考 Redis watch機制的分析。
2.lua腳本
基本原理爲使腳本相當於一個redis命令,可以結合redis原有命令,自定義腳本邏輯。
3.兩者異同
相同點
很好的實現了一致性、隔離性和持久性,但沒有實現原子性,無論是redis事務,還是lua腳本,如果執行期間出現運行錯誤,之前的執行過的命令是不會回滾的。
不同點
(1)redis事務是基於樂觀鎖,lua腳本是基於redis的單線程執行命令。
(2)redis事務的執行原理就是一次命令的批量執行,而lua腳本可以加入自定義邏輯。
二.問題
1.使用場景是什麼
秒殺
因爲redis事務的實現原理是樂觀鎖,所以在高併發的秒殺場景並不是很適合。這裏推薦使用lua腳本來實現,原因是
(1)使用lua腳本能夠很好的按照線程的先後把庫存扣減,後面的線程如果發現庫存不夠了,那麼就直接拒絕掉。
(2)使用redis事務,因爲高併發情況下,多個線程同時watch相同的key,一旦這個時刻有線程先提交了,那麼其它線程的提交就失效了,這樣會導致redis產生很多沒用的請求,而且與線程的先後執行關係不大,這個過程會不斷重複,性能不高。
(3)總的來說,lua腳本能夠實現每次扣減庫存的執行過程只有一個線程,redis事務每次扣減庫存的操作是有一批線程,但只有一個成功。
以下爲兩種實現的代碼對比。
package test.cache;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
/**
* @author jaron
* @date 2019/8/3
*/
public class SecondKill {
public static final String STOCK_TEST_KEY = "stock_test_a";
public static final String STOCK = "1000";
public static void main(String[] args) {
initKey();
redisScriptDeduct();
// redisTransactionDeduct();
// batchDeduct(()->redisScriptDeduct());
// batchDeduct(()->redisTransactionDeduct());
}
//併發測試代碼
private static void batchDeduct(Deduct deduct) {
int threadNum = 334;
CountDownLatch countDownLatch = new CountDownLatch(threadNum);
Long start = System.currentTimeMillis();
for (int i = 0; i < threadNum; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
countDownLatch.await();
deduct.execute();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
countDownLatch.countDown();
}
while (true) {
Long restNum = Long.valueOf((String) get(STOCK_TEST_KEY));
if (restNum == 1) {
System.out.println("milliseconds:" + (System.currentTimeMillis() - start));
return;
}
}
}
//事務實現扣減庫存
public static void redisTransactionDeduct(){
Jedis jedis = RedisUtil.getInstance();
try {
String watch = jedis.watch(STOCK_TEST_KEY);
System.out.println("watch:" + watch);
Integer restNum = Integer.valueOf(jedis.get(STOCK_TEST_KEY));
Integer buyNum = 3;
Transaction transaction = jedis.multi();
if (restNum - buyNum < 0){
transaction.discard();
}
transaction.decrBy(STOCK_TEST_KEY,buyNum);
System.out.println("result:" + transaction.exec());
}catch (Exception e){
e.printStackTrace();
} finally {
if (jedis != null){
jedis.close();
}
}
}
static String script =
"local num = ARGV[1] \n" +
"local key = KEYS[1] \n" +
"local stock = redis.call('get',key) \n" +
"if stock - num >= 0 \n" +
"then redis.call('decrby',key, num) \n" +
"return 1 \n" + // 成功
"else \n" +
"return 0 \n" + //失敗
"end";
//lua腳本實現扣減庫存
public static void redisScriptDeduct(){
Jedis jedis = RedisUtil.getInstance();
try {
Object re = jedis.evalsha(jedis.scriptLoad(script), Arrays.asList(STOCK_TEST_KEY), Arrays.asList("3"));
System.out.println("deduct:" + re);
} catch (Exception e){
e.printStackTrace();
} finally {
if (jedis != null){
jedis.close();
}
}
}
public static void initKey(){
Jedis jedis = RedisUtil.getInstance();
try {
jedis.set(STOCK_TEST_KEY, STOCK);
} finally {
if (jedis != null){
jedis.close();
}
}
Long restNum = Long.valueOf((String)get(STOCK_TEST_KEY));
System.out.println("init:" + restNum);
}
public static Object get(String key){
Jedis jedis = RedisUtil.getInstance();
try {
return jedis.get(key);
} finally {
if (jedis != null){
jedis.close();
}
}
}
@FunctionalInterface
interface Deduct{
void execute();
}
}
package test.cache;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/**
* @author jaron
* @date 2019/8/3
*/
public class RedisUtil {
private RedisUtil(){
}
static class Pool{
private static final JedisPool INSTANCE = initPool();
private static JedisPool initPool() {
JedisPool jedisPool = null;
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(10240);
jedisPoolConfig.setMaxIdle(100);
jedisPoolConfig.setMaxWaitMillis(1000);
jedisPoolConfig.setTestOnBorrow(false);
jedisPoolConfig.setTestOnReturn(true);
jedisPool = new JedisPool(jedisPoolConfig, "127.0.0.1", 6379, 2000);
return jedisPool;
}
}
public static Jedis getInstance(){
return Pool.INSTANCE.getResource();
}
}
限流
可參考 redis的兩種限流方式
文中的兩種方式分別對應的就是redis事務實現和lua腳本的實現。
2.如何選擇
redis事務的可以直接通過應用程序實現,比較簡單。lua腳本更靈活,在大部分場景下使用lua腳本都能夠很好的解決高併發帶來的問題,而且性能比較高。至於redis事務,說實話,在項目中用的比較少,因爲redis事務能夠解決的問題,lua腳本都能夠解決,甚至更好的解決。暫時沒有想到一些非要用redis事務纔可以更好解決的場景。