详谈redis事务与流水线

一、事务与流水线

1.1 事务

事务是一组命令的集合,事务支持一次性执行多个命令。在事务执行过程中,会按顺序串行化执行队列中的命令。事务可以让一个客户端在不被其他客户端打断的情况下执行多个命令。

举个例子来说,中秋了,你要去超市买螃蟹吃,于是买螃蟹的过程中,你就是一个客户端,卖螃蟹的超市是另一个客户端,你们两之间会发生以下的事情:
1、你付钱
2、超市收钱
3、你从超市拿走螃蟹
如果三个按照正常顺序执行完就没有什么问题,但是如果你付了钱,超市收了钱,等到你拿螃蟹的时候,螃蟹已经卖完了,那么这个交易就会出现问题,所以整个交易过程必须保证这三步都顺利完成或者直接全部失败。因此按照事务的执行方法,可以把这三件事装进一个容器内,在你买螃蟹的时候,别人不许拿走你挑好的螃蟹,也不许你不付钱超市不收钱就完成交易。

1.2 事务的开始与执行

redis的事务以特殊命令MULTI为开始,之后传入多个需要执行的命令,最后以EXEC为结束,开始执行

买螃蟹作为一个事务就是
MULTI // 开始事务
1、你付钱
2、超市收钱
3、你从超市拿走螃蟹
EXEC // 执行事务

1.3 流水线

看以上买螃蟹的伪代码会发现,直到EXEC命令的时候,买螃蟹的这个事务内的三条命令才被全部发送给redis,然后redis执行这三个步骤,最终将执行完的结果发送回客户端。就是一次性发送多个命令,然后等待所有回复出现这种做法,通常称为流水线(pipelining)

意思就是,在以往的使用redis的方法就像你叫你妈妈去超市买东西,第一次她从家里去超市买回了牛奶,回了家
然后你还想要巧克力,于是你妈妈就又从家到超市买了巧克力,回了家
然后你又想吃冰激凌,于是你妈妈就又从家到超市买了冰激凌,回了家
读到这里你肯定会说,我可以一次性和她说好买什么东西列一个清单给她就好了呀,确实,像刚刚说的,一次性发送多个命令,然后等待所有回复出现,这种做法就是我们买东西之前列的清单,一次性将要买的商品列好,妈妈只需要去一次超市就可以买回多个东西。

二、事务的实际应用

2.1 应用场景

就拿刚刚买螃蟹的场景来说吧,整个流程是消费者从超市买螃蟹,消费者需要支付螃蟹的价格*数量,消费者拿到购买数量的螃蟹,超市减少消费者购买的螃蟹,超市收到了货款。其中还要注意,消费者是不是没钱购买这么多螃蟹,超市是不是不能提供消费者想要的螃蟹数量。
在这里插入图片描述
先充分理解这个场景,然后接下去思考一下这个场景所需要的数据结构。

2.2 应用数据结构

消费者,首先消费者有一个ID,然后他应该有钱(即可用于买螃蟹的钱),还应该有一个已有螃蟹的数量(默认为0),简单看来,选用一个散列就可以完成
在这里插入图片描述
超市,首先要表明一下超市的螃蟹余量,,其次超市也应该有一个超市ID,因为一个城市肯定有许多家超市,再者超市应该有一个余额(当然我们是为了模拟收到钱才这些写的)
在这里插入图片描述

2.3 代码实现

先贴下主要的逻辑实现,即购买函数

public static boolean purchase(Jedis conn,int number,String consumer,String market){
        long end = System.currentTimeMillis()+1000;
        int total = 0;
        int field = 0;
        int num = 0;
        while (System.currentTimeMillis()<end){

            conn.watch(consumer,market);
            //螃蟹单价
            total = Integer.parseInt(conn.hget(market,"price"));
            System.out.println("###"+total);
            //消费者余额
            field = Integer.parseInt(conn.hget(consumer,"field"));
            //超市剩余螃蟹总数
            num = Integer.parseInt(conn.hget(market,"crabNumber"));
            //消费者钱不够或者超市螃蟹数量不够
            if (total*number>field||num<number){
                conn.unwatch();
                return false;
            }

            Transaction tran = conn.multi();
            //消费者钱减少
            tran.hincrBy(consumer,"field",-total*number);
            //消费者螃蟹增多
            tran.hincrBy(consumer,"crabNumber",number);
            //超市螃蟹减少
            tran.hincrBy(market,"crabNumber",-number);
            //超市钱增多
            tran.hincrBy(market,"field",total*number);
            List<Object> results = tran.exec();
            if(results==null){
                return false;
            }else {
                break;
            }
        }
        return true;
    }

然后在main函数内创建适宜购买测试的例子

public static void main(String[] args) {
        Jedis conn = new Jedis(服务器IP,端口);
        conn.select(4);
        /**客户散列
         * 默认0只螃蟹
         * 1000元钱*/
        System.out.println(conn.hset("consumer:"+18,"crabNumber","0"));
        System.out.println(conn.hset("consumer:"+18,"field","1000"));
        /**超市散列
         * 默认1000只螃蟹
         * 10000元钱
         * 螃蟹100一只*/
        System.out.println(conn.hset("market:"+19,"crabNumber","1000"));
        System.out.println(conn.hset("market:"+19,"price","100"));
        System.out.println(conn.hset("market:"+19,"field","10000"));
        /**购买函数
         * 入参:redis连接,购买数量
         * 出参:是否购买成功*/
        System.out.println(purchase(conn,5,"consumer:"+18,"market:"+19));
    }

最后一行代码是购买函数的调用,此时是购买5只螃蟹,一只100块,500<1000那么此次购买不出意外的话会成功。
运行一下程序
在这里插入图片描述
再查看redis中的数据
客户
可以看到客户原有的1000元花掉了500,还剩500,买进了5只螃蟹
在这里插入图片描述
超市
可以看到超市原有1000只螃蟹卖掉了5只还剩995只
超市原有10000元余额,现剩余10500,即收入500元
在这里插入图片描述
以上是测试成功的例子,那么再测试一下不能完成购买的例子,假设客户只有1000块钱,要买50只螃蟹,显然是不能实现的
那么,修改main函数中最后一行代码

System.out.println(purchase(conn,50,"consumer:"+18,"market:"+19));

先抹掉redis中的相关数据,再运行程序
在这里插入图片描述
查看客户的信息
因为没买成功,所以螃蟹价格和数量都没有发生变化
在这里插入图片描述
超市
因为没有卖出螃蟹,所以螃蟹数量和超市余额都没有变化
在这里插入图片描述

三、非事务型流水线

上一节讲到了使用事务的好处和必要性,本节将介绍不使用事务时的流水线操作。首先我们得模拟一个客户端和redis需要多次连接的场景,比如在《redis构建web应用》这篇blog里面的源码CleanSessionThread部分,有这么一段

/**toArray()方法会返回List中所有元素构成的数组,并且数组类型是Object[]*/
conn.del(sessionKeys.toArray(new String[sessionKeys.size()]));
/**在登录散列中删除这些用户的token*/
conn.hdel("login:", tokens);
/**在最近查看有序集合中移除*/
conn.zrem("recent:", tokens);

很明显我们看到做这样一个简单的操作,需要和redis通信3次,很明显地对应上文妈妈去超市买东西的例子,如果我们能一次性和妈妈说好,就可以一次性干好。于是我们想到了用流水线。

Pipeline pipe = conn.pipelined();
pipe.multi();
/**toArray()方法会返回List中所有元素构成的数组,并且数组类型是Object[]*/
pipe.del(sessionKeys.toArray(new String[sessionKeys.size()]));
/**在登录散列中删除这些用户的token*/
pipe.hdel("login:", tokens);
/**在最近查看有序集合中移除*/
pipe.zrem("recent:", tokens);
pipe.exec();

这样就只需要和redis通信一次照样可以完成想要完成的任务。从而提高了效率。

总结

本文用了两个简单的例子来说明了事务与流水线的相关知识,在实际开发中事务和流水线的使用肯定比这些例子复杂,但是基本的思路是不变的,只要读者好好理解原理,就不难掌握了。

对Java系列知识感兴趣的朋友可以加入QQ群
慧梦软件开发技术联盟:952317701
更多系列文章在java高级程序开发微信公众号
Java高级开发技术

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值