2、基于Lua脚本,判断秒杀库存,一人一单,决定用户是否抢购成功,成功将消息推送到消息队列
编辑3、 初始化lua脚本,主线程中初始化代理对象:用于后面使用事务不会失效
4、主要是开启线程任务,不断从消息队列中获取信息,实现异步下单功能,下单功能在下一步
5、真正下单业务,可以不使用分布锁,在lua脚本中已经判断过了,高并发900个token,200个秒杀券清0
一、实现步骤:
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
3、 初始化lua脚本,主线程中初始化代理对象:用于后面使用事务不会失效
// 初始化执行lua脚本
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
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();
}
}
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;
}
}
2、jmeter测试配置:
CSV数据集设置:
原文地址:https://blog.csdn.net/qq_45524787/article/details/128651449
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.7code.cn/show_41108.html
如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。