Redis笔记之Redis事务

1. 事务

Redis的单条命令是保证原子性的,但是Redis事务不能保证原子性

Redis事务本质:一组命令的集合
步骤:

  1. 开启事务
  2. 操作一组命令(所有命令放入一个队列)
  3. 执行事务

事务中每条命令都会被序列化,执行过程中按顺序执行,不允许其他命令的干扰。

  • 一致性
  • 顺序性
  • 排他性
  • Redis事务没有隔离级别的概念

1.1 Redis事务操作过程

  • 事务开启(multi
  • 命令入队
  • 执行事务(exec

1.1.1 事务的开启与执行

所有事务中的命令在加入时都没有被执行,直到提交时才会开始执行(exec)一次性完成。

案例:

127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 v1 # 命令入队
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> get k1
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> keys *
QUEUED
127.0.0.1:6379> exec # 执行事务
1) OK
2) OK
3) "v1"
4) OK
5) 1) "k3"
   2) "k2"
   3) "k1"
127.0.0.1:6379> 

1.1.2 取消事务(discard)

案例:

127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 v1 # 命令入队
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> discard # 放弃事务
OK
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI  # 当前未开启事务,无法执行
127.0.0.1:6379> get k1 # 被放弃事务中的命令并未执行
(nil)

1.2 事务错误

1.2.1 编译时异常

代码语法错误(编译时异常)所有的命令都不执行

案例:

127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 v1 # 命令入队
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> key * # 遇到语法错误异常
(error) ERR unknown command `key`, with args beginning with: `*`, 
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> exec # 提交事务失败
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k1 # 所有命令都不会被执行
(nil)

1.2.2 运行时异常

代码逻辑错误(运行时异常其他命令可以正常执行 >>> 所以不保证事务的原子性

案例:

127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 v1 # 命令入队
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> incr k1 # 逻辑错误(对字符串无法进行增量)
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) (error) ERR value is not an integer or out of range # 运行时报错
4) "v2" # 其他命令正常执行

虽然中间有一条命令报错了,但是后面的指令依旧执行成功了,所以所Redis单条命令保证原子性,但是Redis不能保证原子性。

总结:

  1. 编译时异常:所有命令都不会被执行
  2. 运行时异常:错误命令不会被执行,其他命令正常执行

1.3 监控

1.3.1 悲观锁

  1. 概念
    悲观锁,顾名思义:很悲观,在每次事务当中,都认为有人会更改数据,所以提前把数据锁住,一种以悲观的态度来防止一切数据冲突。

在修改数据之前把数据锁住,然后对数据进行读写,在它释放锁之前任何人都不能对其数据进行操作,直到前面一个人把锁释放后,下一个人对数据加锁才能操作数据
一般数据库本身锁的机制都是基于悲观锁的机制来实现的

  1. 特点
    可以完全保证数据的独占性和正确性,因为每次请求都会对数据进行加锁,然后进行数据操作,最后再解锁,而加锁解锁的过程会造成消耗,所以性能不高;
  2. 实现
    • mysql 悲观锁的方法:

      1. 开启事务:

        start transaction;
        
      2. 查询信息

        select status from table where id = 1 for update  # for update加锁
        
      3. 根据信息生成订单

        insert into table111(id, goods_id) vlaues(null,1);
        
      4. 修改商品

        update table set status = 2 where id =1;
        
      5. 提交信息

        commit; # commit释放锁
        
    • Java悲观锁的实现

      • Java 中悲观锁的实现包括 synchronized 关键字和 Lock 相关类等,我们以 Lock 接口为例,例如 Lock 的实现类 ReentrantLock,类中的 lock() 等方法就是执行加锁,而 unlock() 方法是执行解锁。处理资源之前必须要先加锁并拿到锁,等到处理完了之后再解开锁。

1.3.2 乐观锁

  1. 概念

    • 很乐观,认为什么时候都不会出现问题,所以不会上锁!在更新数据的时候判断一下,在此期间是否有人修改过这个数据
    • 获取version
    • 更新的时候比较version
  2. 实现

    1. 给表添加version字段,模拟两个线程,线程A和线程B同时执行修改一条数据
    2. 线程A执行更新
      update table set goods='goods_1',version=version+1 where id = 1 and version = 0;
      
    3. 线程B执行更新,此时数据已经被修改了,所以线程B执行不成功
      update table set goods='goods_1',version=version+1 where id = 1 and version = 0;  #   判断version,是否有人修改过这个数据
      
    4. 实现乐观锁控制

    如果不使用乐观锁控制,AB同时操作一条数据,会造成一条数据被修改了两次,产生脏数据,例如买票,只剩下一张票,结果AB都拿到了票,数据库发现票数为-1的情况。

1.3.3 监控数据(watch key)

使用watch key 监控指定数据,相当于乐观锁加锁

正常执行

127.0.0.1:6379> set money 100 # 设置余额:100
OK
127.0.0.1:6379> set use 0 # 支出使用:0
OK
127.0.0.1:6379> watch money # 监控money(上锁)
OK
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby use 20
QUEUED
127.0.0.1:6379> exec # 监视值没有被被中途修改,事务正常执行
1) (integer) 80
2) (integer) 20
127.0.0.1:6379>

测试多线程修改值,使用watch可以当作redis的乐观锁操作( 相当于getversion )

启动另外一个客户端模拟插队线程

  • 线程1:

    # money: 100 use: 0
    127.0.0.1:6379> watch money # 上锁
    OK
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> decrby money 20
    QUEUED
    127.0.0.1:6379> incrby use 20
    QUEUED
    127.0.0.1:6379>  # 此时事务还没有结束,还没执行事务
    
  • 线程2插队:

    127.0.0.1:6379> incrby money 500 # 修改了线程1中监视的money
    (integer) 600
    127.0.0.1:6379> 
    
  • 回到线程1,执行事务:

    127.0.0.1:6379> exec # 执行之前,另外一条线程修改了money,这个时候就会导致事务执行失败
    (nil) # 没有结果,说明事务执行失败
    127.0.0.1:6379> get money # 线程2,修改生效
    "600"
    127.0.0.1:6379> get use # 线程1事务执行失败,数值没有修改
    "0"
    

解锁(unwatch

unwatch进行解锁获取最新值,然后再加锁进行事务。

注意
每次提交执行exec后都会自动释放锁,不管是否成功

posted on   JAVA开发区  阅读(12)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
点击右上角即可分享
微信分享提示