问题场景

比如一个秒杀业务,我们需要判断用户是否有优惠券、是否已经参与过秒杀、库存是否足够、扣减库存、插入订单号业务。上述每一步都需要操作DB,这样的接口性能一定跟不上。

解决办法原理:

添加库存,String类型即可

用户获取优惠券时:添加一个set类型,用于保存所有的优惠券。

lua代码执行:判断用户是否有优惠券、是否参与秒杀、库存是否足够由set集合负责,扣件库存由redis的阻塞队列操作。

开启线程任务,不断从阻塞队列获取信息,实现真实DB下单功能

解决办法实操

添加库存代码

    @Test
    public void 添加库存() {
        int activityId = 10086;
        int activityAmount = 1000;
        redisTemplate.opsForValue().set("秒杀业务:库存", activityAmount);
        System.out.println("添加成功!");
    }
    @Test
    public void 添加用户的秒杀资格() {
        Integer userAccount = 740969606;
        String redisKey = "秒杀业务:优惠券用户列表";
        // 判断用户是否添加过优惠券
        Boolean isMember = redisTemplate.opsForSet().isMember(redisKey, userAccount);
        if (Boolean.FALSE.equals(isMember)) {
            // 添加用户信息进入列表
            Long add = redisTemplate.opsForSet().add(redisKey, userAccount);
            System.out.println("添加成功");
        } else {
            System.out.println("用户已经拥有资格");
        }
    }

校验秒杀资格lua脚本seckill.lua

local userAccount = ARGV[1]
local key1 = "秒杀业务:库存"
local key2 = "秒杀业务:优惠券用户列表"
local key3 = "秒杀业务:用户下单列表"
-- 校验库存是否足够
local kucun = redis.call('get',key1)
if tonumber(kucun) > 0 then
    -- 判断用户是否有购买资格
    if redis.call('sismember',key2,userAccount) == 1 then
        -- 判断用户是否下过单
        if redis.call('sismember',key3,userAccount) == 0 then
            -- 扣除库存
            redis.call('incrby',key1,-1)
            redis.call('sadd',key3,userAccount)
            return 1
        else
            return 0 -- 下单失败:用户不可重复下单
        end
    else
        return -2 -- 用户没有资格
    end
else
    return -1 -- 库存不足
end

异步线程 真实下单 (下面代码不得使用Junit测试)

定义线程池threadPools、阻塞队列绑定线程池与调用orders、初始化lua脚本killLuaScript、全局代理对象currentProxy(避免线程池产生子线程无法获取Spring管理的对象)

    @Autowired
    RedisTemplate redisTemplate;

    private Object currentProxy;
    public static final DefaultRedisScript killLuaScript;

    static {
        killLuaScript = new DefaultRedisScript();
        killLuaScript.setLocation(new ClassPathResource("seckill.lua")); // 指定脚本文件路径
        killLuaScript.setResultType(Long.class); // 指定脚本返回值类型
    }

    // 创建一个阻塞队列
    private BlockingQueue<HashMap> orders = new ArrayBlockingQueue<>(1024 * 1024);

    // 创建线程池
    private static final ExecutorService threadPools = Executors.newSingleThreadExecutor();

    // 交由Spring管理
    @PostConstruct
    private void init() {
        threadPools.submit(new DIYHandle());
    }

    private class DIYHandle implements Runnable {
        @Override
        public void run() {
            while (true) {
                HashMap take = null;
                try {
                    take = orders.take();
                    // 从全局变量中获取代理对象。使用Spring代理对象调用真实下单业务保证事务!我这个测试类叫RedisTemplateTests
                    UserService xxService = (UserService) currentProxy;
                    xxService.doRealOrder(take);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    // 处理订单的类
    @Transactional
    public void doRealOrder(HashMap hashMap) {
        System.out.println("真实下单业务执行了:" + hashMap.toString());
    }


    @Override
    public String seckill() {
        TimeInterval timer = DateUtil.timer();
        Integer userAccount = 740969606;
        // 调用Lua,校验用户下单资格
        Object executeRes = redisTemplate.opsForValue().getOperations().execute(killLuaScript, Collections.emptyList(), userAccount);
        if ((Long) executeRes == 1L) {
            // 将用户下单信息保存到阻塞队列中,方便处理数据
            HashMap<String, Object> hashMap = new HashMap();
            hashMap.put("orderId", UUID.randomUUID());
            boolean add = orders.add(hashMap);
            // 由于子线程不受主线程的事务控制,我们得手动获取为全局对象,以便于新开的子线程受到Spring事务的控制。Junit无法获取代理对象,下面会报错。真实Service调用没问题,必须删除下一行的注解
            //currentProxy = AopContext.currentProxy();
            System.out.println("下单成功!");
        } else {
            System.out.println("下单失败!");
        }
        System.out.println("消耗了:" + timer.interval());
        return timer.interval()+"";
    }

测试

1000个线程不停请求,吞吐量每秒10000,平均延迟100ms

Power By Mac M1 Pro

特殊说明:
上述文章均是作者实际操作后产出。烦请各位,请勿直接盗用!转载记得标注原文链接:www.zanglikun.com
第三方平台不会及时更新本文最新内容。如果发现本文资料不全,可访问本人的Java博客搜索:标题关键字。以获取全部资料 ❤