文章目录

  • 一、事务
  • 1.1 Multi、Exec、Discard
  • 1.2 事务错误处理
  • 1.3 事务冲突(乐观锁、悲观锁)
  • 1.4 事务三特性
  • 1.5 模拟秒杀案例
  • 1.5.1 超售、超时问题
  • 1.5.2 库存遗留问题


一、事务

Redis 事务是隔离操作,所有命令都会序列化 、顺序执行,不会被其他客户端发送的命令打断。

主要作用:串联多个命令,防止插队。

1.1 Multi、Exec、Discard

首先输入 Multi 命令开启事务,所有命令都会进入队列且不执行(组队阶段),直到输入 Exec 后命令依次执行(运行阶段)。

组队阶段可通过 discard 放弃组队,即销毁队列。

redistemplate 阻塞队列 list 批量 redis阻塞队列原理_乐观锁


redistemplate 阻塞队列 list 批量 redis阻塞队列原理_System_02

1.2 事务错误处理

组队阶段:命令错误,则整个队列取消。

执行阶段:命令错误,只是当前命令不执行,其他命令正常执行,不回滚

redistemplate 阻塞队列 list 批量 redis阻塞队列原理_System_03


redistemplate 阻塞队列 list 批量 redis阻塞队列原理_库存遗留_04

1.3 事务冲突(乐观锁、悲观锁)

悲观锁:每次拿数据都会加锁,这样别人来了就会阻塞直到我释放锁。
乐观锁:每次更新数据时判断在此期间是否有人修改过数据,如果没有则更新,否则不操作(适用于多读场景)。

下图,都监视 cost ,首先对 cost + 1,然后再 + 2。但是只有 + 1成功执行,因为 + 1执行后版本改变,+ 2 时检测到版本改变,不操作。

redistemplate 阻塞队列 list 批量 redis阻塞队列原理_超售问题_05


redistemplate 阻塞队列 list 批量 redis阻塞队列原理_乐观锁_06

1.4 事务三特性

1、隔离操作

事务中命令被序列化顺序执行,执行过程中不会被其他命令打断。

2、没有隔离级别

队列中命令没提交前,都不会执行。

3、不保证原子性

事务中一条命令失败,不影响其他命令执行,不回滚。

1.5 模拟秒杀案例

案例介绍:有 n 个商品,给 m 个用户抢购,公布抢中者名单。

@RestController
public class RedisController {

    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping("/go")
    public String testRedis() {
        String userId = new Random().nextInt(50000) + "";
        String productId = "plane-10086";

        // 秒杀过程
        if (userId == null || productId == null) {
            return "秒杀信息错误";
        }

        // 用户 key
        String userKey = "good:" + productId + ":" + userId;

        // 获取库存
        int remain = (int) redisTemplate.opsForValue().get(productId);
        if (remain == 0) {
            return "秒杀未开始";
        }

        // 判断用户是否重复秒杀
        Boolean member = redisTemplate.opsForSet().isMember(userKey, productId);
        if (member) {
            return "已秒杀成功,不可重复秒杀";
        }

        // 库存小于1,秒杀结束
        if (remain <= 0) {
            return "库存不足";
        }

        // 库存-1,用户加入名单
        redisTemplate.opsForValue().decrement(productId);
        redisTemplate.opsForSet().add(userKey, productId);
        
        return "秒杀成功:" + userKey;
    }
}

使用 ab 模拟并发场景:

redistemplate 阻塞队列 list 批量 redis阻塞队列原理_乐观锁_07


redistemplate 阻塞队列 list 批量 redis阻塞队列原理_redis_08


高并发场景下,库存变为了负数,发生超售问题,而且并发线程加大,可能导致 redis 无法处理更多的请求,请求一直等待,出现超时问题

1.5.1 超售、超时问题

超时问题:开启连接池

spring:
  redis:
    host: 175.27.243.243
    port: 6379
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5
        max-wait: 100000ms

超售问题:乐观锁

package com.sugar.redis6.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Random;

@RestController
public class RedisController {

    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping("/go")
    public String Ms() {

        String userId = new Random().nextInt(50000) + "";
        String productId = "plane";
        //set good:plane 10
        // 用户 key 商品 key
        String userKey = "user:" + userId;
        String prodKey = "good:" + productId;

        SessionCallback sessionCallback = new SessionCallback<List>() {
            @Override
            public List execute(RedisOperations operations) throws DataAccessException {
                // 监视库存
                redisTemplate.watch(prodKey);

                // 获取库存, 若库存为0, 表示秒杀未开始
                int remain = (int)redisTemplate.opsForValue().get(prodKey);
                if (remain == 0) {
                    System.out.println("秒杀未开始");
                    return null;
                }

                // 判断库存
                if ((int)redisTemplate.opsForValue().get(prodKey) < 1) {
                    System.out.println("库存不足啦");
                    return null;
                }

                // 判断用户重复秒杀
                Boolean member = redisTemplate.opsForSet().isMember(userKey, prodKey);
                if (member) {
                    System.out.println("重复秒杀");
                    return null;
                }

                // 开始事务, 抢购开始
                redisTemplate.multi();
                redisTemplate.opsForValue().decrement(prodKey);
                redisTemplate.opsForSet().add(userKey,prodKey);

                return redisTemplate.exec();
            }
        };

        List<Object> result = (List<Object>) redisTemplate.execute(sessionCallback);

        if (result != null && result.size() != 0) {
            System.out.println("秒杀成功" + userKey);
            System.out.println("剩余库存:" + redisTemplate.opsForValue().get(prodKey));
        }else {
            System.out.println("秒杀失败");
        }

        return null;
    }
}

使用 Jmeter 测压

redistemplate 阻塞队列 list 批量 redis阻塞队列原理_超售问题_09


redistemplate 阻塞队列 list 批量 redis阻塞队列原理_redis_10


我们发现,100 个线程并发抢购 10 个商品,成功抢购者 6 人。的确解决了超售问题

但是还有一个问题。100 个人来抢个,只抢购了 6 个商品,这合理吗???这不合理,这就是库存遗留问题

1.5.2 库存遗留问题

通过 lua 脚本解决抢占问题,本质是利用 redis 的单线程特性,用任务队列解决多任务并发问题

其实可以发现,本来是一堆 redis 命令顺序执行,采用 lua 后,只需只需一次,减少网络开销,而且具有原子性

local userId=KEYS[1];
local productId=KEYS[2];
local userKey='user:' .. userId;
local prodKey='good:' .. productId;
local userExists=redis.call("sismember",userKey,prodKey);
if tonumber(userExists)==1 then
    return 2;
end
local num=redis.call("get",prodKey);
if tonumber(num)<=0 then
    return 0;
else
    redis.call("decr",prodKey);
    redis.call("sadd",userKey,prodKey);
end
return 1;
DefaultRedisScript redisScript = new DefaultRedisScript();
        redisScript.setLocation(new ClassPathResource("redis.lua"));
        redisScript.setResultType(Long.class);
        List<String> list = new ArrayList<>();
        list.add(userId);
        list.add(productId);

        Object result = redisTemplate.execute(redisScript, list);

        switch (result.toString()) {
            case "1":
                System.out.println("秒杀成功");
                System.out.println("库存剩余" + redisTemplate.opsForValue().get(prodKey));
            case "2":
                System.out.println("重复抢购");
            case "0":
                System.out.println("抢购失败");
        }