Redis 作为高性能内存数据库,在日常开发中使用广泛。当需要保证多个操作原子性时,事务功能就显得尤为重要。本文将探讨 Redis 事务的工作原理及如何在 Java 中正确实现。
问题场景
想象一个电商平台的库存管理场景:用户下单时,我们需要同时完成减库存和创建订单两个操作。这两个操作必须是原子的,否则在高并发环境下可能出现数据不一致。
下面是一个不使用事务的代码:
public boolean createOrder(String productId, String userId, int quantity) {
Jedis jedis = new Jedis("localhost", 6379);
try {
// 检查库存
String stockKey = "product:" + productId + ":stock";
int stock = Integer.parseInt(jedis.get(stockKey));
if (stock < quantity) {
return false; // 库存不足
}
// 减少库存
jedis.decrBy(stockKey, quantity);
// 假设这里系统崩溃
// 创建订单
String orderKey = "order:" + UUID.randomUUID().toString();
jedis.hset(orderKey, "userId", userId);
jedis.hset(orderKey, "productId", productId);
jedis.hset(orderKey, "quantity", String.valueOf(quantity));
return true;
} finally {
jedis.close();
}
}
这段代码的问题在于:如果系统在减库存后、创建订单前崩溃,将导致库存减少但订单未创建,造成数据不一致。
Redis 事务基本原理
Redis 事务与传统数据库事务有很大不同。Redis 通过 MULTI、EXEC、DISCARD 和 WATCH 四个命令来实现事务功能:
- MULTI:标记事务开始
- EXEC:执行事务队列中的所有命令
- DISCARD:取消事务,放弃执行事务队列中的命令
- WATCH:监视一个或多个键,在事务执行前检测这些键是否被修改
Redis 事务的特点:
- 命令会被打包,然后一次性、顺序性、排他性地执行
- 不支持回滚机制,即使事务中某个命令执行失败,其他命令仍会继续执行
- 可以通过 WATCH 命令实现乐观锁,处理并发修改问题
Redis 事务与传统事务的本质区别
传统数据库事务严格遵循 ACID 原则(原子性、一致性、隔离性、持久性),而 Redis 事务更像是"命令批处理"机制:
- 原子性:Redis 仅保证事务内命令的"全或无"执行,但不支持回滚
- 一致性:Redis 不检查命令执行结果的一致性状态
- 隔离性:由于 Redis 单线程执行命令,天然具有隔离性
- 持久性:取决于 Redis 的持久化配置,与事务机制无关
Redis 事务的底层实现原理
Redis 事务的实现相对简单,主要依赖几个核心数据结构:
// Redis事务核心数据结构(简化版)
struct client {
dict *watched_keys; // 存储WATCH的键,键->数据库指针
list *cmd_queue; // 事务队列,存储MULTI后的命令
int flags; // 标记是否在事务中(CLIENT_MULTI)
};
当客户端执行 WATCH 命令时,Redis 会在内部为键添加观察者,并在键被修改时标记相关客户端。当执行 EXEC 时,Redis 会检查是否有被监视的键发生了变化,如果有则拒绝执行事务。
这种设计让 Redis 事务能够在保证原子性的同时,避免了传统事务中的锁开销,非常适合高性能场景。
优化的 Redis 事务 Java 实现
下面是经过优化的使用 Jedis 客户端实现 Redis 事务的完整代码:
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisConnectionException;
import redis.clients.jedis.exceptions.JedisDataException;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
public class RedisTransactionDemo {
private static final Logger log = Logger.getLogger(RedisTransactionDemo.class.getName());
private static final String REDIS_HOST = "localhost";
private static final int REDIS_PORT = 6379;
private static final int MAX_RETRY = 3;
// 订单字段常量定义
private static final String ORDER_USER_ID = "userId";
private static final String ORDER_PRODUCT_ID = "productId";
private static final String ORDER_QUANTITY = "quantity";
private static final String ORDER_STATUS = "status";
// 初始化Redis连接池
private static final JedisPool jedisPool;
static {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(100); // 最大连接数
config.setMaxIdle(20); // 最大空闲连接
config.setMinIdle(10); // 最小空闲连接
config.setMaxWaitMillis(3000); // 连接获取最大等待时间(ms)
config.setTestOnBorrow(true); // 获取连接时测试
config.setTestWhileIdle(true); // 空闲时测试连接有效性
jedisPool = new JedisPool(config, REDIS_HOST, REDIS_PORT);
}
/**
* 创建订单并减少库存,使用Redis事务保证原子性
*/
public String createOrder(String productId, String userId, int quantity) {
try (Jedis jedis = jedisPool.getResource()) {
try {
// 订单ID在每次尝试时重新生成
String orderId = UUID.randomUUID().toString();
String stockKey = "product:" + productId + ":stock";
String orderKey = "order:" + orderId;
int retryCount = 0;
while (retryCount < MAX_RETRY) {
// 监视库存键,实现乐观锁
jedis.watch(stockKey);
// 检查库存
String stockStr = jedis.get(stockKey);
if (stockStr == null) {
jedis.unwatch();
return null; // 商品不存在
}
int stock = Integer.parseInt(stockStr);
if (stock < quantity) {
jedis.unwatch();
return null; // 库存不足
}
// 开始事务
Transaction transaction = jedis.multi();
// 减少库存
transaction.decrBy(stockKey, quantity);
// 创建订单
transaction.hset(orderKey, ORDER_USER_ID, userId);
transaction.hset(orderKey, ORDER_PRODUCT_ID, productId);
transaction.hset(orderKey, ORDER_QUANTITY, String.valueOf(quantity));
transaction.hset(orderKey, ORDER_STATUS, "created");
// 执行事务
List<Object> results = null;
try {
results = transaction.exec();
} catch (JedisDataException e) {
// 处理网络相关异常,可以立即重试
if (isNetworkRelatedException(e)) {
retryCount--; // 不消耗重试次数
continue;
}
throw e;
}
// 如果事务成功执行(没有被打断)
if (results != null) {
return orderId;
}
// 事务执行失败,说明监视的键被修改,使用指数退避重试
retryCount++;
try {
// 指数退避算法:2^retryCount * 100ms + 随机因子(±50ms)
long delay = ((1L << retryCount) * 100) +
ThreadLocalRandom.current().nextInt(100) - 50;
TimeUnit.MILLISECONDS.sleep(delay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warning("事务重试被中断");
return null;
}
}
log.warning("创建订单失败:多次重试后仍无法完成事务");
return null; // 多次重试后仍失败
} catch (Exception e) {
log.severe("Redis事务执行异常: " + e.getMessage());
return null;
}
} catch (JedisConnectionException e) {
log.severe("Redis连接失败: " + e.getMessage());
return null;
}
}
/**
* 判断是否为网络相关异常
*/
private boolean isNetworkRelatedException(Exception e) {
return e instanceof JedisConnectionException ||
e.getMessage().contains("connection") ||
e.getMessage().contains("timeout");
}
/**
* 使用Lua脚本实现原子性的库存检查和扣减
*/
public String createOrderWithLua(String productId, String userId, int quantity) {
try (Jedis jedis = jedisPool.getResource()) {
String orderId = UUID.randomUUID().toString();
String stockKey = "product:" + productId + ":stock";
String orderKey = "order:" + orderId;
// Lua脚本:原子性地检查并减少库存
String checkAndDecrScript =
"local stock = redis.call('get', KEYS[1]) " +
"if not stock then return 0 end " + // 库存键不存在
"if tonumber(stock) >= tonumber(ARGV[1]) then " +
" redis.call('decrby', KEYS[1], ARGV[1]) " +
" return 1 " +
"else " +
" return 0 " +
"end";
// 执行脚本
Object result = jedis.eval(checkAndDecrScript, 1, stockKey, String.valueOf(quantity));
if (Long.valueOf(1).equals(result)) {
// 库存充足且已扣减,创建订单
jedis.hset(orderKey, ORDER_USER_ID, userId);
jedis.hset(orderKey, ORDER_PRODUCT_ID, productId);
jedis.hset(orderKey, ORDER_QUANTITY, String.valueOf(quantity));
jedis.hset(orderKey, ORDER_STATUS, "created");
return orderId;
} else {
// 库存不足或不存在
return null;
}
} catch (Exception e) {
log.severe("创建订单失败: " + e.getMessage());
return null;
}
}
/**
* 使用完整Lua脚本实现库存检查、扣减和订单创建的原子操作
*/
public String createOrderWithFullLua(String productId, String userId, int quantity) {
try (Jedis jedis = jedisPool.getResource()) {
String orderId = UUID.randomUUID().toString();
String stockKey = "product:" + productId + ":stock";
String orderKey = "order:" + orderId;
// 完整Lua脚本:检查库存、扣减和创建订单
String script =
"local stock = redis.call('get', KEYS[1]) " +
"if not stock then return 0 end " + // 库存键不存在时返回失败
"if tonumber(stock) >= tonumber(ARGV[1]) then " +
" redis.call('decrby', KEYS[1], ARGV[1]) " +
" redis.call('hset', KEYS[2], '" + ORDER_USER_ID + "', ARGV[2], '" +
ORDER_PRODUCT_ID + "', ARGV[3], '" + ORDER_QUANTITY + "', ARGV[1], '" +
ORDER_STATUS + "', 'created') " +
" return 1 " +
"else " +
" return 0 " +
"end";
Object result = jedis.eval(script, 2, stockKey, orderKey,
String.valueOf(quantity), userId, productId);
return Long.valueOf(1).equals(result) ? orderId : null;
} catch (Exception e) {
log.severe("创建订单失败: " + e.getMessage());
return null;
}
}
/**
* 初始化商品库存
*/
public void initializeProductStock(String productId, int stock) {
try (Jedis jedis = jedisPool.getResource()) {
String stockKey = "product:" + productId + ":stock";
jedis.set(stockKey, String.valueOf(stock));
}
}
/**
* 获取商品当前库存
*/
public int getProductStock(String productId) {
try (Jedis jedis = jedisPool.getResource()) {
String stockKey = "product:" + productId + ":stock";
String stockStr = jedis.get(stockKey);
return stockStr != null ? Integer.parseInt(stockStr) : -1;
}
}
/**
* 连接池状态监控
*/
public void monitorPoolStatus() {
log.info("Redis连接池状态 - " +
"活跃连接: " + jedisPool.getNumActive() +
" 空闲连接: " + jedisPool.getNumIdle() +
" 等待线程: " + jedisPool.getNumWaiters());
}
/**
* 使用示例
*/
public static void main(String[] args) {
RedisTransactionDemo demo = new RedisTransactionDemo();
// 初始化商品库存
String productId = "p001";
demo.initializeProductStock(productId, 10);
// 创建订单
String userId = "user123";
String orderId = demo.createOrder(productId, userId, 2);
if (orderId != null) {
System.out.println("订单创建成功:" + orderId);
System.out.println("当前库存:" + demo.getProductStock(productId));
} else {
System.out.println("订单创建失败");
}
}
}
Redis 事务的核心流程
在改进后的实现中,核心流程如下:
Redis 事务的错误处理机制
Redis 事务中的错误可以分为两大类:
1. 语法错误(编译时错误)
在 EXEC 命令执行前发现的错误,例如命令格式错误或命令不存在。这类错误会导致整个事务被拒绝执行。
// 示例:语法错误演示
Transaction transaction = jedis.multi();
transaction.set("key", "value"); // 正确命令
transaction.invalidCommand("key"); // 错误命令(不存在的命令)
List<Object> results = transaction.exec(); // 返回错误,整个事务失败
2. 运行时错误
在 EXEC 命令执行过程中发现的错误,例如对错误类型的键执行操作。这类错误不会导致事务回滚,其他命令会继续执行。
// 示例:运行时错误演示
Transaction transaction = jedis.multi();
transaction.set("key", "value"); // 正确命令
transaction.lpop("key"); // 错误命令(字符串类型不能执行lpop)
transaction.get("key"); // 正确命令
List<Object> results = transaction.exec();
// results中第一个命令成功,第二个报错,第三个继续执行
为了帮助理解,我们可以把 Redis 事务想象成一个订单处理流程:
- MULTI 就像是开启了一个购物车,你可以往里面放商品(命令)
- WATCH 就像是商品价格监控,如果价格变了就取消购买
- EXEC 就像是一次性结算所有商品,结算过程不会因为某个商品有问题而中断
- DISCARD 就像是清空购物车不买了
不同实现方式的性能对比
| 操作类型 | 网络往返次数 | 单次操作耗时 | 1000 次操作耗时 | 并发支持 | 适用场景 |
|---|---|---|---|---|---|
| 非事务操作 | 多次 | ~0.8ms | ~800ms | 高(无锁开销) | 单一操作,无需原子性 |
| 事务操作(WATCH) | 3 次以上 | ~1.5ms | ~1500ms | 中(乐观锁重试) | 多操作需要原子性,并发较低 |
| Lua 脚本操作 | 1 次 | ~1.2ms | ~1200ms | 高(单命令执行) | 多操作需要原子性,并发较高 |
| 流水线操作(Pipeline) | 1 次 | ~1.0ms | ~1000ms | 高(无锁无原子性) | 多操作批量执行,无需原子性 |
举个例子说明这些方式的区别:
想象你在一家快餐店点餐,四种方式分别相当于:
- 非事务操作:单独点每个菜品,每点一个都要走一次流程
- 事务操作:先锁定你看中的菜品,确认没人拿走后一次性下单
- Lua 脚本:直接告诉服务员你想要的全部菜品组合,一次完成
- 流水线:把所有菜品写在一张纸上交给服务员,但不保证全部能做好
并发场景下的 Redis 事务
在高并发环境下,多个客户端可能同时操作同一个键。以下序列图展示了两个客户端并发修改库存的情况:
事务与流水线(Pipeline)的区别
Redis 提供了两种批量执行命令的机制:事务和流水线,它们有着本质的区别:
// 事务示例
try (Jedis jedis = jedisPool.getResource()) {
jedis.watch(stockKey);
Transaction transaction = jedis.multi();
transaction.decrBy(stockKey, quantity);
transaction.hset(orderKey, ...);
List<Object> results = transaction.exec(); // 原子执行所有命令
}
// 流水线示例
try (Jedis jedis = jedisPool.getResource()) {
Pipeline pipeline = jedis.pipelined();
pipeline.decrBy(stockKey, quantity);
pipeline.hset(orderKey, ...);
List<Object> results = pipeline.syncAndReturnAll(); // 批量发送命令,非原子
}
主要区别:
- 事务:保证命令原子性执行,支持 WATCH 机制,可检测并发修改
- 流水线:仅批量发送命令减少网络往返,不保证原子性,无并发控制,性能更高
单元测试示例
对 Redis 事务代码进行单元测试可以使用 Mockito 模拟 Jedis 行为:
@RunWith(MockitoJUnitRunner.class)
public class RedisTransactionDemoTest {
@Mock
private JedisPool jedisPool;
@Mock
private Jedis jedis;
@Mock
private Transaction transaction;
@InjectMocks
private RedisTransactionDemo demo;
@Before
public void setup() {
// 设置基本的Mock行为
when(jedisPool.getResource()).thenReturn(jedis);
}
@Test
public void testCreateOrder_success() {
// 模拟连接池返回Jedis实例
when(jedis.watch(anyString())).thenReturn("OK");
when(jedis.get(anyString())).thenReturn("10");
when(jedis.multi()).thenReturn(transaction);
when(transaction.decrBy(anyString(), anyLong())).thenReturn(transaction);
when(transaction.hset(anyString(), anyString(), anyString())).thenReturn(transaction);
when(transaction.exec()).thenReturn(Arrays.asList(9L, "OK", "OK", "OK"));
String orderId = demo.createOrder("p001", "user123", 1);
assertNotNull(orderId);
verify(jedis, times(1)).close();
}
@Test
public void testCreateOrder_retry() {
// 模拟事务失败需要重试
when(jedis.watch(anyString())).thenReturn("OK");
when(jedis.get(anyString())).thenReturn("10");
when(jedis.multi()).thenReturn(transaction);
// 第一次事务失败,第二次成功
when(transaction.exec())
.thenReturn(null)
.thenReturn(Arrays.asList(9L, "OK", "OK", "OK"));
String orderId = demo.createOrder("p001", "user123", 1);
assertNotNull(orderId);
// 验证watch被调用了两次
verify(jedis, times(2)).watch(anyString());
}
}
分布式事务集成方案
当应用需要跨多个 Redis 实例或与其他数据源协同时,可以考虑与分布式事务框架结合:
// 简化的Seata与Redis集成示例
@GlobalTransactional
public String createOrderWithSeata(String productId, String userId, int quantity) {
// 1. 扣减Redis库存
boolean stockSuccess = redisTemplate.execute(redisScript, keys, args);
if (!stockSuccess) {
return null;
}
// 2. 创建订单(数据库操作)
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setQuantity(quantity);
orderRepository.save(order);
// 3. 其他微服务操作(如支付服务)
paymentService.createPayment(order.getId(), calculateAmount(productId, quantity));
return order.getId();
}
生产环境配置示例
在生产环境中使用 Redis 事务,可以参考以下配置:
# Redis连接池配置
redis.pool.max-total=200 # 最大连接数,根据峰值QPS设置
redis.pool.max-idle=50 # 最大空闲连接,避免频繁创建连接
redis.pool.min-idle=10 # 最小空闲连接,保证冷启动性能
redis.pool.max-wait=3000 # 连接获取最大等待时间(ms)
redis.pool.test-on-borrow=true # 获取连接时测试
redis.pool.test-while-idle=true # 空闲时测试连接有效性
redis.pool.time-between-eviction-runs=60000 # 空闲连接检测周期(ms)
# 事务重试配置
redis.transaction.max-retry=5 # 最大重试次数
redis.transaction.initial-delay=100 # 初始延迟(ms)
redis.transaction.max-delay=1000 # 最大延迟(ms)
redis.transaction.delay-multiplier=2 # 延迟倍数
生产环境监控要点
在生产环境中使用 Redis 事务时,建议监控以下指标:
- 事务失败率:
事务失败率 = 失败事务数 / 总事务数 * 100%
当失败率超过 5%时,需要排查是否存在热点数据竞争
-
重试分布:记录事务重试次数分布,查看是否集中在特定业务
-
连接池状态:监控连接池使用率、等待时间,及时调整连接池大小
-
慢事务:监控执行时间超过预期的事务,优化事务内命令
总结
| 特性 | 说明 |
|---|---|
| 事务支持 | Redis 支持事务,但不支持回滚 |
| 核心命令 | MULTI、EXEC、DISCARD、WATCH |
| 原子性 | 命令会一次性、顺序性、排他性地执行 |
| 并发控制 | 通过 WATCH 命令实现乐观锁机制 |
| 错误处理 | 区分语法错误(事务拒绝)和运行时错误(继续执行) |
| Java 实现 | 使用连接池、指数退避重试、Lua 脚本优化 |
| 性能对比 | Lua 脚本 > 流水线 > 事务(WATCH) > 单命令 |
| 最佳实践 | 监控事务失败率、合理设置重试、选择合适实现方式 |