redis 事務與Lua腳本

一.原理

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事務纔可以更好解決的場景。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章