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事务才可以更好解决的场景。

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