zoukankan      html  css  js  c++  java
  • Redis的事务

    Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

    Redis事务的主要作用就是串联多个命令防止别的命令插队。Redis的事务没有提供关系型数据库的回滚特性。

    Multi、Exec、discard

    从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。

    组队的过程中可以通过discard来放弃组队。

    示意图:

     例子:

    192.168.71.170:6379[1]> MULTI
    OK
    192.168.71.170:6379[1]> set k1 va
    QUEUED
    192.168.71.170:6379[1]> get k1
    QUEUED
    192.168.71.170:6379[1]> EXEC
    1) OK
    2) "va"
    192.168.71.170:6379[1]>

    discard取消:

    192.168.71.170:6379[1]> MULTI
    OK
    192.168.71.170:6379[1]> set k2 v2
    QUEUED
    192.168.71.170:6379[1]> get k1 k2
    (error) ERR wrong number of arguments for 'get' command
    192.168.71.170:6379[1]> EXEC
    (error) EXECABORT Transaction discarded because of previous errors.

    组队中某个命令出现了报告错误,执行时整个的所有队列会都会被取消。这个主要是命令的格式错误。

     例子(反正就是输命令的时候不能出错):

    192.168.71.170:6379[1]> MULTI
    OK
    192.168.71.170:6379[1]> set k2 v2
    QUEUED
    192.168.71.170:6379[1]> get k1 k2
    (error) ERR wrong number of arguments for 'get' command
    192.168.71.170:6379[1]> EXEC
    (error) EXECABORT Transaction discarded because of previous errors.

    如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。

     WATCH key 命令

    在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

     例如:

    软件园170:1>keys *
    
    软件园170:1>watch k1
    "OK"
    软件园170:1>multi
    "OK"
    软件园170:1>set k1 0
    "QUEUED"
    软件园170:1>incr k1
    "QUEUED"

    但是这个时候还没有exec执行。

    如果另外一个客户端:

    软件园170:1>set k1 10
    "OK"
    软件园170:1>keys *
     1)  "k1"

    然后再执行:

    软件园170:1>exec
    
    软件园170:1>get k1
    "10"

    会发现这次事务命令进队列后,exec是无效的。因为有watch k1, 但是k1被另一个客户端修改了。

    unwatch命令

    取消 WATCH 命令对所有 key(不仅仅是当前刚watch的key) 的监视。 如果在执行 WATCH 命令之后, EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了。

    以下是一个商品抢购的例子,帮助深入理解redis事务:

    先考虑事务冲突的问题

    三个请求

    1、一个请求想给金额减8000

    2、一个请求想给金额减5000

    3、一个请求想给金额减1000

    如果不加任何处理,当三个请求都判断当前金额10000大于本次消费金额会同时进行交易,就是直接导致剩余金额为-4000,导致超买。

    解决思路:

     

    悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

    乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。如svn。

    秒杀,记录商品库存和秒杀成功者清单。

    商品库存:

    key string
    sk:prodid:qt

    剩余个数

    秒杀成功者清单

    key

    set
    sk:prod-id:usr

    成功者的user_id

    超卖问题

    看一下代码,没有加事务,会导致,如果两个进程获取到库存为1,都同时减一,会导致,库存变为-1,导致超卖。

    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.Transaction;
    
    import java.util.List;
    import java.util.Random;
    
    public class SecondKillTest {
        public static void main(String[] args) {
            Jedis jedis = null;
            String productId = "";
            String userId = "" + new Random().nextInt(10000);
            try {
                jedis = new Jedis("192.168.71.170", 6379);
                jedis.auth("linewell-123");
                String memberKey = "sk:" + productId + ":users";
                Boolean flg = jedis.sismember(memberKey, userId);
                if (flg) {
                    System.out.println("该用户已经成功秒杀过,无法继续参与此活动");
                    return;
                } else {
                    //获取库存
                    String key = "sk:" + productId + ":qt";
                    String qtString = jedis.get(key);
                    if (qtString == null) {
                        System.out.println("改商品没有秒杀活动");
                        return;
                    } else {
                        int qt = Integer.valueOf(qtString);
                        if (qt <= 0) {
                            System.out.println("该商品已被抢光");
                        } else {
                            jedis.decr(qtString);
                            jedis.sadd(memberKey, userId);
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (jedis != null) {
                    jedis.close();
                }
            }
        }
    }

    所以我们用watch加事务来解决超卖问题:

    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.Transaction;
    
    import java.util.List;
    import java.util.Random;
    
    public class SecondKillTest {
        public static void main(String[] args) {
            Jedis jedis = null;
            String productId = "";
            String userId = "" + new Random().nextInt(10000);
            try {
                jedis = new Jedis("192.168.71.170", 6379);
                jedis.auth("linewell-123");
                String memberKey = "sk:" + productId + ":users";
                Boolean flg = jedis.sismember(memberKey, userId);
                if (flg) {
                    System.out.println("该用户已经成功秒杀过,无法继续参与此活动");
                    return;
                } else {
                    //获取库存
                    String qtkey = "sk:" + productId + ":qt";
                    jedis.watch(qtkey);
                    String qtString = jedis.get(qtkey);
                    if (qtString == null) {
                        System.out.println("改商品没有秒杀活动");
                        return;
                    } else {
                        int qt = Integer.valueOf(qtString);
                        if (qt <= 0) {
                            System.out.println("该商品已被抢光");
                        } else {
                            Transaction multi = jedis.multi();
                            multi.decr(qtString);
                            multi.sadd(memberKey, userId);
                            List<Object> exec = multi.exec();
                            if (exec == null || exec.isEmpty()) {
                                System.out.println("操作失败");
                            } else {
                                System.out.println("秒杀成功");
                            }
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (jedis != null) {
                    jedis.close();
                }
            }
        }
    }

    这个使用了,watch和事务,解决超卖问题,但是可能会导致库存积压,因为这是乐观锁的形式,如果较多的失败,可能会导致,库存积压,因为本来就没有多少用户的话,加上用户失败,会导致东西卖不完。

    lua脚本:

    Lua 是一个小巧的脚本语言,Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,Lua并没有提供强大的库,一个完整的Lua解释器不过200k,所以Lua不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。lua也有自己的语法。

     调用的时候:

     先scriptLoad编译一下,然后evalsha调用,2表示两个参数。

    将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。但是注意redis的lua脚本功能,只有在2.6以上的版本才可以使用。

    通过lua脚本解决争抢问题,实际上是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。

  • 相关阅读:
    使用FolderBrowserDialog组件选择文件夹
    使用OpenFileDialog组件打开多个文
    使用OpenFileDialog组件打开对话框
    获取弹出对话框的相关返回值
    PAT 甲级 1139 First Contact (30 分)
    PAT 甲级 1139 First Contact (30 分)
    PAT 甲级 1138 Postorder Traversal (25 分)
    PAT 甲级 1138 Postorder Traversal (25 分)
    PAT 甲级 1137 Final Grading (25 分)
    PAT 甲级 1137 Final Grading (25 分)
  • 原文地址:https://www.cnblogs.com/chenmz1995/p/12578144.html
Copyright © 2011-2022 走看看