Redis事务和锁机制
# Redis事务和锁机制
# 1.Redis事务
# 1.1 基本概念
Redis事务是一个单独的隔离操作:事务中的所有命令都会被序列化、按顺序地执行。事务在执行的过程中,不会被其他的客户端发来的命令请求所打断。Redis事务主要作用就是串联多个命令防止别的命令插队。
# 1.2 三个命令
Redis事务是一种将多个命令打包执行的机制,它可以确保这些命令在执行期间不会被其他客户端的命令中断。Redis的事务通过MULTI、EXEC、DISCARD和WATCH命令来实现。
MULTI命令
:这是一个事务的开启命令。在调用MULTI命令之后,所有后续的命令将会被放入一个队列中,而不会立即执行。执行事务
:在调用MULTI命令之后,可以依次调用多个命令,这些命令都会被放入事务队列中,但不会立即执行。一旦执行了EXEC命令,Redis就会按照顺序执行队列中的所有命令。DISCARD命令
:如果在MULTI命令和EXEC命令之间需要放弃当前事务,可以使用DISCARD命令。它会清空事务队列,取消事务。WATCH命令
:在执行事务过程中,可以使用WATCH命令来监视一个或多个键。如果被监视的键在执行EXEC命令之前被其他客户端修改,事务就会被中断。
在执行EXEC命令时,Redis会依次执行事务队列中的所有命令,并且不会中断执行过程。Redis的事务是原子性的,即要么全部执行,要么全部失败。如果事务中的某个命令执行出错,仍然会继续执行事务中的其他命令,但不会回滚已执行的命令。
1.开启事务,加入组队命令,并执行提交事务:
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set number1 10
QUEUED
127.0.0.1:6379(TX)> set number2 12
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK
2) OK
127.0.0.1:6379> mget number1 number2
1) "10"
2) "12"
2.开启事务,加入组队命令,进行中断事务:
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set key3 value3
QUEUED
127.0.0.1:6379(TX)> set key4 value4
QUEUED
127.0.0.1:6379(TX)> DISCARD
OK
127.0.0.1:6379> get key3
(nil)
注意:Redis的事务并不支持回滚操作,也不会对事务中的命令进行隔离,它只是将命令打包执行,保证在执行期间不会被其他客户端的命令中断。
3.演示提交事务过程中,出现异常,导致不会回滚的情况:
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set b1 10
QUEUED
127.0.0.1:6379(TX)> set b2
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379(TX)> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379>
# 如果组队成功,但是队列有错误,单条记录会报错,其他则执行成功。
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set c1 c1
QUEUED
127.0.0.1:6379(TX)> INCR c1
QUEUED
127.0.0.1:6379(TX)> set c2 c2
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
# 2.锁的机制
想象这样一个场景:在我的账户里有 1000 块钱,此时我的 朋友1 和 朋友2 要同时从我的账户中取 600 块钱,由于 朋友1 取钱的时候看到的是 1000 元,朋友2 取钱的时候看到的也是 1000 元,因此账户中同时扣款中最后就只剩 -200 元,但实际上我们不允许账户出现负数情况,此时就出现了事务冲突。
# 2.1 悲观锁
悲观锁是一种并发控制机制,用于在多个线程或进程同时访问共享资源时保证数据的一致性和完整性。
悲观锁的核心思想是,对共享资源的访问持保守态度,假设会发生冲突,并采取相应的措施来阻止其他线程对资源的访问,直到当前线程完成操作。
悲观锁的实现通常使用互斥锁(Mutex)
或读写锁(ReadWriteLock)
。以下是悲观锁的一些常见特性和使用场景:
互斥锁(Mutex)
:互斥锁是最常见的悲观锁机制。每次访问共享资源之前,线程会先尝试获取互斥锁,如果锁已经被其他线程获取,则当前线程会被阻塞,直到锁被释放。这种机制可用于保护临界区代码,确保同一时间只有一个线程在执行。读写锁(ReadWriteLock)
:读写锁是悲观锁的一种变体,允许多个线程同时读取共享资源,但在进行写操作时需要独占访问。这种机制适用于读多写少的场景,可以提高并发性能。数据库中的悲观锁
:在数据库中,悲观锁也被广泛应用。通过在事务中使用SELECT...FOR UPDATE语句,可以获取指定行的排他锁,防止其他事务对相同行进行修改。
使用悲观锁的主要优点是可以确保数据的一致性和完整性,但缺点是会增加系统的开销和降低并发性能。一旦锁持有时间过长或存在大量锁冲突,可能会导致程序性能下降或出现死锁情况。
因此,在设计并发系统时,需要根据业务需求和性能要求选择合适的锁机制,悲观锁仅是其中一种选择。
简答的说就是每次业务执行之前,事务开始的时候,先上锁。当前业务执行完毕或者提交事务之后才会解锁,后面的才能继续操作。
# 2.2 乐观锁
乐观锁是一种并发控制机制,它假设在多个线程或进程同时访问共享资源时,冲突的概率较低,因此不会立即阻塞其他线程或进程的访问,而是在更新共享资源时进行冲突检测和处理。
乐观锁通常基于版本号或时间戳等机制来实现。
乐观锁的核心思想是,每次访问共享资源时都假设不会发生冲突,继续进行操作,并在操作完成后进行冲突检测。如果在冲突检测时发现其他线程已经修改了共享资源,当前线程需要重新获取最新的资源版本并重新执行操作,直至操作成功或达到一定重试次数的限制。
以下是乐观锁的一些常见特性和使用场景:
版本号控制
:乐观锁通常使用版本号或时间戳的方式来标识共享资源的版本。在每次更新资源时,版本号会自增或更新为最新的时间戳。在进行冲突检测时,将检查当前操作的版本号是否匹配最新的版本号,如果不匹配,则说明有冲突发生,需要重新执行操作。无阻塞操作
:乐观锁不会立即阻塞其他线程或进程的访问,而是相信冲突的概率较低,继续进行操作。只有在冲突检测时才会引发冲突处理机制,例如重新获取资源的最新版本并重试操作。适用于读多写少的场景
:乐观锁适用于读多写少的并发场景,因为在读操作期间没有加锁,允许其他线程并发读取共享资源。只有在写操作时需要进行冲突检测和处理,写操作较少的情况下,不会对并发读取产生明显的性能影响。数据库中的乐观锁
:在数据库中,乐观锁通常使用行版本号或时间戳字段来实现。当执行更新操作时,会检查当前更新的记录版本与数据库中的版本是否一致,如果一致则更新成功,否则认为发生了冲突,需要进行处理。
使用乐观锁的主要优点是可以提高并发性能,减少阻塞并发访问。但缺点是需要处理冲突的机制,在高并发场景下可能会引发较多的重试操作,并增加系统开销。
在实际应用中,乐观锁常常与悲观锁结合使用,根据具体场景选择合适的并发控制机制来保证数据的一致性和完整性。
Redis就是使用这个check-and-set的机制实现事务的。
简单的使用两个线程或者两个终端,演示一下Redis的锁的机制,这里使用WATCH命令来监视balance的变化:
# 线程一
127.0.0.1:6379> WATCH balance
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set balance 10
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK
127.0.0.1:6379>
# 线程二
127.0.0.1:6379> WATCH balance
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set balance 20
QUEUED
127.0.0.1:6379(TX)> EXEC
(nil) # 可以看到这里的balance是空的
127.0.0.1:6379>
# 3.Redis的事务三特性
单独的隔离操作:
- 事务中所有的命令都会被序列化、按顺序地执行。事务在执行的过程中,不会被其他的客户端发送来的命令请求打断。
没有隔离级别的概念:
- 队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行。
不保证原子性:
- 事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。