redis 7.乐观锁、悲观锁,Redis事务,秒杀案例,redisTemplate执行事务操作和LUA脚本


一.超卖问题

1.1 出现原因

  在多个用户同时发起对同一个商品的下单请求时,先查询商品库存,再修改商品库存,会出现资源竞争问题,导致商品被超卖。

在这里插入图片描述

1.2 乐观锁、悲观锁

1.2.1 悲观锁

  对于数据冲突,持一种悲观的态度,先以一种预防的姿态在修改数据之前加锁,然后再对数据进行读写,在它释放锁之前其他任何人不能对其数据进行操作,直到锁被释放。一般关系型数据库的锁机制都是基于悲观锁的,如oracle中select ... for update

特点:完全保证数据的独占性和正确性,但是加锁释放锁的过程会造成消耗性能不高

1.2.2 乐观锁

  对于数据冲突,持一种乐观态度,操作数据时不会对操作的数据进行加锁(这使得多个任务可并行操作数据),等到数据提交时再验证数据是否存在冲突(CAS 即 check-and-set 一般通过版本号对比的方式实现);

特点:乐观锁是一种并发类型的锁,本身不对数据进行加锁通而是通过业务实现锁的功能,省掉了对数据加锁和解锁的过程,可一定程度上提高性能。但是在并发非常高的情况下,CAS导致大部分操作无功而返而浪费资源,因此在高并发的场景下,乐观锁的性能反而不如悲观锁。

1.3 redis乐观锁

  MULTI 、 EXEC 、 DISCARD 和 WATCH 是 Redis 事务相关的命令;事务带有2个重要属性:
  事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

1.3.1 MULTI

   MULTI 执行之后, 客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行, 而是被放到一个队列中
在这里插入图片描述

1.3.2 EXEC

   exec命令负责触发并执行事务中的所有命令:
在这里插入图片描述

如果客户端在使用 MULTI 开启了一个事务之后,却因为断线而没有成功执行 EXEC ,那么事务中的所有命令都不会被执行;
如果客户端成功在开启事务之后执行 EXEC ,那么事务中的所有命令都会被执行。

1.3.3 事务中的错误

使用事务时可能会遇上以下两种错误:

   事务在执行 EXEC 之前,入队的命令可能会出错。比如,命令可能会产生语法错误(参数数量错误,参数名错误等),或者其他更严重的错误,比如内存不足(如果服务器使用 maxmemory 设置了最大内存限制的话)。从 Redis 2.6.5 开始,服务器会对命令入队失败的情况进行记录,并在客户端调用 EXEC 命令时,拒绝执行并自动放弃这个事务。
在这里插入图片描述

   命令可能在 EXEC 调用之后失败。如,事务中的命令可能处理了错误类型的键,比如将列表命令用在了字符串键上面。在 EXEC 命令执行之后所产生的错误, 并没有对它们进行特别处理: 即使事务中有某个/某些命令在执行时产生了错误, 事务中的其他命令仍然会继续执行。
在这里插入图片描述

1.3.4 DISCARD

   当执行 DISCARD 命令时, 事务会被放弃, 事务队列会被清空, 并且客户端会从事务状态中退出

在这里插入图片描述

1.3.5 WATCH

WATCH 命令为 Redis 事务提供 check-and-set (CAS)操作

   被 WATCH 的键会被监视是否被改动过了。 如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消。
客户端 1启动监听,并正在进行set操作:
在这里插入图片描述
客户端2同时修改键k11的值:
在这里插入图片描述
客户端1exec提交事务,执行失败
在这里插入图片描述

注:使用UNWATCH 命令可以手动取消对键的监视。

1.4 超卖问题解决

1.4.1 利用乐观锁淘汰用户,解决超卖问题:

在这里插入图片描述

@RestController
@RequestMapping("/redisTest")
public class RedisTestController {
    @Autowired
    private RedisTemplate redisTemplate;

    @PostMapping("/secKill")
    public boolean testRedis(String uid, String prodid) {
        /*模拟不同用户*/
        Random random = new Random();
        int i = random.nextInt(10000);
        uid = uid + i;
        //1 uid和prodid非空判断
        if (uid == null || prodid == null) {
            return false;
        }

        String kcKey = "sk:" + prodid + ":qt";
        String userKey = "sk:" + prodid + ":user";

        /*redisTemplate 开启事务支持*/
        redisTemplate.setEnableTransactionSupport(true);
        /*监视库存*/

        redisTemplate.watch(kcKey);
        String kc = redisTemplate.opsForValue().get(kcKey).toString();
        if (kc == null) {
            System.out.println("秒杀还没有开始,请等待");
            return false;
        }
        if (redisTemplate.opsForSet().isMember(userKey, uid)) {
            System.out.println("已经秒杀成功了,不能重复秒杀");
            return false;
        }
        if (Integer.parseInt(kc) <= 0) {
            System.out.println("秒杀已经结束了");
            return false;
        }
        redisTemplate.multi();
        redisTemplate.opsForValue().decrement(kcKey, 1);
        redisTemplate.opsForSet().add(userKey, uid);
        List exec = redisTemplate.exec();
        if (exec == null || exec.size() == 0) {
            System.out.println("秒杀失败,晚了一步,重新秒杀");
            return false;
        }
        return true;
    }
}

使用ab压测工具,执行命令:

ab -n 20000 -c 2000 -k -p ./postfile -T application/x-www-form-urlencoded http://192.168.0.109:8080/redisTest/secKill

在这里插入图片描述
结果发现:
在这里插入图片描述

注:

RedisTemplate 的默认配置不支持事务

(a) 在代码中若不添加 redisTemplate.setEnableTransactionSupport(true);则抛出
异常io.lettuce.core.RedisCommandExecutionException: ERR EXEC without MULTI

在这里插入图片描述
此时ab压测仍然发现超卖现象
在这里插入图片描述

(b) 更常见的写法仍是采用 RedisTemplate 的默认配置,通过使用 SessionCallback,该接口保证其内部所有操作都是在同一个Session中。

    SessionCallback<Object> callback = new SessionCallback<Object>() {
        @Override
        public Object execute(RedisOperations operations) throws DataAccessException {
            operations.multi();
             添加逻辑
            return operations.exec();
        }
    };
   redisTemplate.execute(callback));

二.库存遗留

2.1 库存遗留

  高并发情况下,CAS导致大部分请求失败。
在这里插入图片描述
2000个请求,500的库存,仍然有库存剩余
在这里插入图片描述

2.2 LUA脚本

  Lua 是一个小巧的脚本语言,Lua脚本可以被C/C++ 代码调用,也可以调用C/C++的函数,
LUA

2.2.1 redis中的LUA脚本

  将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。

  Redis 使用单个 Lua 解释器去运行所有脚本,保证脚本会以原子性(atomic)的方式执行: 当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。 这和使用 MULTI / EXEC 包围的事务很类似。 在其他别的客户端看来,脚本的效果(effect)要么是不可见的(not visible),要么就是已完成的(already completed)。 另一方面,这也意味着,执行一个运行缓慢的脚本并不是一个好主意。当不得不使用一些跑得比较慢的脚本时,其他客户端会因为服务器正忙而无法执行命令。

2.2.2 通过lua脚本解决秒杀问题

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

2.2.3 redisTemplate执行LUA脚本

@RestController
@RequestMapping("/redisTest")
public class RedisTestController {
    @Autowired
    private RedisTemplate redisTemplate;

    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";


    @PostMapping("/secKill")
    public boolean testRedis(String uid, String prodid) {
        /*模拟不同用户*/
        Random random = new Random();
        int i = random.nextInt(10000);
        uid = uid + i;
        //1 uid和prodid非空判断
        if (uid == null || prodid == null) {
            return false;
        }
        //调用lua脚本并执行
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(Long.class);//返回类型是Long
        redisScript.setScriptText(secKillScript);
        Object execute = redisTemplate.execute(redisScript, Arrays.asList(uid, prodid), "mypar");


        String reString = String.valueOf(execute);
        if ("0".equals(reString)) {
            System.err.println("已抢空!!");
            return false;
        } else if ("1".equals(reString)) {
            System.out.println("抢购成功!!!!");
            return true;
        } else if ("2".equals(reString)) {
            System.err.println("该用户已抢过!!");
            return false;
        } else {
            System.err.println("抢购异常!!");
            return false;
        }

    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

但行益事莫问前程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值