redis中使用事务

事务是指一个执行过程,要么全部执行成功,要么失败什么都不改变。不会存在一部分成功一部分失败的情况,也就是事务的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底层执行命令是单线程,如果单个事务中执行过多的命令会导致其他客户端的请求被阻塞。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

论MYSQL注入的入门注解

&#x1f4d1;打牌 &#xff1a; da pai ge的个人主页 &#x1f324;️个人专栏 &#xff1a; da pai ge的博客专栏 ☁️宝剑锋从磨砺出&#xff0c;梅花香自苦寒来 &#x1f4d1;什么是MySQL注入&…

AI模型平台Hugging Face存在API令牌漏洞;大型语言模型与任务模型

&#x1f989; AI新闻 &#x1f680; AI模型平台Hugging Face存在API令牌漏洞&#xff0c;黑客可窃取、修改模型 摘要&#xff1a;安全公司Lasso Security发现AI模型平台Hugging Face上存在API令牌漏洞&#xff0c;黑客可获取微软、谷歌等公司的令牌&#xff0c;并能够访问模…

【PyTorch】训练过程可视化

文章目录 1. 训练过程中的可视化1.1. alive_progress1.2. rich.progress 2. 训练结束后的可视化2.1. tensorboardX2.1.1. 安装2.1.2. 使用 1. 训练过程中的可视化 主要是监控训练的进度。 1.1. alive_progress 安装 pip install alive_progress使用 from alive_progress i…

持续集成交付CICD: Sonarqube REST API 查找与新增项目

目录 一、实验 1.SonarQube REST API 查找项目 2.SonarQube REST API 新增项目 一、实验 1.SonarQube REST API 查找项目 &#xff08;1&#xff09;Postman测试 转换成cURL代码 &#xff08;2&#xff09;Jenkins添加凭证 &#xff08;3&#xff09;修改流水线 pipeline…

HCIP考试实验

实验更新中&#xff0c;部分配置解析与分析正在完善中........... 实验拓扑图 实验要求 要求 1、该拓扑为公司网络&#xff0c;其中包括公司总部、公司分部以及公司骨干网&#xff0c;不包含运营商公网部分。 2、设备名称均使用拓扑上名称改名&#xff0c;并且区分大小写。 3…

SQL server 根据已有数据库创建相同的数据库

文章目录 用导出的脚本创建相同的数据库导出建表脚本再次建表 一些sql语句 用导出的脚本创建相同的数据库 导出建表脚本 首先&#xff0c;右击要导出的数据库名&#xff0c;依次选择任务-生成脚本。 简介&#xff08;第一页&#xff09;处选择下一步&#xff0c;然后来到选择…

MAMBA介绍:一种新的可能超过Transformer的AI架构

有人说&#xff0c;“理解了人类的语言&#xff0c;就理解了世界”。一直以来&#xff0c;人工智能领域的学者和工程师们都试图让机器学习人类的语言和说话方式&#xff0c;但进展始终不大。因为人类的语言太复杂&#xff0c;太多样&#xff0c;而组成它背后的机制&#xff0c;…

MAC 系统在vs code中,如何实现自动换行

目录 问题描述&#xff1a; 问题解决&#xff1a; 问题描述&#xff1a; 在vscode中&#xff0c;有些时候&#xff0c;一行内容过多&#xff0c;如果不能自动换行&#xff0c;就需要拖动页面&#xff0c;才能看到完整的内容。如下图两行所示&#xff1a; 问题解决&#xff1a…

国标GB28181设备注册安防监控平台EasyCVR不上线是什么原因?

安防视频监控EasyCVR平台兼容性强&#xff0c;可支持的接入协议众多&#xff0c;包括国标GB28181、RTSP/Onvif、RTMP&#xff0c;以及厂家的私有协议与SDK&#xff0c;如&#xff1a;海康ehome、海康sdk、大华sdk、宇视sdk、华为sdk、萤石云sdk、乐橙sdk等。平台能将接入的视频…

【Python】Flask + MQTT 实现消息订阅发布

目录 Flask MQTT 实现消息订阅发布准备开始1.创建Flask项目2创建py文件&#xff1a;mqtt_demo.py3.代码实现4.项目运行5.测试5.1 测试消息接收5.2 测试消息发布6、扩展 Flask MQTT 实现消息订阅发布 准备 本次项目主要使用到的库&#xff1a;flask_mqtt pip install flask…

如何自定义负载均衡策略

参考官方资源 Home Netflix/ribbon Wiki (github.com)6. 客户端负载均衡器&#xff1a;功能区 (spring.io)负载均衡策略 内置负载均衡规则类规则描述RoundRobinRule简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。AvailabilityFilteringRule对以下两种服务器进…

10.Java程序设计-基于SSM框架的微信小程序家教信息管理系统的设计与实现

摘要是论文的开篇&#xff0c;用于简要概述研究的目的、方法、主要结果和结论。以下是一个简化的摘要示例&#xff0c;你可以根据实际情况进行修改和扩展&#xff1a; 摘要 随着社会的发展和教育需求的增长&#xff0c;家教服务作为一种个性化的学习方式受到了广泛关注。为了更…

STM32L051使用HAL库操作实例(13)- 读取IAQ-CORE-C传感器实例

目录 一、前言 二、传感器参数 三、STM32CubeMX配置&#xff08;本文使用的STM32CubeMX版本为6.1.2&#xff09;例程使用模拟I2C进行数据读取 1.MCU选型 2.使能时钟 3.时钟配置 4.GPIO口配置 四、配置STM32CubeMX生成工程文件 五、点击GENERATE CODE生成工程文件 六、…

系统设计-缓存介绍

该图说明了我们在典型架构中缓存数据的位置。 沿着流程有多个层次。 客户端应用程序&#xff1a;HTTP 响应可以由浏览器缓存。我们第一次通过 HTTP 请求数据&#xff0c;返回时在 HTTP 标头中包含过期策略&#xff1b;我们再次请求数据&#xff0c;客户端应用程序首先尝试从浏…

看图学源码之 Atomic 类源码浅析二(cas + 分治思想的原子累加器)

原子累加器 相较于上一节看图学源码 之 Atomic 类源码浅析一&#xff08;cas 自旋操作的 AtomicXXX原子类&#xff09;说的的原子类&#xff0c;原子累加器的效率会更高 XXXXAdder 和 XXXAccumulator 区别就是 Adder只有add 方法&#xff0c;Accumulator是可以进行自定义运算方…

工业4G路由器助力轨道交通城市地铁实现数字化转型

随着城市的科技不断发展&#xff0c;地铁系统的智能化程度也在不断提高。地铁闸机的网络部署已经成为地铁建设中必不可少环节。而4G路由器作为地铁闸机的网络通讯设备&#xff0c;助力轨道交通地铁闸机实现数字化转型。 工业4G路由器在地铁系统光纤宽带网络遇到故障或其他问题…

2023 金砖国家职业技能大赛网络安全省赛二三阶段样题(金砖国家未来技能挑战赛)

2023 金砖国家职业技能大赛网络安全省赛二三阶段样题&#xff08;金砖国家未来技能挑战赛&#xff09; 第二阶段&#xff1a; 安全运营 **背景&#xff1a;**作为信息安全技术人员必须能够掌握操作系统加固与安全管控、防火 墙一般配置、常见服务配置等相关技能&#xff0c;利…

java WebSocket带参数处理使用

1、webSocket实现代码 Component public class WebSocketStompConfig {//这个bean的注册,用于扫描带有ServerEndpoint的注解成为websocket// ,如果你使用外置的tomcat就不需要该配置文件Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpoi…

微信小程序 -- ios 底部小黑条样式问题

问题&#xff1a; 如图&#xff0c;ios有的机型底部伪home键会显示在按钮之上&#xff0c;导致点击按钮的时候误触 解决&#xff1a; App.vue <script>export default {wx.getSystemInfo({success: res > {let bottomHeight res.screenHeight - res.safeArea.bott…

Xcode15 Library ‘iconv.2.4.0‘ not found

Xcode 15运行老代码报错&#xff1a;Library iconv.2.4.0 not found 解决&#xff1a; TARGETS-->Bulid Phases --> Link Binary With Libraries 添加一个“Libiconv.tbd”, 同时把原来的 “libiconv.2.4.0.tbd”删除&#xff08;一定要删除&#xff0c;不然运行还是…