Redis 事务和Pipeline--JAVA(系列文章三)

本文探讨了Redis中的事务特性,指出其不同于传统数据库的事务处理,没有回滚功能,更像是批处理。介绍了Redis事务的基本命令如watch、unwatch、multi、exec和discard,并展示了使用Jedis进行事务操作的代码示例。同时,文章提到了watch命令在乐观锁中的应用,但指出在高并发场景下可能存在的问题。为了解决这些问题,文章推荐使用setnx命令进行锁操作。此外,文章还讲解了Pipeline的概念,它用于批量操作,速度比事务更快,但需要注意避免阻塞Redis服务器。最后,提供了使用Pipeline的示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在传统的关系型数据当中,使用事务是我们最常见的操作。来到Redis当中这里有事务吗,Redis是支持事务的。但是这个事务跟关系型数据库的传统事务不一样,在关系型数据库当中我们可以对出现错误的sql进行回滚,但是在redis是没有这一说的。

在Redis事务当中,所有操作都是在提交的统一执行的,所以并没有回归操作,其实这个事务更像是批处理的感觉。以下就是事务常用的命令:

1、watch

2、unwatch

3、multi

4、exec

5、discard


以下就是使用Jedis使用事务的代码:

public static void main(String args[]){

    GenericObjectPoolConfig config = new GenericObjectPoolConfig();
    config.setMaxIdle(8);
    config.setMaxTotal(10);
    config.setMinIdle(2);
    config.setMaxWaitMillis(3000);

    jedisPool = new JedisPool(config, "localhost");

    Jedis conn = jedisPool.getResource();

    Transaction transaction = conn.multi();

    Response<Long> newListPushResult = transaction.rpush("newList","A","B","C");

    Response<List<String>> newListResponse =  transaction.lrange("newList",0,-1);

    transaction.exec();

    System.out.println("newListPushResult : " + newListPushResult.get());

    for (String item : newListResponse.get()) {
        System.err.println( item + " ");
    }

    conn.close();
}

通过代码我们可以看到我们的执行结果是在exec之后才统一返回,所以Jedis会用一个Response对象最为事务对象transaction的执行放回值。如果我们在transaction执行exec方法之前调用response对象的get方法会出现异常:

Exception in thread "main" redis.clients.jedis.exceptions.JedisDataException: Please close pipeline or multi block before calling this method.

事实上我们还可以使用exec方法的返回值获得事务执行过程的结果,但是这个显然是不够方便的:

public static void main(String args[]){

    GenericObjectPoolConfig config = new GenericObjectPoolConfig();
    config.setMaxIdle(8);
    config.setMaxTotal(10);
    config.setMinIdle(2);
    config.setMaxWaitMillis(3000);

    jedisPool = new JedisPool(config, "localhost");

    Jedis conn = jedisPool.getResource();

    Transaction transaction = conn.multi();

    transaction.rpush("newList","A","B","C");

    transaction.lrange("newList",0,-1);

    List<Object> result = transaction.exec();

    System.out.println("newListPushResult : " + result.get(0));

    List<String> newListValues = (List<String>) result.get(1);

    for (String item : newListValues) {
        System.err.println( item + " ");
    }

    conn.close();
}

事务其中最为只要的功能是实现锁,redis提供一个watch命令。watch是一种乐观锁,watch命令的参数为key,当我们watch了一个key的时候,在事务执行之前被修改了,事务是不会执行成功的。只要我们在watch到事务执这段时间未被修改事务才会执行成功。当然如果我们在业务的中途不需要监控这个key的变化了也可以使用unwatch命令进行取消watch。以下是watch在jedis事务代码上的使用(下面会模拟一段购买商品的业务):

public static void main(String args[]){

    GenericObjectPoolConfig config = new GenericObjectPoolConfig();
    config.setMaxIdle(8);
    config.setMaxTotal(10);
    config.setMinIdle(2);
    config.setMaxWaitMillis(3000);

    jedisPool = new JedisPool(config, "localhost");

    Jedis conn = jedisPool.getResource();


    //market 有序集合 score 商品价格  member 商品名称
    conn.zadd("market",100,"ProductA");
    conn.zadd("market",33,"ProductB");

    //用户对象
    conn.hset("user:1","funds","900");

    //监控market和user:1两个KEY的变化
    conn.watch("user:1","market");

    Double price = conn.zscore("market","ProductA");
    String funds_str = conn.hget("user:1","funds");

    if(price == null || funds_str == null){
        System.out.println("商品或用户信息不存在");
        conn.unwatch();
        return;
    }

    double funds = Double.valueOf(funds_str);

    if(price > funds){
        System.out.println("余额不足");
        conn.unwatch();
        return ;
    }

    //模拟中图余额被修改了,导致不够钱买单
    conn.hset("user:1","funds","20");

    Transaction transaction = conn.multi();

    transaction.hincrByFloat("user:1","funds",-price);

    transaction.zrem("market","ProductA");

    transaction.zadd("user_package:1",price,"ProductA");

    List<Object> result = transaction.exec();

    if(result == null || result.size() == 0 ){
        System.out.println(">>>执行失败,中途被watch的key出现修改");
    }else{
        System.out.println(">>>>执行成功");
    }

    conn.close();
}

输出如下:

>>>执行失败,中途被watch的key出现修改。

我们可以使用redis-cli查看是否有做修改:

127.0.0.1:6379> ZRANGE market 0 -1

1) "ProductB"

2) "ProductA"

可以清楚看到是没有再market中删除 ProductA的,如果在测试过程中都可以使用 MONITOR监控命令,但是在正式环境需要慎重使用 MONITOR命令,因为这样会导致输出缓存暴增问题。

虽然watch能解决我们对锁的需求,但是watch也不是一个完美的解决方案。因为watch是属于乐观锁,如果对于大并发的业务系统中显然是不合适的,因为redis的处理速度相当的快,所以导致很多时候会出现执行失败,每次失败我们都会在一段时间中尝试重试。这样会导致大量的带宽和资源损耗,重复执行也导致很多时候执行效率低下的问题。而且我们发现watch是针对key进行乐观锁的,其实在market这个集合当中,我只需要market中的ProductA不被修改和User的funds不被修改即可,并不需要将整个key进行监控。

为了弥补这个问题我们会使用setnx命令进行锁操作,其实setnx并不是什么锁,以下是redis中文社区对setnx命令的解释:

SETNX key value

将 key 的值设为 value ,当且仅当 key 不存在。

若给定的 key 已经存在,则 SETNX 不做任何动作。

SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

可用版本:
>= 1.0.0
时间复杂度:
O(1)
返回值:
设置成功,返回 1 。
设置失败,返回 0 。
那我们应该怎么去利用setnx如果存在就无法set的这个特性去实现锁呢?看下面的代码就清楚了:

public static void main(String args[]) throws InterruptedException {

    GenericObjectPoolConfig config = new GenericObjectPoolConfig();
    config.setMaxIdle(8);
    config.setMaxTotal(10);
    config.setMinIdle(2);
    config.setMaxWaitMillis(3000);

    jedisPool = new JedisPool(config, "localhost");

    Jedis conn = jedisPool.getResource();


    //market 有序集合 score 商品价格  member 商品名称
    conn.zadd("market", 100, "ProductA");
    conn.zadd("market", 33, "ProductB");

    //用户对象
    conn.hset("user:1", "funds", "900");

    long startTime = System.currentTimeMillis();
    boolean isMarketProductLocked = false;
    boolean isUserFundsLocked = false;
    String locked_id = UUID.randomUUID().toString();
    while (System.currentTimeMillis() - startTime < 3000) {
        if (!isMarketProductLocked) {
            if (conn.setnx("lock:market:productA", locked_id) == 1) {
                isMarketProductLocked = true;
            }
        }
        if (!isUserFundsLocked) {
            if (conn.setnx("lock:user:1:funds", locked_id) == 1) {
                isUserFundsLocked = true;
            }
        }
        if(isUserFundsLocked && isMarketProductLocked){
            break;
        }
        Thread.sleep(80);
    }

    if (!(isMarketProductLocked && isUserFundsLocked)) {
        System.out.println("请求锁失败!");
        return;
    }


    Double price = conn.zscore("market", "ProductA");
    String funds_str = conn.hget("user:1", "funds");

    if (price == null || funds_str == null) {
        System.out.println("商品或用户信息不存在");
        conn.unwatch();
        return;
    }

    double funds = Double.valueOf(funds_str);

    if (price > funds) {
        System.out.println("余额不足");
        conn.unwatch();
        return;
    }


    Transaction transaction = conn.multi();

    transaction.hincrByFloat("user:1", "funds", -price);

    transaction.zrem("market", "ProductA");

    transaction.zadd("user_package:1", price, "ProductA");

    List<Object> result = transaction.exec();

    if (result == null || result.size() == 0) {
        System.out.println(">>>执行失败,中途被watch的key出现修改");
    } else {
        System.out.println(">>>>执行成功");
    }

    conn.del("lock:market:productA");
    conn.del("lock:user:1:funds");

    conn.close();
}
其实这是利用setnx的特性在业务中实现的元素级别悲观锁。以下是我写的一个业务测试锁的性能(模拟一堆用户对商城上购买商品的测试):

public class App {

    volatile public static long successCounter = 0;
    volatile public static long failCounter = 0;
    public static long time = 0;
    volatile public static JedisPool jedisPool;

    public static void main(String[] args) {

        GenericObjectPoolConfig config = new GenericObjectPoolConfig();
        config.setMaxIdle(8);
        config.setMaxTotal(12);
        config.setMinIdle(5);
        config.setMaxTotal(30);
        config.setMaxWaitMillis(3000);

        jedisPool = new JedisPool(config, "localhost");

        Jedis conn = jedisPool.getResource();
        conn.flushAll();


        List<User> userList = init(jedisPool);
        time = System.currentTimeMillis();

        for (User item : userList) {
            try {
                new Thread(new BuyItem(jedisPool.getResource(), item)).start();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }

    static List<User> init(JedisPool pool) {
        List<User> users = new ArrayList<User>();
        users.add(new User(1, "A", 9999999999d));
        users.add(new User(2, "B", 9999999999d));
        users.add(new User(3, "C", 9999999999d));
        users.add(new User(4, "D", 9999999999d));
        users.add(new User(5, "E", 9999999999d));
        users.add(new User(6, "F", 9999999999d));
        users.add(new User(7, "G", 9999999999d));
        users.add(new User(8, "H", 9999999999d));
        users.add(new User(9, "I", 9999999999d));
        users.add(new User(10, "J", 9999999999d));

        List<Grocery> groceries = new LinkedList<Grocery>();
        for (int i = 0; i < 1000000; i++) {
            double price = new Random().nextInt(10) + 10;
            groceries.add(new Grocery("grocery_" + i, price));
        }

        initUser(pool.getResource(), users);
        initMarket(pool.getResource(), groceries);
        return users;
    }

    static void initUser(Jedis conn, List<User> users) {
        Pipeline pipeline = conn.pipelined();
        pipeline.multi();
        for (User item : users) {
            String hashKey = "User:" + item.getId();
            pipeline.hset(hashKey, "username", item.getUsername());
            pipeline.hset(hashKey, "funds", String.valueOf(item.getFunds()));
        }
        pipeline.exec();
        conn.close();
    }

    static void initMarket(Jedis conn, List<Grocery> groceries) {
        Pipeline pipeline = conn.pipelined();
        pipeline.multi();
        int index = 0;
        for (Grocery item : groceries) {
            String zSetKey = "market";
            pipeline.zadd(zSetKey, item.getPrice(), item.getName());
            if (index++ >= 1000) {
                index = 0;
                pipeline.exec();
                pipeline.multi();
            }
        }
        pipeline.exec();
        conn.close();
    }


    static class BuyItem implements Runnable {

        private Jedis conn;
        private User user;

        public BuyItem(Jedis conn, User user) {
            this.conn = conn;
            this.user = user;
        }

        public void run() {

            String buyerKey = "User:" + user.getId();
            String markterKey = "market";
            String inventoryOfbuyerKey = "inventory:" + user.getId();
            String salerKey = null;

            try {
                for (int i = 0; i < 5000; i++) {

                    try {
                        String id = UUID.randomUUID().toString();

                        long randmon = (long) (Math.random() * conn.zcard(markterKey));

                        String groceryKey = (String) conn.zrange(markterKey, randmon, randmon).toArray()[0];

                        boolean hasLock = false;
                        String lockName = "lock:" + markterKey + ":" + groceryKey;
                        long startTime = System.currentTimeMillis();
                        while (System.currentTimeMillis() - startTime <= 5000) {
                            if (conn.setnx(lockName, id) == 1) {
                                hasLock = true;
                                break;
                            }
                        }
                        if (!hasLock) {
                            System.err.println("can not got a lock!");
                            failCounter++;
                            continue;
                        }

                        double price = conn.zscore(markterKey, groceryKey);

                        if (Double.valueOf(conn.hget(buyerKey, "funds")) < price) {
                            return;
                        }

                        Transaction transaction = conn.multi();

                        transaction.hincrByFloat(buyerKey, "funds", -price);
                        transaction.zadd(inventoryOfbuyerKey, price, groceryKey);
                        transaction.zrem(markterKey, groceryKey);

                        List<Object> result = transaction.exec();

                        transaction.close();

                        if (result == null || result.size() <= 0) {
                            System.out.println("can not exec ! ");
                            failCounter++;
                        } else {
                            successCounter++;
                        }

                        while (true) {
                            conn.watch(lockName);
                            if (id.equals(conn.get(lockName))) {
                                conn.del(lockName);
                                conn.unwatch();
                                break;
                            } else {
                                conn.unwatch();
                                break;
                            }
                        }

                    } catch (Exception e) {
                        e.printStackTrace();
                        App.failCounter++;
                    }
                }
            } finally {
                System.out.println(">>>>>failCounter:" + App.failCounter);
                System.out.println(">>>>>successCounter:" + App.successCounter);
                System.out.println(">>>>>time:" + (System.currentTimeMillis() - time));
                conn.close();
            }
        }


    }
}

输出如下:

>>>>>failCounter:0  失败为0
>>>>>successCounter:49820  成功交易49820次
>>>>>time:11948 耗时为11948毫秒

其实如果稍微做优化这个速度可以更快,不过我们这里是提现了setnx锁的作用而已。


Pipeline

其实Pipeline跟事务一样都是做批量操作的,如果不必要使用事务可以使用Pipeline,Pipeline比事务的速度更快,因为事务处理批量提交还需要其他的一些业务逻辑的。使用pipeline。

Pipeline pipeline = conn.pipelined();

Response<Long> rpushResult = pipeline.rpush("testList", "A", "B", "C");
Response<List<String>> testListResponse = pipeline.lrange("testList", 0, -1);

pipeline.sync();

System.out.println("rpushResult:" + rpushResult.get());

for (String item : testListResponse.get()) {
    System.out.print( item + " ");
}

当然可以使用pipeline.syncAndReturnAll(),这个跟transaction.exec的返回方式一样。

但是需要注意不要一次性搞一堆数据发出去,这样很可能会阻塞redis,因为人redis是单线程的去执行命令的,所以如果一个命令太慢可能会阻塞其他客户端所以使用flushall,keys * 这些操作也可能会阻塞客户端。pipline和transaction也同理,如果exec和sync一次性提交的命令非常多就非常有可能会阻塞redis,同时jedis读写也是会超时的,如果一次性pipeline提交的数据太多也可能会导致读写超时异常,所以我上面一次性写入market的测试数据都是通过分批次去提交的:

static void initMarket(Jedis conn, List<Grocery> groceries) {
    Pipeline pipeline = conn.pipelined();
    pipeline.multi();
    int index = 0;
    for (Grocery item : groceries) {
        String zSetKey = "market";
        pipeline.zadd(zSetKey, item.getPrice(), item.getName());
        if (index++ >= 1000) {
            index = 0;
            pipeline.exec();
            pipeline.multi();
        }
    }
    pipeline.exec();
    conn.close();
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值