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