使用Redis事务与乐观锁、Lua脚本解决秒杀系统问题

该栏目会系统的介绍 Redis 的知识体系,共分为相关概念、操作指令、主从复制等模块


文章目录


事务简介

1、概述

  • :redis 事务是一个单独的隔离操作,就是用来串联多个命令防止别的命令插队

2、特性

  • 单独的隔离操作
  • 没有隔离级别
  • 不保证原子性

3、相关命令

功能 指令
组队 multi
执行 exec
取消组队 discard
监视 watch
取消监视 unwatch

秒杀案例

  • 连接超时问题:使用连接池解决
  • 超卖问题:使用事务和乐观锁
  • 遗留库存问题:使用Lua脚本
/**
 * 1、使用连接池解决超时问题
 * 2、使用事务与乐观锁解决超卖问题,但引入遗留库存问题,用Lua脚本解决
 *
 * @param productId 商品Id
 * @param userId    用户Id
 * @return 操作结果
 */
@Test
public boolean secKill(String productId, String userId) {
    if (productId == null || userId == null) {
        return false;
    }

    // 定义key
    String stockKey = "sk:" + productId + ":stock";
    String userKey = "sk:" + productId + ":user";

    // 判断秒杀是否开始
    final String stock = (String) stringOps.get(stockKey);
    if (stock == null) {
        System.out.println("秒杀活动还没开始,请等待");
        return false;
    }
    // 判断用户是否已经秒杀过
    final Boolean result = Optional.ofNullable(setOps.isMember(userKey, userId)).get();
    if (result) {
        System.out.println("用户已经参加过秒杀了");
        return false;
    }
    // 判断秒杀是否结束
    if (Integer.parseInt(stock) <= 0) {
        System.out.println("秒杀结束...");
        return false;
    }

    // 监视库存,使用乐观锁机制
    redisTemplate.watch(stockKey);

    // 开启事务
    redisTemplate.multi();
    stringOps.decrement(stockKey);
    setOps.add(userKey, userId);
    final List<Object> results = redisTemplate.exec();
    if (results.size() == 0) {
        System.out.println("秒杀失败");
        return false;
    }

    System.out.println("秒杀成功");
    return true;
}

/**
 * LUA脚本概述:将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接
 * redis的次数。提升性能。LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完
 * 成一些redis事务性的操作
 * 
 * 使用Lua脚本解决遗留库存问题
 *
 * @param productId 商品Id
 * @param userId    用户Id
 * @return 操作结果
 */
@Test
public boolean secKillScript(String productId, String userId) {
    String secKillScript = "local userid=KEYS[1];\r\n" +
        "local prodid=KEYS[2];\r\n" +
        "local qtkey='sk:'..prodid..\":qt\";\r\n" +
        "local usersKey='sk:'..prodid..\":usr\";\r\n" +
        "local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" +
        "if tonumber(userExists)==1 then \r\n" +
        "   return 2;\r\n" +
        "end\r\n" +
        "local num= redis.call(\"get\" ,qtkey);\r\n" +
        "if tonumber(num)<=0 then \r\n" +
        "   return 0;\r\n" +
        "else \r\n" +
        "   redis.call(\"decr\",qtkey);\r\n" +
        "   redis.call(\"sadd\",usersKey,userid);\r\n" +
        "end\r\n" +
        "return 1";

    // 定义key
    String stockKey = "sk:" + productId + ":stock";
    String userKey = "sk:" + userId + ":user";

    // 执行Lua脚本
    DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
    redisScript.setScriptText(secKillScript);
    redisScript.setResultType(Long.class);
    final long result = Optional.ofNullable(redisTemplate.execute(
    			redisScript, Arrays.asList(userId, productId))).get();
    if (0 == result) {
        System.out.println("秒杀活动结束");
        return false;
    } else if (1 == result) {
        System.out.println("秒杀成功");
        return true;
    } else if (2 == result) {
        System.out.println("用户已经参加过秒杀了");
        return false;
    } else {
        System.out.println("秒杀异常!");
        return false;
    }
    
}
上一篇:OpenResty限频限流


下一篇:[记录点滴] OpenResty中Redis操作总结