【5】Redis从入门到放弃---秒杀案例(Redis的事务+锁机制+lua脚本)

摘要:
Redis事务1.1 Redis事务的定义:Redis事务是一个单独的隔离操作:事务中的所有命令都将被序列化并顺序执行。Redis事务主要用于连接多个命令,以防止其他命令在队列中跳转。Redis使用这种检查和设置机制来实现事务。取消监视取消WATCH命令对所有密钥的监视。

(1)Redis的事务

1.1 Redis事务的定义:

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

1.2 Multi、Exec、discard命令

组队阶段:从输入multi命令开始,后面输入的任务命令都会依次放入到队列中,但不会执行;

执行阶段:及就是从输入exec开始,Redis会将之前的命令队列中的命令依次执行;

取消事务:只能在组队的过程中可以通过discard命令来放弃组队。

1.3 实操如下:

 场景一:组队成功,提交也成功

【5】Redis从入门到放弃---秒杀案例(Redis的事务+锁机制+lua脚本)第1张

 场景二:组队阶段报错,提交失败

【5】Redis从入门到放弃---秒杀案例(Redis的事务+锁机制+lua脚本)第2张

 场景三:组队成功,提交有成功有失败情况

【5】Redis从入门到放弃---秒杀案例(Redis的事务+锁机制+lua脚本)第3张

 1.4 Redis事务三特性

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

没有隔离级别的概念:队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行

不保证原子性 :事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

(2)Redis的事务锁机制

 2.1 Redis的锁机制

在实际业务中,有一些场景例如:秒杀、抢车票等等,同一时间多个请求进来,那可能就会存在超卖现象,针对这种情况我们可以使用事务和redis的锁机制来解决这种问题。

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

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

2.2 watch和unwatch的命令

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

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

http://doc.redisfans.com/transaction/exec.html

(3)秒杀案例

  1. 使用Redis解决计数器和人员记录的事务操作
  2. 模拟:单个请求到并发秒杀(使用工具JMeter模拟测试)
  3. 超卖问题:利用事务和乐观锁淘汰用户,解决超卖问题
  4. 模拟:加大库存,会存在秒杀结束却还有库存
  5. 使用LUA脚本解决库存剩余问题

1.使用Redis解决计数器和人员记录的事务操作

写个秒杀测试类如下:

/**
 * 秒杀案例,一个用户只能秒杀成功一次
 */
@RestController
@RequestMapping("/testRedisSeckill")
public class TestRedisSeckillController {

    @Autowired
    private RedisTemplate redisTemplate;


    @GetMapping("/doSeckill")
    public boolean doSeckill() throws IOException {
        String usrId = new Random().nextInt(50000) + "";
        return doSeckillFun(usrId, "20210731");
    }
}
/**
     * 秒杀过程1(高并发下会超卖)
     *
     * @param usrId 用户id
     * @param atcId 活动id
     * @return
     * @throws IOException
     */
    private boolean doSeckillFun(String usrId, String atcId) throws IOException {
        //1.参数校验
        if (usrId == null || atcId == null) return false;

        //2.设置Redis值(库存key== atcId:stock, 秒杀成功用户key== atcId:userId)
        String stockKey = atcId + ":stock";
        String userIdKey = atcId + ":userId";

        //3.获取库存,如果库存是空,秒杀还没开始
        Object stock = redisTemplate.opsForValue().get(stockKey);//获取库存
        if (stock == null) {
            System.out.println("别着急,秒杀还没开始呢!!");
            return false;
        }

        //4.判断用户是否重复秒杀操作(Set类型操作)
        if (redisTemplate.opsForSet().isMember(userIdKey, usrId)) {
            System.out.println("你已经秒杀成功了,不能重复秒杀");
            return false;
        }

        //5.判断库存数量,小于1,秒杀结束
        int stock1 = (int) stock;
        if (stock1 < 1) {
            System.out.println("秒杀结束了。。。");
            return false;
        }

        //6.秒杀过程(库存减1,把秒杀成功用户添加到用户清单)
        redisTemplate.opsForSet().add(userIdKey, usrId);
        redisTemplate.opsForValue().decrement(stockKey);//库存-1
        System.out.println("恭喜你!秒杀成功了!");
        return true;
    }

1.1 模拟场景1:单个请求

先设置库存10个

【5】Redis从入门到放弃---秒杀案例(Redis的事务+锁机制+lua脚本)第4张

 Jmeter模拟单个请求

【5】Redis从入门到放弃---秒杀案例(Redis的事务+锁机制+lua脚本)第5张

 【5】Redis从入门到放弃---秒杀案例(Redis的事务+锁机制+lua脚本)第6张

【5】Redis从入门到放弃---秒杀案例(Redis的事务+锁机制+lua脚本)第7张

 查看redis剩余库存和用户清单:

【5】Redis从入门到放弃---秒杀案例(Redis的事务+锁机制+lua脚本)第8张

 1.2 模拟高并发500个请求

看着没什么毛病对吧,那如果我把并发加大到500,会出现什么情况呢?

【5】Redis从入门到放弃---秒杀案例(Redis的事务+锁机制+lua脚本)第9张

在执行之前先清空redis的数据,点击jmeter执行

【5】Redis从入门到放弃---秒杀案例(Redis的事务+锁机制+lua脚本)第10张

 控制台输出:

【5】Redis从入门到放弃---秒杀案例(Redis的事务+锁机制+lua脚本)第11张

 查看redis数据

 【5】Redis从入门到放弃---秒杀案例(Redis的事务+锁机制+lua脚本)第12张

 发现库存 -190,出现超卖了,所以我们的场景1的代码在高并发的情况下会出现超卖的问题,那么针对这个问题我们需要使用乐观锁来解决

2.使用乐观锁来解决

代码如下:

/**
     * 秒杀过程2(乐观锁解决超卖问题)
     *
     * @param usrId 用户id
     * @param atcId 活动id
     * @return
     * @throws IOException
     */
    private boolean doSeckillFun(String usrId, String atcId) throws IOException {
        //1.参数校验
        if (usrId == null || atcId == null) return false;

        //2.设置Redis值(库存key== atcId:stock, 秒杀成功用户key== atcId:userId)
        String stockKey = atcId + ":stock";
        String userIdKey = atcId + ":userId";

        //通过 SessionCallback,保证所有的操作都在同一个 Session 中完成
        //更常见的写法仍是采用 RedisTemplate 的默认配置,即不开启事务支持。
        // 但是,我们可以通过使用 SessionCallback,该接口保证其内部所有操作都是在同一个Session中
        SessionCallback<Object> callback = new SessionCallback<Object>() {

            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                //3. 打开事务支持
                //redisTemplate.setEnableTransactionSupport(true);

                //4.增加乐观锁进行对库存的监视
                operations.watch(stockKey);

                //5.获取库存,如果库存是空,秒杀还没开始
                Object stock = operations.opsForValue().get(stockKey);//获取库存
                if (stock == null) {
                    System.out.println("别着急,秒杀还没开始呢!!");
                    return false;
                }

                //6.判断用户是否重复秒杀操作(Set类型操作)
                if (operations.opsForSet().isMember(userIdKey, usrId)) {
                    System.out.println("你已经秒杀成功了,不能重复秒杀");
                    return false;
                }

                //7.判断库存数量,小于1,秒杀结束
                int stock1 = (int) stock;
                if (stock1 < 1) {
                    System.out.println("秒杀结束了。。。");
                    return false;
                }

                //8. 增加事务
                operations.multi();

                //9.秒杀过程
                operations.opsForValue().decrement(stockKey);//库存-1
                operations.opsForSet().add(userIdKey, usrId);//把秒杀成功用户添加到用户清单

                //10.执行事务
                List<Object> list = operations.exec();

                //11.判断事务提交是否失败
                if (list == null || list.size() == 0) {
                    System.out.println("秒杀失败");
                    return false;
                }

                System.out.println("恭喜你!秒杀成功了!");
                return true;
            }
        };

        return (boolean) redisTemplate.execute(callback);
    }

【5】Redis从入门到放弃---秒杀案例(Redis的事务+锁机制+lua脚本)第13张

 2.1 设置10个库存,继续模拟500个并发请求,结果如下:

【5】Redis从入门到放弃---秒杀案例(Redis的事务+锁机制+lua脚本)第14张

 【5】Redis从入门到放弃---秒杀案例(Redis的事务+锁机制+lua脚本)第15张

 终于解决超卖问题了,嘻嘻

 2.2 那我把库存加大到300个,继续模拟500个并发请求,会出现什么情况呢?

【5】Redis从入门到放弃---秒杀案例(Redis的事务+锁机制+lua脚本)第16张

 执行Jmeter模拟500个并发,结果如下:

【5】Redis从入门到放弃---秒杀案例(Redis的事务+锁机制+lua脚本)第17张

 虽然没有超卖问题了,但是有500个请求却还剩余102个库存,那么就有下边的lua解决库存遗留问题。

3. LUA脚本在Redis中的优势 :将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。 LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。 但是注意redis的lua脚本功能,只有在Redis 2.6 以上的版本才可以使用。 利用lua脚本淘汰用户,解决超卖问题。 redis 2.6版本以后,通过lua脚本解决争抢问题,实际上是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。

使用Lua脚本解决库存遗留的问题,代码如下:

 /**
     * 秒杀过程3(LUA解决库存剩余问题)
     *
     * @param usrId 用户id
     * @param atcId 活动id
     * @return
     * @throws IOException
     */
    private boolean doSeckillFun(String usrId, String atcId) throws IOException {

        String luaScript = "local userId=KEYS[1];
" +
                "local stockKey=KEYS[2];
" +
                "local userIdKey=KEYS[3];
" +
                "local userExists=redis.call("sismember",userIdKey,userId); 
" +
                "if tonumber(userExists)==1 
" +
                "then 
" +
                "  return 2;
" +
                "end 
" +
                "local num= redis.call("get" ,stockKey);
" +
                "if tonumber(num)<=0 then   return 0;
" +
                "else 
 " +
                " redis.call("decr",stockKey);
" +
                "redis.call("sadd",userIdKey,userId);
" +
                "end 
" +
                "return 1;";

        // 指定 lua 脚本,并且指定返回值类型
        // (为什么返回值不用 Integer 接收而是用 Long。这里是因为 spring-boot-starter-data-redis 提供的返回类型里面不支持 Integer。)
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
        List<String> keys = new ArrayList<>();
        keys.add(usrId);
        keys.add(atcId + ":stock");
        keys.add(atcId + ":userId");
        // 参数一:redisScript,参数二:key列表,参数三:arg(可多个)
        Long result = (Long) redisTemplate.execute(redisScript, keys);
        if (0 == result) {
            System.out.println("秒杀结束了。。。");
        } else if (1 == result) {
            System.out.println("恭喜你!秒杀成功了!");
            return true;
        } else if (2 == result) {
            System.out.println("你已经秒杀成功了,不能重复秒杀");
        } else {
            System.out.println("秒杀异常啦~");
        }

        return false;
    }

lua脚本:

local userId=KEYS[1];
local stockKey=KEYS[2];
local userIdKey=KEYS[3];
local userExists=redis.call("sismember",userIdKey,userId); 
if tonumber(userExists)==1
then
  return 2;
end
local num= redis.call("get" ,stockKey);
if tonumber(num)<=0 then   return 0;
else
 redis.call("decr",stockKey);
redis.call("sadd",userIdKey,userId);
end
return 1;

这样一个完美的秒杀案例就完成了。嘻嘻嘻~~~~

免责声明:文章转载自《【5】Redis从入门到放弃---秒杀案例(Redis的事务+锁机制+lua脚本)》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇jquery 改变标签样式Linux权限问题(2)-unzip引发的权限问题下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

js Base64与字符串互转

1、base64加密 在页面中引入base64.js文件,调用方法为: <!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <title>base64加密</title> <script type="text/jav...

腾讯蓝鲸cmdb部署

腾讯蓝鲸CMDB项目地址: https://github.com/Tencent/bk-cmdb 蓝鲸配置平台 (CMDB)http://172.16.6.10:8088 环境(单机测试): Centos6 16G 200G 依赖环境: Java 1.8.0_92 python 2.7 ZooKeeper 3.4.12 Redis 3.2.11 M...

Java经典练习题_Day03

一、选择 B D(死循环) E(switch) BC 二、编程 1、读入一个整数,表示一个人的年龄。 如果小于6岁,则输出“儿童”, 6岁到13岁,输出“少儿”; 14岁到18岁,输出“青少年”; 18岁到35岁,输出“青年”; 35岁到50岁,输出“中年”; 50 岁以上输出“中老年”。 import java.util.*; /* * 读入一个整数...

YAML简述

一、基础 YAML(Yet Another Markup Language),是一个JSON的超集,意味着任何有效JSON文件也都是一个YAML文件。它规则如下: 1)大小写敏感 2)使用缩进表示层级关系,但不支持tab缩进,只支持空格 3)缩进的数量不重要但至少一个空格,只要相同层级使用相同数量的空格即可 4)“#”表示注释,从这个字符开始,直到行末,都...

实战MEF(5):导出元数据

如何理解元数 我们可以把元数据理解为随类型一起导出的附加信息。有时候我们会考虑,把元数据随类型一并导出,增加一些说明,使得我们在导入的时候,可以多一些筛选条件。 默认的类型导出带有元数据吗 上面的内容我说得比较简洁,也许您不是很理解,不要紧,在编程里面,很多东西我们都是写了代码后才理解的。所以,我的理论功底比较差,最不擅长的就是长篇大论,还是从代码中看吧。...

Spark学习进度11-Spark Streaming&amp;amp;Structured Streaming

Spark Streaming Spark Streaming 介绍 批量计算  流计算 Spark Streaming 入门  Netcat 的使用  项目实例 目标:使用 Spark Streaming 程序和 Socket server 进行交互, 从 Server 处获取实时传输过来的字符串, 拆开单词并统计单词数量, 最后打印出来每一个小批...