redis中使用事务

news2024/11/28 23:42:02

事务是指一个执行过程,要么全部执行成功,要么失败什么都不改变。不会存在一部分成功一部分失败的情况,也就是事务的ACID四大特性(原子性、一致性、隔离性、持久性)。但是redis中的事务并不是严格意义上的事务,它只是保证了多个命令执行是按照顺序执行,在执行过程中不会插入其他的命令,并不会保证所有命令都成功。也就是说在命令执行过程中,某些命令的失败不会回滚前面已经执行成功的命令,也不会影响后面命令的执行。
redis中的事务跟pipeline很类似,但pipeline是批量提交命令到服务器,服务器执行命令过程中是一条一条执行的,在执行过程中是可以插入其他的命令。而事务是把这些命令批量提交到服务器,服务器执行命令过程也是一条一条执行的,但是在执行这一批命令时是不能插入执行其他的命令,必须等这一批命令执行完成后才能执行其他的命令。

1、事务基本结构

与数据库事务执行过程类似,redis事务是由multi、exec、discard三个关键字组成,对比数据库事务的begin、commit、rollback三个关键字。
命令行示例如下:

127.0.0.1:6379> set key1 value111
OK
127.0.0.1:6379> set key2 value222
OK
127.0.0.1:6379> mget key1 key2
1) "value111"
2) "value222"
127.0.0.1:6379> 

# 第一个事务
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set key1 value-111
QUEUED
127.0.0.1:6379(TX)> setm key2 value-222
(error) ERR unknown command `setm`, with args beginning with: `key2`, `value-222`, 
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> mget key1 key2
1) "value111"
2) "value222"
127.0.0.1:6379> 

# 第二个事务
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set key1 value-111
QUEUED
127.0.0.1:6379(TX)> set key2 value-222
QUEUED
127.0.0.1:6379(TX)> discard
OK
127.0.0.1:6379> 
127.0.0.1:6379> mget key1 key2
1) "value111"
2) "value222"
127.0.0.1:6379> 

# 第三个事务
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set key1 value-111
QUEUED
127.0.0.1:6379(TX)> set key3 value-333 vddd
QUEUED
127.0.0.1:6379(TX)> set key2 value-222
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR syntax error
3) OK
127.0.0.1:6379> 
127.0.0.1:6379> mget key1 key2 key3
1) "value-111"
2) "value-222"
3) (nil)
127.0.0.1:6379> 

在上面的示例过程中,第一个事务执行时输入了一个错误的命令,在提交事务时整个命令都没有执行;第二个事务提交了多个命令,但是最后回滚了事务,整个事务也不会执行;第三个事务在提交命令时,故意设置一个执行失败的命令,会发现这个失败的命令并不会影响其他命令的成功。

2、事务的执行步骤

redis中的事务是分两步执行的:第一步命令排队,也就是将所有要执行的命令添加到一个队列中,在这一步中命令不会真正执行;第二步命令执行或取消,在这一步中真正处理队列中的命令,如果是exec命令,就执行这些命令;如果是discard命令,就取消执行命令。这里需要注意,如果在排队中某些命令解析出错,即使调用了exec命令,整个队列中的命令也不会执行;但是如果在执行过程中出现了错误,它并不会影响其他命令的正常执行,一般使用封装好的客户端不会出现这种命令错误情况。

3、并发事务

多线程的项目就会有并发问题,并发问题就会存在数据不一致,数据库中解决并发问题是通过锁来实现的,在操作数据前加锁,保证数据在整个执行过程中不被其他程序修改。这种方式加锁是悲观锁,每次更新操作都认为数据会被其他程序修改,导致程序的并发性能不好;还有一种加锁方式是乐观锁,每次直到真正更新数据时才判断数据是否被更新了,如果数据被更新就放弃操作,对于读多写少的场景非常适合,一般实现乐观锁是通过版本号机制。
redis中就支持这种乐观锁机制,它的实现是通过watch命令,在开始执行事务前先通过watch监控被更新的key,如果在事务执行时发现这些key被修改了,那么就不执行事务中的命令。
下面演示的是扣费场景:在进行扣费前,先判断用户的余额,如果余额够扣,就扣减用户的账号余额;如果余额不足,就不能扣减账号余额。

  1. watch某个key后,如果数据没有被其他客户端修改,事务将成功执行:
127.0.0.1:6379> set user:balance:1 100
OK
127.0.0.1:6379> watch user:balance:1
OK
127.0.0.1:6379> get user:balance:1
"100"
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby user:balance:1 -100
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 0
127.0.0.1:6379> get user:balance:1
"0"
127.0.0.1:6379> 
  1. watch某个key后,如果key对应的值被其他程序修改了,执行事务将不成功;如果不用watch命令,事务会成功执行。
    下图展示了在两个客户端验证事务:
    1、首先在下面的客户端设置键的值为100;
    2、然后设置 watch 该值,并且开启事务;
    3、执行减100的命令;
    4、在上面的客户端中修改这个键的值,减3;
    5、下面的客户端执行 exec 命令提交事务。
    这几个步骤执行完成后,发现数据没有修改成功,表示 watch 命令监控到数据变动没有执行事务中的命令。
    watch事务演示
    下面演示步骤与上面一样,只不过在事务前没有 watch 命令,发现数据被修改了。
    没有watch事务演示

  2. watch命令只对当前客户端中的 multi / exec 之间的命令有影响,不在它们之间的命令不受影响,可以正常执行:

127.0.0.1:6379> set user:balance:1 100
OK
127.0.0.1:6379> watch user:balance:1
OK
127.0.0.1:6379> incrby user:balance:1 -3
(integer) 97
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby user:balance:1 -100
QUEUED
127.0.0.1:6379(TX)> set watchkey aaa
QUEUED
127.0.0.1:6379(TX)> exec
(nil)
127.0.0.1:6379> mget user:balance:1 watchkey
1) "97"
2) (nil)
127.0.0.1:6379> 

上面这段代码在watch命令后对键的值进行了修改,发现更新成功;watch的key对应值被修改了,导致事务内的命令不执行,所以后面mget命令没有获取到新的值。

  1. 与watch对应有一个unwatch命令,它表示watch某个key后可以通过unwatch取消监控;如果watch某个key后有 exec 或 discard 命令执行,程序会自动取消监控,不必再使用unwatch取消监控:
127.0.0.1:6379> watch user:balance:1
OK
127.0.0.1:6379> unwatch
OK
127.0.0.1:6379> 
4、代码中使用
  1. 使用jedis实现扣费逻辑

首先还是先使用jedis测试上面提出扣费场景:
引入依赖:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>4.3.0</version>
</dependency>

主要代码逻辑如下:

import redis.clients.jedis.*;
import java.time.Duration;
import java.util.List;

public class JedisUtil {

    /**
     * 连接池
     */
    private JedisPool jedisPool;

    /**
     * 连接初始化
     * @param host
     * @param port
     * @param password
     */
    public JedisUtil(String host, int port, String password) {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(256);
        config.setMaxIdle(256);
        config.setMinIdle(1);
        config.setMaxWait(Duration.ofMillis(300));

        if(password != null && !"".equals(password)) {
            jedisPool = new JedisPool(config, host, port, 500, password);
        } else {
            jedisPool = new JedisPool(config, host, port, 500);
        }
    }

    /**
     * 关闭连接池
     */
    public void close() {
        if(jedisPool != null && !jedisPool.isClosed()) {
            jedisPool.clear();
            jedisPool.close();
        }
    }

    /**
     * 获取连接
     * @return
     */
    public Jedis getJedis() {
        if(jedisPool != null && !jedisPool.isClosed()) {
            return jedisPool.getResource();
        }
        return null;
    }

    /**
     * 归还jedis对象
     * @param jedis
     */
    public void returnJedis(Jedis jedis) {
        if(jedis != null) {
            jedis.close();
        }
    }

    public static void main(String[] args) {
        // 获取jedis连接
        JedisUtil util = new JedisUtil("192.168.56.101", 6379, "");
        // 键
        String key = "user:balance:1";
        util.deduct(key, 100);
    }

    /**
     * 扣减金额
     */
    public void deduct(String key, int money) {
        Jedis jedis = this.getJedis();
        // 监控键对应值的变化
        jedis.watch(key);

        // 获取账户余额,当余额足够时扣减金额
        String val = jedis.get(key);
        if(val != null && Integer.parseInt(val) >= money) {
            // 开启事务
            Transaction multi = jedis.multi();
            try {
                // 事务中的命令
                multi.incrBy(key, -money);
                System.out.println("run in multi!");

                // 执行事务
                List<Object> exec = multi.exec();
                System.out.println("run exec : " + exec);
            } catch (Exception e) {
                // 放弃事务
                multi.discard();
                e.printStackTrace();
            } finally {
                this.returnJedis(jedis);
            }
        } else {
            // 取消监控
            jedis.unwatch();
            System.out.println("余额不足...");
        }
    }
}

在上面代码中执行事务部分添加断点,并通过其他客户端更新watch对应key的值,发现事务并不执行,这就达到了数据逻辑的一致性,不会因为其他客户端扣减金额后,该客户端继续扣减余额导致剩余金额为负数的情况。

  1. redisTemplate使用事务

在redisTemplate中使用事务,有三种方式,下面的代码是实现上面扣费逻辑的过程:
(1)使用SessionCallback实现:

public void runTransaction(final String key, final String value) {
    List<Object> exec = redisTemplate.execute(new SessionCallback<List<Object>>() {
        @Override
        public <K, V> List<Object> execute(RedisOperations<K, V> operations) throws DataAccessException {
            List<Object> exec = null;
            // 监控键对应值的变化
            operations.watch((K) key);

            ValueOperations<String, String> op1 = (ValueOperations<String, String>) operations.opsForValue();
            String val = op1.get(key);
            int num = Integer.parseInt(value);
            if(val != null && Integer.parseInt(val) >= num) {
                try {
                    // 开启事务
                    operations.multi();

                    // 事务中的命令
                    op1.increment(key, -num);

                    // 执行事务
                    exec = operations.exec();
                } catch (NumberFormatException e) {
                    // 放弃事务
                    operations.discard();
                    e.printStackTrace();
                }
            } else {
                // 取消监控
                operations.unwatch();
                System.out.println("余额不足...");
            }
            return exec;
        }
    });
    System.out.println(exec);
}

(2)使用RedisCallback实现:

public void runTransaction(final String key, final String value) {
    List<Object> exec = redisTemplate.execute(new RedisCallback<List<Object>>() {
        @Override
        public List<Object> doInRedis(RedisConnection connection) throws DataAccessException {
            List<Object> exec = null;
            // 监控键对应值的变化
            connection.watch(key.getBytes(StandardCharsets.UTF_8));

            byte[] val = connection.get(key.getBytes(StandardCharsets.UTF_8));
            int num = Integer.parseInt(value);
            if(val != null && Integer.parseInt(new String(val)) >= num) {
                try {
                    // 开启事务
                    connection.multi();

                    // 事务中的命令
                    connection.incrBy(key.getBytes(StandardCharsets.UTF_8), -num);

                    // 执行事务
                    exec = connection.exec();
                } catch (NumberFormatException e) {
                    // 放弃事务
                    connection.discard();
                    e.printStackTrace();
                }
            } else {
                // 取消监控
                connection.unwatch();
                System.out.println("余额不足...");
            }
            return exec;
        }
    });
    System.out.println(exec);
}

(3)使用@Transactional注解实现:

@Transactional
public void runTransaction(final String key, final String value) {
    // 监控键对应值的变化
    redisTemplate.watch(key);
    String val = redisTemplate.opsForValue().get(key);
    int num = Integer.parseInt(value);
    if(val != null && Integer.parseInt(val) >= num) {
        // 开启事务支持
        // 开启这个值后,所有的命令都会在exec执行完才返回结果,所以需要返回值的命令要在这个方法前执行
        redisTemplate.setEnableTransactionSupport(true);
        try {
            // 开启事务
            redisTemplate.multi();

            // 事务中的命令
            redisTemplate.opsForValue().increment(key, -num);

            // 执行事务
            List<Object> exec = redisTemplate.exec();
            System.out.println(exec);
        } catch (Exception e) {
            // 放弃事务
            redisTemplate.discard();
            e.printStackTrace();
        } finally {
            // 关闭事务支持
            redisTemplate.setEnableTransactionSupport(false);
        }
    } else {
        // 取消监控
        redisTemplate.unwatch();
        System.out.println("余额不足...");
    }
}

事务只能保证每一条命令的原子性,并不能保证事务内所有命令的原子性,上面的示例代码已经验证了这个结论,其实redis中已经提供了一些多值指令,如:mset、mget、hmset、hmget。但是这些只能是一种数据类型的多键值对操作,这些命令是原子操作。
上面这种扣费逻辑,除了使用redis的事务支持,还可以使用lua脚本实现,lua脚本在服务端执行与事务中的命令类似,是不可分割的整体,下面演示lua脚本内容,可以实现上面一样的处理结果:
lua脚本如下:

local b = redis.call('get', KEYS[1]);
if tonumber(b) >= tonumber(ARGV[1]) then
  local rs = redis.call('incrby', KEYS[1], 0 - tonumber(ARGV[1]));
  return rs;
else 
  return nil;
end;

测试过程:

127.0.0.1:6379> set user:balance:1 100
OK
127.0.0.1:6379> get user:balance:1
"100"
127.0.0.1:6379> eval "local b = redis.call('get', KEYS[1]); if tonumber(b) >= tonumber(ARGV[1]) then local rs = redis.call('incrby', KEYS[1], 0 - tonumber(ARGV[1])); return rs; else return nil; end;" 1 user:balance:1 100
(integer) 0
127.0.0.1:6379> get user:balance:1
"0"
127.0.0.1:6379> set user:balance:1 100
OK
127.0.0.1:6379> incrby user:balance:1 -3
(integer) 97
127.0.0.1:6379> eval "local b = redis.call('get', KEYS[1]); if tonumber(b) >= tonumber(ARGV[1]) then local rs = redis.call('incrby', KEYS[1], 0 - tonumber(ARGV[1])); return rs; else return nil; end;" 1 user:balance:1 100
(nil)
127.0.0.1:6379> get user:balance:1
"97"
127.0.0.1:6379> 

第一次执行余额正常够扣场景,第二次设置余额不足,会发现扣减逻辑并没有执行。
以上内容就是redis中事务的全部内容,要记住几点:
(1)redis中的事务跟我们平时用的数据库中的事务有一些差异的,它能保证多条命令执行时中间不会插入其他的命令,但并不会保证所有命令都执行成功,单条redis命令能保证原子性,但事务中的多条命令并不是原子性。
(2)redis中事务分两步完成:第一步将所有命令添加到命令队列中,这一步并不会执行命令;第二步执行队列中的命令。如果第一步中的命令有错误,第二步并不会执行;如果第二步已经开始执行了,那么部分失败的命令并不会影响其他正确命令的结果,这样就会导致一部分命令成功而另外一部分命令失败的场景。
(3)事务中不宜执行过多的命令或非常耗时的命令,因为redis底层执行命令是单线程,如果单个事务中执行过多的命令会导致其他客户端的请求被阻塞。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1291160.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java基础-代码块及其细节

代码块概念&#xff1a; 注意调用时机 好处与使用场景 将构造器的冗余部分提取到代码块 每个构造器执行时都会先执行代码块 静态代码块与普通代码块的区别 注意&#xff1a;创建对象实例时&#xff0c;静态代码块只会被调用一次 例子 public Class DD{static{//打印"…

【Linux】stat命令使用

stat命令 stat命令用于显示文件的状态信息。stat命令的输出信息比ls命令的输出信息要更详细。 著者 由Michael Meskes撰写。 stat命令 -Linux手册页 语法 stat [文件或目录] 命令选项及作用 执行令 &#xff1a; stat --help 执行命令结果 参数 -L、 --dereference 跟…

imutils库介绍及安装学习

目录 介绍 本机环境 安装 常用函数 使用方法 图像平移 图像缩放 图像旋转 骨架提取 通道转换 OPenCV版本的检测 综合测试 目录 介绍 本机环境 安装 常用函数 使用方法 图像平移 图像缩放 图像旋转 骨架提取 通道转换 OPenCV版本的检测 介绍 imutils 是一…

CSM2433 一款集成2.4G+125K 和8位RISC 的SOC芯片

CSM2433是一款集成2.4GHz频段发射器、125KHz接收器和8位RISC&#xff08;精简指令集&#xff09;MCU的SOC芯片。 无线收发器特性&#xff1a; 发射工作在 2.4GHz ISM 频段 发射兼容 BLE 4.2 接收工作在 15KHz-150KHz 内置 32 次可编程 NVM 存储器 3.3V 编程电压 集成低电…

Android笔记(十七):PendingIntent简介

PendingIntent翻译成中文为“待定意图”&#xff0c;这个翻译很好地表示了它的涵义。PendingIntent描述了封装Intent意图以及该意图要执行的目标操作。PendingIntent封装Intent的目标行为的执行是必须满足一定条件&#xff0c;只有条件满足&#xff0c;才会触发意图的目标操作。…

【每日一题】—— D. Jumping Through Segments(Codeforces Round 913 (Div. 3))(二分)

&#x1f30f;博客主页&#xff1a;PH_modest的博客主页 &#x1f6a9;当前专栏&#xff1a;每日一题 &#x1f48c;其他专栏&#xff1a; &#x1f534; 每日反刍 &#x1f7e1; C跬步积累 &#x1f7e2; C语言跬步积累 &#x1f308;座右铭&#xff1a;广积粮&#xff0c;缓称…

配置端口安全示例

组网需求 如图1所示&#xff0c;用户PC1、PC2、PC3通过接入设备连接公司网络。为了提高用户接入的安全性&#xff0c;将接入设备Switch的接口使能端口安全功能&#xff0c;并且设置接口学习MAC地址数的上限为接入用户数&#xff0c;这样其他外来人员使用自己带来的PC无法访问公…

【MATLAB源码-第96期】基于simulink的光伏逆变器仿真,光伏,boost,逆变器(IGBT)。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 1. 光伏单元&#xff08;PV Cell&#xff09; 工作原理&#xff1a;光伏单元通过光电效应将太阳光转换为直流电。它们的输出取决于光照强度、单元温度和负载条件。Simulink建模&#xff1a;在Simulink中&#xff0c;光伏单元…

mysql的组合查询

mysql的组合查询 1、mysql的内连接查询 在 MySQL 中&#xff0c;内连接&#xff08;INNER JOIN&#xff09;是一种根据两个或多个表之间的匹配条件&#xff0c;将多个表中的数据进行联接的操作。内连接只返回符合联接条件的行&#xff0c;而不会返回未匹配的行。 内连接的语…

QT5.4.1无法打开文件

问题描述&#xff1a;起初是在QT代码中运行打开文件代码&#xff1a; QString gFilename QFileDialog::getOpenFileName(this,"open File",path,"*", nullptr,QFileDialog::DontUseNativeDialog);时&#xff0c;出现了堵塞情况&#xff0c;经过多次实验一…

ElasticSearch篇---第五篇

系列文章目录 文章目录 系列文章目录前言一、什么是ElasticSearch?二、ElasticSearch中的集群、节点、索引、文档、类型是什么?三、ElasticSearch中的分片是什么?前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,…

nodejs微信小程序+python+PHP新闻发布系统的设计与实现-计算机毕业设计推荐

目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 3 2.1 nodejs简介 4 2.2 express框架介绍 6 2.4 MySQL数据库 4 第3章 系统分析 5 3.1 需求分析 5 3.2 系统可行性分析 5 3.2.1技术可行性&#xff1a;…

Stable Diffusion AI绘画系列【18】:东方巨龙,威武霸气

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

深圳冬季穿搭造型

深圳冬季穿搭造型 今天是2023年11月17日&#xff0c;北方在11月初就降下了大雪&#xff0c;那是我的老家&#xff0c;无比怀念。 而在深圳&#xff0c;冬天是体会不到那份冷冽的寒冷的&#xff0c;所以在深圳&#xff0c;每天的晚上&#xff0c;我都会出去散步&#xff0c;昨…

走迷宫(详细分析)

目录 一、课题描述 输入样例&#xff1a; 输出样例&#xff1a; 二、需求分析 输入的形式和输入值的范围&#xff1a; 输出的形式&#xff1a; 程序所能达到的功能&#xff1a; 三、概要设计 四、流程图 五 、代码详细注释 六、测试数据和结果 一、课题描述 以一个…

win10编译golang程序报病毒错误

错误为command-line-arguments: open C:\Users\ADMINI~1\AppData\Local\Temp\go-build435947867\b001\exe\a.out.exe: Operation did not complete successfully because the file contains a virus or potentially unwanted software. 解决办法&#xff0c;将Local/Temp目录添…

流量分析1--菜刀666

1&#xff1a;菜刀666&#xff1a; 题目描述 分析流量包&#xff0c;过滤http数据流 追踪TCP数据流 对比第5个流和第7个流发现&#xff0c;同样的目录下 多出了6666.jpg。猜测是由攻击者上传&#xff0c;直接在请求包里搜索FFD8--FFD9 保存为1.jpg 利用foremost工具对1.jpg进…

【Axure高保真原型】个性化自定义图片显示列表

今天和大家分享个性化自定义图片显示列表的原型模板&#xff0c;鼠标点击多选按钮&#xff0c;可以切换按钮选中或者取消选中&#xff0c;按钮选中时&#xff0c;对应图片会在列表中显示&#xff0c;按钮取消后&#xff0c;对应图片会自动隐藏。那这个模板是用中继器制作的&…

三数之和(LeetCode 15)

文章目录 1.问题描述2.难度等级3.热门指数4.解题思路方法一&#xff1a;暴力法方法二&#xff1a;排序双指针 5.实现示例参考文献 1.问题描述 给你一个整数数组 nums&#xff0c;判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k &#xff0c;同时…

Qt开发学习笔记01

设置窗口背景图 在 .h 文件中添加引用和方法 #include <QPainter> #include <QPixmap> void paintEvent(QPaintEvent *);.cpp 文件中实现 paintEvent void sur_dev::paintEvent(QPaintEvent *ev) {QPainter painter(this);QPixmap pix;pix.load(":/image/bj01…