相比关系型数据库中的事务模型,Redis 中事务要简单一些。Redis 中的事务不能保证原子性,也就是说,事务中某一个命令执行时出现异常不会影响其它命令的执行;Redis 中的事务具有隔离性,即当前事物可以不被其它事务打断,但没有隔离级别的概念。
一、事务基本概念
在 Redis 中,事务相关的常用命令如下:
-
multi
:开启事务。 - 输入要执行的 Redis 命令,可以理解为命令入队列。
-
exec
:执行事务,会依次执行队列中的命令,各个命令的执行结果会在exec
结束后统一返回。 -
discard
:取消事务,放弃执行队列中的命令,注意,已经开始执行的事务是无法取消的。 -
watch
:在通过multi
开启事务之前,我们可以使用watch
命令监控指定的 key,在事务执行之前,如果被监控的 key 对应的值被修改了,exec
将放弃执行当前事务队列中的所有命令,这也是一种乐观锁
实现。 -
unwatch
:是和watch
对应的命令,用来取消对 key 的监控,但如果执行了exec
或discard
命令,则事务中所有被监控的 key 都将自动取消监控,则无需再手动执行该命令了。
下边我们结合Jedis
来看看如何使用事务。
二、事务的基本操作
public class TransactionTest {
public static void main(String[] args) {
new TransactionTest().test1();
}
public void test1() {
JedisPool jedisPool = new JedisPool("localhost", 6379);
Jedis jedis = jedisPool.getResource();
jedis.auth("shehuan");
jedis.flushDB();
// 开启事务
Transaction tx = jedis.multi();
try {
// 命令入队
tx.set("key1", "value1");
tx.incr("key1");
tx.set("key2", "10");
tx.incr("key2");
// 执行事务
List<Object> results = tx.exec();
// 查看事务执行结果
results.forEach(r -> {
System.out.println(r.toString());
});
} catch (Exception e) {
e.printStackTrace();
} finally {
jedis.close();
}
}
}
从上边可以看出
tx.incr("key1")
命令虽然执行失败,但不影响其它命令,事务正常执行结束,这也验证了 Redis 中的事务不能保证原子性。
三、取消事务
public class TransactionTest {
public static void main(String[] args) {
new TransactionTest().test2();
}
public void test2() {
JedisPool jedisPool = new JedisPool("localhost", 6379);
Jedis jedis = jedisPool.getResource();
jedis.auth("shehuan");
jedis.flushDB();
// 开启事务
Transaction tx = jedis.multi();
try {
// 命令入队
tx.set("key1", "value1");
tx.set("key2", "10");
// 制造异常
int i = 1 / 0;
// 执行事务
List<Object> results = tx.exec();
// 查看事务执行结果
results.forEach(r -> {
System.out.println(r.toString());
});
} catch (Exception e) {
e.printStackTrace();
// 取消事务
tx.discard();
System.out.println("key1=" + jedis.get("key1"));
System.out.println("key2=" + jedis.get("key2"));
} finally {
jedis.close();
}
}
}
在事务执行前,我们手动制造了异常,这样事务就不会执行,捕获异常后再取消事务,如果有业务上的异常我们可以这样处理。注意,在事务执行过程中发生异常是无法被捕获的,文中第一个例子就说明了这一点。
四、watch 监控
有关watch
命令的作用在前边已经介绍过了,下边通过一个简单的商品抢购例子看下具体的用法。
public class TransactionTest {
public static void main(String[] args) {
new TransactionTest().test3();
}
public void test3() {
JedisPool jedisPool = new JedisPool("localhost", 6379);
Jedis jedis = jedisPool.getResource();
jedis.auth("shehuan");
jedis.flushDB();
// 设置商品库存为1000件
jedis.set("stock", "1000");
// 监控库存
jedis.watch("stock");
// 获取库存
int stock = Integer.parseInt(jedis.get("stock"));
// 如果库存大于购买数量
if (stock > 10) {
stock = stock - 10;
} else {
// 取消监控
jedis.unwatch();
return;
}
// 开启事务
Transaction tx = jedis.multi();
try {
// 减扣库存
tx.set("stock", String.valueOf(stock));
// 执行事务
List<Object> results = tx.exec();
// 如果事务执行过程中发现监控的 key 对应的值发生改变,也就是库存在其它地方被修改,则事务的执行结果为 null
if (results == null) {
System.out.println("库存减扣失败!");
} else {
System.out.println("剩余库存:" + jedis.get("stock"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
jedis.close();
}
}
}
在执行事务代码行打上断点,然后运行程序:
在 Redis 客户端窗口修改库存:
现在释放断点,让程序继续执行,最终结果如下:
由于在程序监控库存后,我们又在客户端窗口修改了库存,导致事务执行时发现监控的 key 对应的值发生了变化,所以放弃执行事务中的命令,不会去减扣库存,此时事务的执行结果为null
。
正常情况下,如果我们不人工干预,则结果符合我们的预期:
网友评论