场景示例
假设你正在开发一个电商系统,使用 Redis 来管理商品库存和用户积分。在一个业务操作中,当用户购买商品时,需要同时减少商品库存并增加用户积分。这两个操作需要在一个事务中保证原子性,要么都成功,要么都失败。
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
public class RedisTransactionExample {
public static void main(String[] args) {
try (Jedis jedis = new Jedis("localhost", 6379)) {
// 商品库存键
String productStockKey = "product:1:stock";
// 用户积分键
String userPointsKey = "user:1:points";
// 获取当前商品库存和用户积分
Integer productStock = Integer.parseInt(jedis.get(productStockKey));
Integer userPoints = Integer.parseInt(jedis.get(userPointsKey));
int purchaseQuantity = 1;
int pointsToAdd = 10;
// 检查库存是否足够
if (productStock < purchaseQuantity) {
System.out.println("Insufficient stock");
return;
}
// 开启事务
Transaction transaction = jedis.multi();
try {
transaction.decrBy(productStockKey, purchaseQuantity);
transaction.incrBy(userPointsKey, pointsToAdd);
// 执行事务
transaction.exec();
System.out.println("Purchase and points addition successful");
} catch (Exception e) {
System.out.println("Transaction failed: " + e.getMessage());
// 这里如果发生异常,Redis不会自动回滚已执行的命令
}
}
}
}
解决办法
- 应用层手动回滚:在捕获到异常后,在应用层手动编写回滚逻辑。
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
public class RedisTransactionRollbackExample {
public static void main(String[] args) {
try (Jedis jedis = new Jedis("localhost", 6379)) {
// 商品库存键
String productStockKey = "product:1:stock";
// 用户积分键
String userPointsKey = "user:1:points";
// 获取当前商品库存和用户积分
Integer productStock = Integer.parseInt(jedis.get(productStockKey));
Integer userPoints = Integer.parseInt(jedis.get(userPointsKey));
int purchaseQuantity = 1;
int pointsToAdd = 10;
// 检查库存是否足够
if (productStock < purchaseQuantity) {
System.out.println("Insufficient stock");
return;
}
// 开启事务
Transaction transaction = jedis.multi();
try {
transaction.decrBy(productStockKey, purchaseQuantity);
transaction.incrBy(userPointsKey, pointsToAdd);
// 执行事务
transaction.exec();
System.out.println("Purchase and points addition successful");
} catch (Exception e) {
System.out.println("Transaction failed: " + e.getMessage());
// 手动回滚
if (productStock!= null) {
jedis.incrBy(productStockKey, purchaseQuantity);
}
if (userPoints!= null) {
jedis.decrBy(userPointsKey, pointsToAdd);
}
}
}
}
}
在这个改进后的代码中,当事务执行过程中捕获到异常,会在应用层手动将商品库存和用户积分恢复到事务执行前的状态,从而保证业务逻辑的正确性。
除了在应用层手动回滚,还可以考虑以下几种解决 Redis 事务中命令执行失败问题的办法:
1. 使用 Lua 脚本
- 原理:Lua 脚本在 Redis 中是原子执行的,即脚本一旦开始执行,不会被其他命令打断。可以将相关操作封装在 Lua 脚本中,利用 Lua 的逻辑判断来处理可能出现的错误情况,确保所有操作要么全部成功,要么全部失败。
import redis.clients.jedis.Jedis;
import java.util.Arrays;
import java.util.List;
public class RedisLuaTransactionExample {
public static void main(String[] args) {
try (Jedis jedis = new Jedis("localhost", 6379)) {
String productStockKey = "product:1:stock";
String userPointsKey = "user:1:points";
int purchaseQuantity = 1;
int pointsToAdd = 10;
String luaScript = "local productStock = tonumber(redis.call('GET', KEYS[1]))\n" +
"local userPoints = tonumber(redis.call('GET', KEYS[2]))\n" +
"if productStock < ARGV[1] then\n" +
" return {0, 'Insufficient stock'}\n" +
"end\n" +
"redis.call('DECRBY', KEYS[1], ARGV[1])\n" +
"redis.call('INCRBY', KEYS[2], ARGV[2])\n" +
"return {1, 'Operation successful'}\n";
List<String> keys = Arrays.asList(productStockKey, userPointsKey);
List<String> args = Arrays.asList(String.valueOf(purchaseQuantity), String.valueOf(pointsToAdd));
List<Object> result = jedis.eval(luaScript, keys, args);
if ((long) result.get(0) == 0) {
System.out.println("Transaction failed: " + result.get(1));
} else {
System.out.println("Purchase and points addition successful");
}
}
}
}
- 优势:Lua 脚本可以在 Redis 服务器端原子地执行多个操作,避免了应用层与 Redis 之间多次交互,减少了网络开销。同时,Lua 脚本内可以实现复杂的逻辑判断,确保所有操作的一致性。
2. 采用乐观锁机制
- 原理:在执行事务前,先获取数据的版本号或时间戳。在事务执行过程中,每次操作前再次检查版本号或时间戳是否发生变化。如果发生变化,说明数据在其他地方被修改过,事务需要重新执行。
- 示例:
import redis.clients.jedis.Jedis;
public class RedisOptimisticLockExample {
public static void main(String[] args) {
try (Jedis jedis = new Jedis("localhost", 6379)) {
String productStockKey = "product:1:stock";
String userPointsKey = "user:1:points";
String versionKey = "product:1:version";
int purchaseQuantity = 1;
int pointsToAdd = 10;
while (true) {
// 获取版本号
String version = jedis.get(versionKey);
// 获取当前商品库存和用户积分
Integer productStock = Integer.parseInt(jedis.get(productStockKey));
Integer userPoints = Integer.parseInt(jedis.get(userPointsKey));
// 检查库存是否足够
if (productStock < purchaseQuantity) {
System.out.println("Insufficient stock");
return;
}
// 开启事务
jedis.watch(versionKey);
if (!version.equals(jedis.get(versionKey))) {
jedis.unwatch();
continue;
}
try {
jedis.multi();
jedis.decrBy(productStockKey, purchaseQuantity);
jedis.incrBy(userPointsKey, pointsToAdd);
jedis.incr(versionKey);
jedis.exec();
System.out.println("Purchase and points addition successful");
break;
} catch (Exception e) {
jedis.unwatch();
System.out.println("Transaction failed, retrying...");
}
}
}
}
}
优势:乐观锁机制在高并发场景下,对于读操作较多的情况性能较好,因为它不需要像悲观锁那样在操作前一直持有锁,减少了锁竞争带来的性能开销。同时,它可以在一定程度上保证数据的一致性,避免因数据并发修改导致的问题。
5481



被折叠的 条评论
为什么被折叠?



