Redis 事务

简介

为了确保连读多个操作的原子性,一个成熟的数据库通常都会有事务支持。Redis事务也不例外,Redis的事务使用非常简单,不同于关系数据库,我们无需理解那么多复杂的事务模型,就可以直接使用。不过也正是因为这种简单性,他的事务模型很不严格。要求我们不能像使用关系型数据库的事务一样来使用Redis。

Redis事务的基本使用

每个事务的操作都有begin、commit和rollback,大致形式如下:

begin(); 
try {
 command1(); 
 command2(); ....
  commit();
} 
catch(Exception e) { 
  rollback();
}

Redis在形式上看起来也差不多,分别是multi/exec/discard。multi指示事务的开始,exec指示事务的执行,discard指示事务的丢弃。

> multi
OK
> incr books QUEUED
> incr books QUEUED
> exec 
(integer) 1
(integer) 2

上面的指令演示了一个完整的事务过程,所有的指令在exec之前不执行,而是缓存在服务器的一个事务队列中,服务器一旦收到exec指令,才开始执行整个事务队列。执行完毕后,将执行结果一次性返回。因为Redis单线程特性,他不用担心自己在执行队列的时候被其他指令打搅,可以保证他们能得到【原子性】执行。

原子性

事务的原子性指要么事务全部成功,要么全部失败,那么Redis事务执行时原子性的吗? 答案:不是

> multi
OK
> set books iamastring
QUEUED // 表示指令已被缓存
> incr books
QUEUED
> set poorman iamdesperate
QUEUED
> exec
1) OK
2) (error) ERR value is not an integer or out of range 3) OK
> get books
"iamastring"
> get poorman
"iamdesperate

上面例子中事务执行中间遇到失败了,但是后面的指令还继续执行,poorman的值能继续得到设置。
所以Redis的事务根本不能算【原子性】,他仅仅满足了事务的【隔离性】,隔离性中的串行化——当前执行的事务不被其他事务打断的权利。

discard

Redis为事务提供一个discard指令,用于丢弃事务缓存队列中的所有指令,在exec执行之前。

> get books 
(nil)
> multi 
OK
> incr books 
QUEUED
> incr books 
QUEUED
> discard 
OK
> get books 
(nil)

discard之后,队列中所有指令都没执行,就好像multi和discard中间的所有指令从未发生过。

优化

上面的Redis事务在发送每个指令到事务缓存队列时都要经过一次网络读写,当一个事务内部的指令较多时,需要的网络IO时间也会线性增长。所以通常Redis的客户端在执行事务时都会结合pipeline一起使用,这样可以将多次IO操作压缩为单次IO操作。

Watch

业务场景: Redis存储了我们的账户余额数据,他是一个整数。现在有两个并发的客户端要对账户余额进行修改操作,这个修改不是一个简单的incrby指令,而是要对余额乘以倍数。我们需要先取出余额然后在内存里乘以倍数,再将结果写回Redis。

这样就会出现并发问题,因为有多个客户端会并发进行操作,我们可以通过Redis的分布式锁来避免冲突,但是分布式锁属于悲观锁,此外,Redis提供了watch机制,他属于一种乐观锁。具体使用方式如下:

while True: 
 do_watch()
 commands() 
 multi() 
 send_commands() 
try:
 exec()
 break
except WatchError:
 continue

watch会在事务开始之前盯住1个或多个关键变量,当事务执行时,也就是服务器收到了exec指令要顺序执行缓存的事务队列时,Redis会检查关键变量自watch之后,是否发生变化,如果被修改了,exec指令将返回null或者其他异常告知客户端事务执行失败。此时,客户端可以选择进行重试。

> watch books 
OK
> incr books
(integer) 1
# 被修改了
> multi
OK
> incr books
QUEUED
> exec # 事务执行失败 (nil)

注意:Redis禁止在multi和exec之间执行watch指令

业务场景代码

public class TransactionDemo {
    public static void main(String[] args) {
        Jedis jedis = new Jedis();
        String userId = "abc";
        String key = keyFor(userId);
        System.out.println(doubleAccount(jedis, userId));
        jedis.close();
    }
    public static int doubleAccount(Jedis jedis, String userId) {
        String key = keyFor(userId);
        while (true) {
            jedis.watch(key);
            int value = Integer.parseInt(jedis.get(key));
            value *= 2; // 加倍
            Transaction tx = jedis.multi();
            tx.set(key, String.valueOf(value));
            List<Object> res = tx.exec();
            if (res != null) {
                break; // 成功了 
            }
          }
      return Integer.parseInt(jedis.get(key)); // 重新获取余额 
   }
   public static String keyFor (String userId){
       return String.format("account_{}", userId);
   }
}
  • 26
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值