Redis事务详述

本文详细介绍了Redis事务的原理、指令以及在Jedis中的应用,包括MULTI(开启事务)、EXEC(执行事务)、DISCARD(取消事务)和WATCH(监视键)。强调了事务的非原子性和使用注意事项,如网络开销和事务中断情况。
摘要由CSDN通过智能技术生成

一、简介

  • 是什么:Redis事务可以一次执行多个命令,本质是一组命令的组合,一个事务中的所有命令都会序列化,按顺序地串行化执行,执行中不会被其他命令插入,不许加塞。
  • 能干嘛:一个队列中,一次性,顺序性,排他性的执行一系列命令。

Redis类似大多数成熟的数据库系统一样,提供了事务机制。Redis的事务机制非常简单,它没有严格的事务模型,无法像关系型数据库一样保证操作的原子性。

Redis事务最大的作用是保证多个指令的串行执行,它可以借助于Redis单线程读写的特性,保证Redis事务中的指令不会被事务外的指令打搅,不过要注意它不是原子性的。

  • 单独的隔离操作:事务中的所有命令都会序列化,按顺序地串行化执行,事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  • 没有隔离级别的概念:队列中的命令没有提交之前都不会实际的被执行,因为事务提交前任何指令都不会被实际执行,也就不存在“事务内的查询要看到事务里的更新,在事务外查询不能看到”这个让人万分头痛的问题。
  • 不保证原子性:redis同一个事务中如果有一条命令执行失败,其他的命令仍然会执行,没有回滚。

Redis事务可以一次执行多个命令(允许在一次单独的步骤中执行一组命令),并且带有以下两个重要的保证。

  • 批量操作在发送EXEC命令之前被放入队列缓存。
  • 收到EXEC命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行
  • 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列之中

完整事务案例:

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指令,才开始执行整个事务中的指令。事务全部指令执行完毕后,一次性返回全部的结果。

一个事务从开始到执行会经历以下三个阶段

  1. 开启:以MULTI开始一个事务。
  2. 入队:将多个命令入队到事务中,接到这些命令并不会立即执行,而是放到等待执行的事务队列里面。
  3. 执行:由EXEC命令触发事务。

在这里插入图片描述

使用Redis事务,一个最需要注意的问题是,指令多,网络开销高;因此我们一定要结合管道pipeline一起使用,这样可以将多次网络io操作压缩成单次。

2、指令介绍

2.1 简介

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

2.2 MULTI(开启事务)

MULTI用于标记一个事务的开始,事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由 EXEC 命令原子性(atomic)地执行。MULTI指令总是返回OK。

127.0.0.1:6379> multi
OK

2.3 EXEC(执行事务)

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

在这里插入图片描述

2.4 DISCARD(取消事务)

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"

在这里插入图片描述

2.5 WATCH(监视)

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服务响应异常
在这里插入图片描述

2.6 UNWATCH

UNWATCH用于取消WATCH命令对所有key的监视。如果在执行 WATCH 命令之后, EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了。因为 EXEC 命令会执行事务,因此 WATCH 命令的效果已经产生了;而 DISCARD 命令在取消事务的同时也会取消所有对 key 的监视,因此这两个命令执行之后,就没有必要执行 UNWATCH 了。

在这里插入图片描述

2.7 事务的错误处理

部分执行

如果执行的某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会被执行,不会回滚。

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>

在这里插入图片描述

3、Jedis 使用事务

通过模拟一个简单的余额增加的例子,使用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;
 
/**
 * <p>
 *      Redis事务demo
 * </p>
 *
 * @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;
 
/**
 * <p>
 *      测试Redis事务
 * </p>
 *
 * @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

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值