目录

一、实现步骤:

1、新增秒杀优惠券的同时,将优惠券信息保存到Redis中

2、基于Lua脚本,判断秒杀库存,一人一单,决定用户是否抢购成功,成功将消息推送到消息队列

​编辑3、 初始化lua脚本,主线程中初始化代理对象:用于后面使用事务不会失效 

4、主要是开启线程任务,不断从消息队列中获取信息,实现异步下单功能,下单功能在下一步

5、真正下单业务,可以不使用分布锁,在lua脚本中已经判断过了,高并发900个token,200个秒杀券清0

二、整体流程:

三、jmeter高并发测压:

1、生产数据:

2、jmeter测试配置:


一、实现步骤

1、新增秒杀优惠券的同时,将优惠券信息保存到Redis

数据下图所示: 

2、基于Lua脚本判断秒杀库存,一人一单,决定用户是否抢购成功,成功将消息推送到消息队列

---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by用户已成仙.
--- DateTime: 2023/1/11 21:55
---
-- 1.参数列表
-- 1.1.优惠券id用户id订单id
local voucherId = ARGV[1]
local userId = ARGV[2]
local orderId = ARGV[3]

-- 2.数据key
-- 2.1.库存key、订单key
local stockKey = 'seckill:stock:' .. voucherId
local orderKey = 'seckill:order:' .. voucherId

-- 3.脚本业务
-- 3.1.判断库存是否充足 get stockKey库存不足,返回1
if(tonumber(redis.call('get', stockKey)) <= 0) then
    return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId,redis存在该用户,说明重复下单返回2
if(redis.call('sismember', orderKey, userId) == 1) then
    return 2
end
-- !!! 执行到这说明可以正常下单直接返回订单号给用户,将消息推送给Stream消息队列
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
-- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0

下单两次后, redis查看存储数据

seckill:order:3

seckill:stock:3

3、 初始化lua脚本,主线程初始化代理对象用于后面使用事务不会失效 

    // 初始化执行lua脚本
    private static final DefaultRedisScript<Long&gt; SECKILL_SCRIPT;

    static {
        SECKILL_SCRIPT = new DefaultRedisScript<&gt;();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
        SECKILL_SCRIPT.setResultType(Long.class);
    }
    private IVoucherOrderService proxy;
    @Override
    public Result asynSeckillVoucher(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        // 1.执行lua脚本获取结果
        long orderId = redisIdWorker.nextId("order");
        Long res = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(), userId.toString(), String.valueOf(orderId));
        // 2.对结果进行判断,0:下单成功   1:库存不足   2:同一用户不能重复下单
        // 类型转换一下,避免出现问题

        int r = res.intValue();
        if(r != 0){
            return Result.fail(r == 1? "库存不足": "同一用户不能重复下单");
        }
        // 3.订单信息已经放入基于Stream消息队列中

        // 4.另开线程处理订单信息,下面
        // 4.2.4 在主线程获取代理对象注入
        proxy = (IVoucherOrderService) AopContext.currentProxy();
        return Result.ok(orderId);
    }

4、主要是开启线程任务,不断从消息队列获取信息,实现异步下单功能下单功能在下一步

注意:这个过程可能出现消息队列存在,需提前创建好,代码如下

    // 4.1 使用线程池
    private static final ExecutorService EXECUTORSERVICE = Executors.newSingleThreadExecutor();

    // 4.2 类初始化完成后执行
    @PostConstruct
    private void init(){
        // 这里可以使用lambda表达式
        EXECUTORSERVICE.submit(new HandleOrderTask());
    }
    // 4.2.1 实现Runnable接口
    public class HandleOrderTask implements Runnable{
        String queueName = "stream.orders";
        @Override
        public void run() {
            while (true){
                // try出现异常,在ACK确认过程出现异常就是确认,将把该消息放入pending-list中重新ACK
                try {
                    // 根据控制报错队列存在,就先创建
                    initStreamQueue();
                    // 4.2.2 获取基于Stream消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS stream.orders >
                    List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                            Consumer.from("g1", "c1"),
                            StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
                            StreamOffset.create(queueName, ReadOffset.lastConsumed())
                    );
                    // 4.2.3 判断消息获取是否成功
                    if(list == null || list.isEmpty()){
                        // 获取失败说明没有消息,继续下一次循环
                        continue;
                    }
                    // 4.2.4 获取成功,下单消息, 解析消息,封装成VoucherOrder信息,进行下单
                    MapRecord<String, Object, Object> entries = list.get(0);
                    Map<Object, Object> map = entries.getValue();
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(map, new VoucherOrder(), true);
                    // 下单业务
                    proxy.asyncCreateVoucherOrder(voucherOrder);
                    // 4.2.5 ACk确认
                    stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", entries.getId());
                } catch (Exception e) {
                    log.error("异步线程处理订单异常:"+e.getMessage());
                    // 处理pending-list的数据
                    handlePendingList();
                }
            }
        }

        // 创建消息队列名称与组名称,不然会报错
        private void initStreamQueue() {
            Boolean exists = stringRedisTemplate.hasKey(queueName);
            if (BooleanUtil.isFalse(exists)) {
                log.info("基于stream消息队列名称不存在,开始创建消息队列名称:" + queueName);
                // 不存在,需要创建
                stringRedisTemplate.opsForStream().createGroup(queueName, ReadOffset.latest(), "g1");
                log.info("队列名称:"+queueName + " 与group:g1创建完毕");
                return;
            }
            // stream存在,判断group是否存在
            StreamInfo.XInfoGroups groups = stringRedisTemplate.opsForStream().groups(queueName);
            if(groups.isEmpty()){
                log.info("group不存在,开始创建group");
                // group不存在,创建group
                stringRedisTemplate.opsForStream().createGroup(queueName, ReadOffset.latest(), "g1");
                log.info("group创建完毕");
            }
        }

        // 出现异常的消息可能未被确认,就会被放到pending-list中,所以需在pending-list二次处理
        private void handlePendingList() {
            while (true){
                try {
                    // 4.2.2 获取pending-list的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 STREAMS streams.order 0
                    // 0 代表从第一条开始读
                    List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                            Consumer.from("g1", "c1"),
                            StreamReadOptions.empty().count(1),
                            StreamOffset.create(queueName, ReadOffset.from("0"))
                    );
                    // 4.2.3 判断消息获取是否成功
                    if(list == null || list.isEmpty()){
                        // 获取失败说明pending-list没有异常消息,结束循环
                        break;
                    }
                    // 4.2.4 获取成功,重新下单消息
                    // 解析消息,封装成VoucherOrder信息,进行下单
                    MapRecord<String, Object, Object> entries = list.get(0);
                    Map<Object, Object> map = entries.getValue();
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(map, new VoucherOrder(), true);
                    // ---处理订单信息
                    proxy.asyncCreateVoucherOrder(voucherOrder);
                    // 4.2.5 ACk确认
                    stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", entries.getId());
                } catch (Exception e) {
                    // 预防 pending-list中出现异常,但不处理,继续循环处理订单信息
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException ex) {
                        ex.printStackTrace();
                    }
                }
            }
        }
    }

5、真正下单业务可以使用分布锁,在lua脚本中已经判断过了,高并发900个token,200个秒杀券清0

    @Override
    @Transactional
    public void asyncCreateVoucherOrder(VoucherOrder voucherOrder) {
        Long userId = voucherOrder.getUserId();
        Long voucherId = voucherOrder.getVoucherId();
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        if(count > 0){
            // 用户已经购买过
            log.error("用户已经购买过一次!");
            return;
        }

        // 5、扣减库存(使用乐观锁思想,修改时判断是否被修改过)
        boolean isSuccess = seckillVoucherService.update().setSql("stock = stock - 1")
                .eq("voucher_id", voucherId).gt("stock", 0).update();
        if(!isSuccess){
            log.error("库存不足,抢购失败!");
            return;
        }
        save(voucherOrder);
    }

二、整体流程

三、jmeter高并发测压:

1、生产数据:

 代码生成

注意:这里针对黑马点评,其他的也可以改过去,Result返回值返回code,不然下面代码报错,用户id需连续

@SpringBootTest
@AutoConfigureMockMvc
class HmDianPingApplicationTests {
    
    @Resource
    private IUserService userService;
    
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private UserServiceImpl userServiceI;
    @Test
    public void createUserBy1000(){
        List<String> phones = RandomPhoneNumber.randomCreatePhone(1000);
        phones.stream().forEach(phone -> {
            if(!RegexUtils.isPhoneInvalid(phone)){
                User login_user = new User();
                login_user.setPhone(phone);
                login_user.setCreateTime(LocalDateTime.now());
                login_user.setUpdateTime(LocalDateTime.now());
                String nickName_suf = RandomUtil.randomString(10);
                login_user.setNickName(UserConstans.User_Pre + nickName_suf);
                userServiceI.save(login_user);
            }
        });
    }

    @Test
    public void tokenBy1000() throws Exception {
        String phone = "";
        String code = "";
        //注意!这里的绝对路径设置自己想要的地方
        OutputStreamWriter osw = null;
        osw = new OutputStreamWriter(new FileOutputStream("D:\token.txt"));
        //先模拟10个用户的登录
        for (int i = 1; i < 1000; i++) {
            User user = userService.getById(i);
            phone = user.getPhone();
            //创建虚拟请求模拟通过手机号发送验证码
            ResultActions perform1 = mockMvc.perform(MockMvcRequestBuilders
                    .post("/user/code?phone=" + phone));
            //获得Response的body信息
            String resultJson1 = perform1.andReturn().getResponse().getContentAsString();
            //将结果转换result对象
            Result result = JSONUtil.toBean(resultJson1, Result.class);
            //获得验证code = result.getData().toString();
            //创建登录表单
            LoginFormDTO loginFormDTO = new LoginFormDTO();
            loginFormDTO.setCode(code);
            loginFormDTO.setPhone(phone);
            //将表转换json格式字符串
            String loginFormDtoJson = JSONUtil.toJsonStr(loginFormDTO);
            //创建虚拟请求模拟登录
            ResultActions perform2 = mockMvc.perform(MockMvcRequestBuilders.post("/user/login")
                    //设置contentType表示json信息
                    .contentType(MediaType.APPLICATION_JSON)
                    //放入json对象
                    .content(loginFormDtoJson));
            String resultJson2 = perform2.andReturn().getResponse().getContentAsString();
            Result result2 = JSONUtil.toBean(resultJson2, Result.class);
            //获得token
            String token = result2.getData().toString();
            //写入
            osw.write(token+"n");
        }
        //关闭输出osw.close();
    }
}

RandomPhoneNumber 如下

package com.hmdp.utils;

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

public class RandomPhoneNumber {

    //中国移动
    public static final String[] CHINA_MOBILE = {

            "134", "135", "136", "137", "138", "139", "150", "151", "152", "157", "158", "159",
            "182", "183", "184", "187", "188", "178", "147", "172", "198"
    };
    //中国联通
    public static final String[] CHINA_UNICOM = {

            "130", "131", "132", "145", "155", "156", "166", "171", "175", "176", "185", "186", "166"
    };
    //中国电信
    public static final String[] CHINA_TELECOME = {

            "133", "149", "153", "173", "177", "180", "181", "189", "199"
    };

    /**
     * 生成手机号 * @param op 0 移动 1 联通 2 电信
     */
    public static String createMobile(int op) {

        StringBuilder sb = new StringBuilder();
        Random random = new Random();
        String mobile01;//手机号前三位
        int temp;
        switch (op) {
            case 0:
                mobile01 = CHINA_MOBILE[random.nextInt(CHINA_MOBILE.length)];
                break;
            case 1:
                mobile01 = CHINA_UNICOM[random.nextInt(CHINA_UNICOM.length)];
                break;
            case 2:
                mobile01 = CHINA_TELECOME[random.nextInt(CHINA_TELECOME.length)];
                break;
            default:
                mobile01 = "op标志位有误!";
                break;
        }
        if (mobile01.length() > 3) {

            return mobile01;
        }
        sb.append(mobile01);
        //生成手机号后8位
        for (int i = 0; i < 8; i++) {

            temp = random.nextInt(10);
            sb.append(temp);
        }
        return sb.toString();
    }

    /**
     * 随机生成指定手机号 * @param num 生成个数 *
     * @return
     */
    public static List<String> randomCreatePhone(int num) {

        List<String> phoneList = new ArrayList<>();
        Random random = new Random();
        StringBuilder sb = new StringBuilder();
        for (int i = 1; i <= num; i++) {

            int op = random.nextInt(3);//随机运营商标志位
            phoneList.add(createMobile(op));
        }
        return phoneList;
    }
}

生成token.txt:

2、jmeter测试配置

线程组(Thread Group配置

 HTTP请求配置

 CSV数据集设置

 请求设置

测试通过!!! 

 

原文地址:https://blog.csdn.net/qq_45524787/article/details/128651449

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_41108.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注