MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Redis事务管理:MULTI、EXEC与DISCARD命令

2022-06-233.5k 阅读

Redis事务概述

在数据库管理中,事务是一组操作的集合,这些操作要么全部成功执行,要么全部不执行,以此保证数据的一致性和完整性。Redis作为一个高性能的键值对数据库,也提供了事务管理功能,通过 MULTIEXECDISCARD 命令来实现。

Redis 的事务和传统关系型数据库中的事务在实现机制和特性上有一些差异。传统关系型数据库通常使用锁机制来保证事务的隔离性,而 Redis 的事务主要是通过命令队列和原子性执行来实现。

MULTI 命令

MULTI 命令用于开启一个事务块。当客户端发送 MULTI 命令后,后续发送的命令不会立即执行,而是被放入一个队列中。

语法

MULTI

代码示例(Python 与 Redis-py)

import redis

r = redis.Redis(host='localhost', port=6379, db=0)

# 开启事务
pipe = r.pipeline()
pipe.multi()

# 命令入队
pipe.set('key1', 'value1')
pipe.get('key1')

# 执行事务
results = pipe.execute()
print(results)

在上述 Python 代码中,通过 redis - py 库,我们首先创建了一个 Redis 连接对象 r。然后使用 pipeline() 方法创建一个管道对象 pipe,并通过 pipe.multi() 开启事务。接着将 setget 命令放入事务队列,最后通过 pipe.execute() 执行事务。

工作原理

当 Redis 服务器接收到 MULTI 命令时,它会将客户端的状态切换为事务状态。在这个状态下,服务器会将后续接收到的命令放入一个队列中,而不是立即执行。这个队列会一直增长,直到接收到 EXECDISCARD 命令。

EXEC 命令

EXEC 命令用于执行在 MULTI 命令之后入队的所有命令。一旦执行 EXEC,Redis 会按照命令入队的顺序依次执行这些命令,并且这些命令的执行是原子性的,即要么所有命令都成功执行,要么因为某个命令执行失败而导致整个事务回滚(在 Redis 2.6.5 之前,如果事务队列中的某个命令执行失败,已执行的命令不会回滚;2.6.5 之后,会忽略执行失败的命令,继续执行其他命令)。

语法

EXEC

代码示例(Python 与 Redis-py 延续上文)

在上面开启事务并将命令入队的代码基础上,pipe.execute() 实际上就相当于在 Redis 中执行 EXEC 命令。当调用 execute() 时,之前入队的 setget 命令会被原子性地执行。

工作原理

当 Redis 接收到 EXEC 命令时,它会遍历事务队列,依次执行队列中的每个命令。在执行过程中,Redis 会保证这些命令的执行不会被其他客户端的命令打断。如果在执行过程中某个命令出现错误(例如命令格式错误、类型错误等),在 Redis 2.6.5 之前,整个事务会停止执行,并且已执行的命令不会回滚;在 2.6.5 及之后的版本,会忽略该错误命令,继续执行后续命令。

DISCARD 命令

DISCARD 命令用于取消一个事务块。当执行 DISCARD 时,Redis 会清空事务队列,并将客户端的状态从事务状态切换回正常状态。

语法

DISCARD

代码示例(Python 与 Redis-py)

import redis

r = redis.Redis(host='localhost', port=6379, db=0)

pipe = r.pipeline()
pipe.multi()

pipe.set('key2', 'value2')
pipe.get('key2')

# 取消事务
pipe.discard()

# 此时事务已取消,下面尝试执行事务中的命令会报错
try:
    results = pipe.execute()
except redis.exceptions.InvalidStateError as e:
    print(f"执行事务失败: {e}")

在上述代码中,我们先开启事务并将 setget 命令入队,然后调用 pipe.discard() 取消事务。此时如果尝试执行 pipe.execute(),会抛出 InvalidStateError 异常,因为事务已经被取消。

工作原理

当 Redis 接收到 DISCARD 命令时,它会释放事务队列占用的内存空间,并将客户端状态切换回正常状态。在正常状态下,客户端可以像往常一样发送单个命令,而不是事务相关的命令。

事务中的错误处理

在 Redis 事务中,错误主要分为两种类型:入队错误和执行错误。

入队错误

入队错误是指在 MULTI 命令之后,EXEC 命令之前,客户端发送的命令格式不正确或不符合当前数据类型要求。例如,向一个已经是字符串类型的键执行 LPUSH 命令(LPUSH 用于列表类型)。

当发生入队错误时,Redis 会记住这个错误,并在客户端执行 EXEC 命令时,整个事务不会执行,而是返回一个错误信息。

代码示例(Python 与 Redis-py)

import redis

r = redis.Redis(host='localhost', port=6379, db=0)

pipe = r.pipeline()
pipe.multi()

# 这里故意设置一个错误命令,向字符串类型键执行 LPUSH 操作
pipe.set('string_key', 'test')
pipe.lpush('string_key', 'element')

try:
    results = pipe.execute()
except redis.exceptions.ResponseError as e:
    print(f"执行事务失败: {e}")

在上述代码中,我们先设置了一个字符串类型的键 string_key,然后尝试对其执行 lpush 操作,这会导致入队错误。当执行 pipe.execute() 时,会捕获到 ResponseError 异常,并打印出错误信息。

执行错误

执行错误是指在 EXEC 命令执行过程中,某个命令本身的执行出现错误,例如对一个不存在的键执行 GET 操作(这在 Redis 中是合法操作,返回 None,但假设是其他不合法操作)。

在 Redis 2.6.5 之前,如果事务队列中的某个命令执行错误,整个事务会停止执行,并且已执行的命令不会回滚。在 2.6.5 及之后的版本,Redis 会忽略执行错误的命令,继续执行后续命令。

代码示例(Python 与 Redis-py)

import redis

r = redis.Redis(host='localhost', port=6379, db=0)

pipe = r.pipeline()
pipe.multi()

# 这里假设一个执行错误的命令,对不存在的键执行 INCRBY 操作(假设 INCRBY 对不存在键报错)
pipe.incrby('non_existent_key', 1)
pipe.set('new_key', 'new_value')

results = pipe.execute()
print(results)

在上述代码中,如果假设 INCRBY 对不存在的键会报错(实际 Redis 中 INCRBY 对不存在键会先初始化为 0 再操作),那么在 2.6.5 之前,INCRBY 报错后事务停止,SET 命令不会执行;在 2.6.5 及之后,INCRBY 报错被忽略,SET 命令会继续执行。

事务与一致性

Redis 的事务机制通过原子性执行命令队列来保证数据的一致性。在事务执行过程中,其他客户端无法干扰这些命令的执行顺序和结果。

例如,在一个电商应用中,可能需要在 Redis 中同时更新商品库存和用户订单信息。通过事务,可以确保库存减少和订单创建这两个操作要么都成功,要么都失败,从而保证数据的一致性。

事务与并发控制

虽然 Redis 的事务提供了一定程度的原子性,但它并没有像传统关系型数据库那样复杂的并发控制机制。

在高并发场景下,如果多个客户端同时对相同的键进行操作,可能会出现竞争条件。为了解决这个问题,Redis 提供了 WATCH 命令,它可以用于监控一个或多个键,当事务执行时,如果被监控的键在 WATCH 之后、EXEC 之前被其他客户端修改,那么当前事务会被取消,EXEC 命令返回 nil

语法

WATCH key [key...]

代码示例(Python 与 Redis-py)

import redis

r = redis.Redis(host='localhost', port=6379, db=0)

# 监控键
r.watch('shared_key')

# 获取键的值
value = r.get('shared_key')
if value is not None:
    new_value = int(value) + 1

    pipe = r.pipeline()
    pipe.multi()

    pipe.set('shared_key', new_value)
    results = pipe.execute()
    if results is None:
        print("事务被取消,键已被其他客户端修改")
    else:
        print("事务执行成功")
else:
    print("键不存在")

# 取消监控
r.unwatch()

在上述代码中,我们首先使用 r.watch('shared_key') 监控 shared_key。然后获取键的值并进行计算,接着开启事务并设置新的值。如果在事务执行前 shared_key 被其他客户端修改,pipe.execute() 会返回 None,表示事务被取消。最后通过 r.unwatch() 取消监控。

事务性能考量

由于 Redis 的事务是通过命令队列实现的,在事务执行时,会依次执行队列中的命令。如果事务队列中的命令数量过多,可能会导致 Redis 服务器在执行事务时的响应时间变长。

为了提高性能,可以尽量减少事务队列中的命令数量,或者将一些可以独立执行的命令拆分出来单独执行。同时,在高并发场景下,合理使用 WATCH 命令可以避免不必要的事务重试,提高系统的整体性能。

应用场景

  1. 电商库存与订单管理:在电商系统中,更新商品库存和创建订单这两个操作需要保证一致性。可以通过 Redis 事务将减少库存和创建订单的命令放入同一个事务中,确保要么两者都成功,要么都失败。
  2. 银行转账:在银行转账操作中,从一个账户扣除金额并向另一个账户增加金额这两个操作需要原子性执行。可以使用 Redis 事务来保证数据的一致性。
  3. 缓存更新:在使用 Redis 作为缓存时,可能需要同时更新多个相关的缓存键值对。通过事务可以确保这些更新操作要么都成功,要么都失败,避免出现部分更新的情况。

事务与持久化

Redis 支持两种持久化方式:RDB(Redis Database)和 AOF(Append - Only File)。在事务执行过程中,持久化机制会正常工作。

对于 RDB 持久化,它是通过定期快照的方式将内存数据保存到磁盘。如果在事务执行过程中进行快照,事务中的数据修改会被包含在快照中。

对于 AOF 持久化,它是通过追加写的方式将命令记录到日志文件。在事务执行时,MULTIEXEC 以及事务队列中的命令都会被记录到 AOF 文件中,以保证数据的一致性和可恢复性。

总结 Redis 事务特性

  1. 原子性:事务中的命令要么全部执行,要么全部不执行(在 Redis 2.6.5 之后,执行错误的命令会被忽略,不影响其他命令执行)。
  2. 一致性:通过原子性执行保证数据的一致性,在事务执行期间,其他客户端无法干扰。
  3. 隔离性:Redis 的事务通过命令队列实现一定程度的隔离性,事务内的命令按顺序执行,不受其他客户端命令的影响。
  4. 持久性:结合 RDB 和 AOF 持久化机制,事务中的数据修改可以被持久化到磁盘,保证数据的可靠性。

通过深入理解 Redis 的 MULTIEXECDISCARD 命令,以及事务中的错误处理、并发控制和性能考量等方面,开发人员可以更好地利用 Redis 的事务功能,构建出高性能、高可靠的应用程序。无论是在缓存管理、电商系统还是金融应用等领域,Redis 事务都能发挥重要作用,确保数据的一致性和完整性。同时,合理使用事务与其他 Redis 特性相结合,可以进一步提升系统的性能和可扩展性。在实际应用中,需要根据具体的业务需求和场景,灵活运用 Redis 事务,以达到最佳的应用效果。