Redis类似大多数成熟的数据库系统一样,提供了事务机制。Redis的事务机制非常简单,它没有严格的事务模型,无法像关系型数据库一样保证操作的原子性。
Redis事务最大的作用是保证多个指令的串行执行,它可以借助于Redis单线程读写的特性,保证Redis事务中的指令不会被事务外的指令打搅,不过要注意它不是原子性的。
Redis事务可以一次执行多个命令(允许在一次单独的步骤中执行一组命令),并且带有以下两个重要的保证。
完整事务案例:
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set number 1
QUEUED
127.0.0.1:6379> incr number
QUEUED
127.0.0.1:6379> exec
1) OK
2) (integer) 2
127.0.0.1:6379>
multi开启一个事务之后,所有指令都不执行,而是缓存到事务队列中,直到服务器接收到exec指令,才开始执行整个事务中的指令。事务全部指令执行完毕后,一次性返回全部的结果。
一个事务从开始到执行会经历以下三个阶段
使用Redis事务,一个最需要注意的问题是,指令多,网络开销高;因此我们一定要结合管道pipeline一起使用,这样可以将多次网络io操作压缩成单次。
Redis事务相关的指令有五个,分别是MULTI、EXEC、DISCARD、WATCH、UNWATCH
指令 | 指令作用 | 返回值 |
---|---|---|
MULTI | 标记一个事务块的开始 | 总是返回 OK |
EXEC | 执行所有事务块内的命令 | 事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil |
DISCARD | 取消事务,放弃执行事务块内的所有命令,如果正在使用 WATCH 命令监视某个(或某些) key,那么取消所有监视,等同于执行命令 UNWATCH | 总是返回 OK |
WATCH | 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断 | 总是返回 OK |
UNWATCH | 取消 WATCH 命令对所有 key 的监视。如果在执行WATCH 命令之后, EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了。因为 EXEC 命令会执行事务,因此 WATCH 命令的效果已经产生了;而 DISCARD 命令在取消事务的同时也会取消所有对 key 的监视,因此这两个命令执行之后,就没有必要执行 UNWATCH 了 | 总是返回 OK |
MULTI用于标记一个事务的开始,事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由 EXEC 命令原子性(atomic)地执行。MULTI指令总是返回OK。
127.0.0.1:6379> multi
OK
EXEC用于执行所有事务块内的命令,假如某个(或某些) key 正处于 WATCH 命令的监视之下,且事务块中有和这个(或这些) key 相关的命令,那么 EXEC 命令只在这个(或这些) key 没有被其他命令所改动的情况下执行并生效,否则该事务被打断(abort)。
示例:转账功能:A向B账号转账50元
127.0.0.1:6379> set acount:a 100
OK
127.0.0.1:6379> set acount:b 120
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> get acount:a
QUEUED
127.0.0.1:6379> get acount:b
QUEUED
127.0.0.1:6379> decrby acount:a 50
QUEUED
127.0.0.1:6379> incrby acount:b 50
QUEUED
127.0.0.1:6379> exec
1) "100"
2) "120"
3) (integer) 50
4) (integer) 170
DISCARD用于取消事务,放弃执行事务块内的所有命令。如果正在使用 WATCH 命令监视某个(或某些) key,那么取消所有监视,等同于执行命令 UNWATCH 。DISCARD指令总是返回OK。
127.0.0.1:6379> set count 1
OK
127.0.0.1:6379> watch count
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr count
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> get count
"1"
WATCH用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。这个实现方式也很简单,WATCH是在事务之间发送的指令,Redis服务在接收到指令时,会记录下该key对应的值,当Redis服务接收到EXEC指令,需要执行事务时,Redis服务首先会检查WATCH的key的值,从WATCH之后是否发生改变即可。
127.0.0.1:6379> watch count
OK
127.0.0.1:6379> incr count
(integer) 2
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr count
QUEUED
127.0.0.1:6379> exec
(nil)
注意禁止在MULTI和EXEC之间执行WATCH指令,这会导致Redis服务响应异常
UNWATCH用于取消WATCH命令对所有key的监视。如果在执行 WATCH 命令之后, EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了。因为 EXEC 命令会执行事务,因此 WATCH 命令的效果已经产生了;而 DISCARD 命令在取消事务的同时也会取消所有对 key 的监视,因此这两个命令执行之后,就没有必要执行 UNWATCH 了。
如果执行的某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会被执行,不会回滚。
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set a hello
QUEUED
127.0.0.1:6379> incr a
QUEUED
127.0.0.1:6379> set b word
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
127.0.0.1:6379> keys *
1) "b"
2) "a"
127.0.0.1:6379> get a
"hello"
127.0.0.1:6379>
队列中的某个命令出现了报告错误,执行时整个的所有队列都会被取消。
(empty list or set)
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set a 111
QUEUED
127.0.0.1:6379> setadfef dfs
(error) ERR unknown command 'setadfef'
127.0.0.1:6379> set b 22
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379>
通过模拟一个简单的余额增加的例子,使用Jedis客户端来使用Redis的事务。
package com.lizba.redis.tx;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import java.math.BigDecimal;
import java.util.List;
/**
*
* Redis事务demo
*
*
* @Author: Liziba
* @Date: 2021/9/9 23:53
*/
public class TransactionDemo {
private Jedis client;
public TransactionDemo(Jedis client) {
this.client = client;
}
/**
* 添加余额
*
* @param userId 用户id
* @param amt 添加余额
* @return
*/
public BigDecimal addBalance(String userId, BigDecimal amt) {
String key = this.keyFormat(userId);
// 初始用户余额为0
client.setnx(key, "0");
while (true) {
client.watch(key);
BigDecimal balance = new BigDecimal(client.get(key)).setScale(2, BigDecimal.ROUND_HALF_UP);
BigDecimal amount = balance.add(amt);
Transaction tx = client.multi();
tx.set(key, amount.toPlainString());
List<Object> exec = tx.exec();
// 返回值不为空则证明Redis事务成功
if (exec != null) {
break;
}
}
return new BigDecimal(client.get(key)).setScale(2, BigDecimal.ROUND_HALF_UP);
}
/**
* 获取总金额
*
* @param userId 用户id
* @return
*/
public BigDecimal getAmount(String userId) {
String amt = client.get(keyFormat(userId));
return new BigDecimal(amt);
}
/**
* Redis key
* @param userId 用户id
* @return
*/
private String keyFormat(String userId) {
return String.format("balance:%s",userId);
}
}
测试代码:
package com.lizba.redis.tx;
import redis.clients.jedis.Jedis;
import java.math.BigDecimal;
import java.util.concurrent.CountDownLatch;
/**
*
* 测试Redis事务
*
*
* @Author: Liziba
* @Date: 2021/9/10 0:03
*/
public class TestTransactionDemo {
private static CountDownLatch count = new CountDownLatch(100);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
Jedis client = new Jedis("192.168.211.109", 6379);
TransactionDemo demo = new TransactionDemo(client);
demo.addBalance("liziba", BigDecimal.TEN);
client.close();
count.countDown();
}).start();
}
count.await();
Jedis client = new Jedis("192.168.211.109", 6379);
BigDecimal amt = new TransactionDemo(client).getAmount("liziba");
System.out.println(amt.toPlainString());
}
}
测试结果:
预期1000,结果1000