文章目录
- 一、实现思路
- 二、实现代码
一、实现思路
二、实现代码
order.lua脚本代码:
-- 参数列表
local productIdStr = ARGV[1]
local productNameStr = ARGV[2]
local cartQuantityStr = ARGV[3]
local orderId = ARGV[4]
local userId = ARGV[5]
local orderDate = ARGV[6]
local productPriceStr = ARGV[7]
local productPhotoStr = ARGV[8]
local cartIdStr = ARGV[9]
local stockKey = KEYS[1]
-- 函数列表
local Str_split = '根据字符串逗号分割成table'
function Str_split(str, splitStr)
local subStrTable = {};
while (true) do
local pos = string.find(str, splitStr);
if (not pos) then
if str ~= "" then
subStrTable[#subStrTable + 1] = str;
end
break;
end
local subStr = string.sub(str, 1, pos - 1);
if subStr ~= "" then
subStrTable[#subStrTable + 1] = subStr;
end
str = string.sub(str, pos + string.len(splitStr), #str);
end
return subStrTable;
end
-- 转成table
local productIdList = Str_split(productIdStr, ',')
local cartQuantityList = Str_split(cartQuantityStr, ',')
local productNameList = Str_split(productNameStr, ',')
-- 根据商品id获取每个商品的库存
local stockList = redis.pcall('hmget', stockKey, unpack(productIdList))
-- 根据购买的每个商品库存是否足够
for i,v in ipairs(stockList) do
if(tonumber(v) < tonumber(cartQuantityList[i])) then
return '商品【' .. productNameList[i] .. '】库存不足!'
end
end
-- 扣减库存
local payLoad = {};
for i,v in ipairs(stockList) do
table.insert(payLoad, productIdList[i])
table.insert(payLoad, tonumber(v) - tonumber(cartQuantityList[i]))
end
redis.pcall('hmset', stockKey, unpack(payLoad))
-- 写入订单消息队列 id: 是指 stream 中的消息 ID,通常使用 * 号表示自动生成
redis.call('xadd', 'stream.orders', '*', 'userId', userId,
'productIdList', productIdStr, 'id', orderId,
'cartQuantity', cartQuantityStr, 'orderDate', orderDate,
'productNameList', productNameStr, 'productPriceList', productPriceStr,
'productPhotoList', productPhotoStr, 'cartIdList', cartIdStr)
return '成功'
OrderServiceImpl.java文件代码:
package com.yjq.programmer.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.sun.deploy.util.StringUtils;
import com.yjq.programmer.bean.CodeMsg;
import com.yjq.programmer.bean.RedisConstant;
import com.yjq.programmer.dao.*;
import com.yjq.programmer.domain.*;
import com.yjq.programmer.dto.*;
import com.yjq.programmer.enums.OrderStateEnum;
import com.yjq.programmer.service.IOrderService;
import com.yjq.programmer.service.IUserService;
import com.yjq.programmer.utils.CommonUtil;
import com.yjq.programmer.utils.CopyUtil;
import com.yjq.programmer.utils.UuidUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.stream.*;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
/**
* @author 杨杨吖
* @QQ 823208782
* @WX yjqi12345678
* @create 2023-06-17 16:18
*/
@Service
@Transactional
public class OrderServiceImpl implements IOrderService {
@Resource
private OrderMapper orderMapper;
@Resource
private UserMapper userMapper;
@Resource
private OrderItemMapper orderItemMapper;
@Resource
private ProductMapper productMapper;
@Resource
private CartMapper cartMapper;
@Resource
private IUserService userService;
// @Resource
// private RedissonClient redissonClient;
private static final DefaultRedisScript<String> SECKILL_SCRIPT;
@Resource
private StringRedisTemplate stringRedisTemplate;
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
private Logger log = LoggerFactory.getLogger(OrderServiceImpl.class);
@PostConstruct
private void init() {
SECKILL_ORDER_EXECUTOR.submit(new OrderHandler());
}
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("lua/order.lua"));
SECKILL_SCRIPT.setResultType(String.class);
}
private class OrderHandler implements Runnable {
@Override
public void run() {
while (true) {
try {
// 1.获取消息队列中的订单信息 XREAD GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS stream.orders >
// ReadOffset.lastConsumed() 获取下一个未消费的订单
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
StreamOffset.create("stream.orders", ReadOffset.lastConsumed())
);
// 2.判断订单信息是否为空
if (list == null || list.isEmpty()) {
// 如果为null,说明没有消息,继续下一次循环
continue;
}
// 解析数据 获取一条数据 因为上面count(1)指定获取一条
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
OrderDTO orderDTO = BeanUtil.fillBeanWithMap(value, new OrderDTO(), true);
// 3.创建订单
crateOrder(orderDTO);
// 4.确认消息 XACK
stringRedisTemplate.opsForStream().acknowledge("stream.orders", "g1", record.getId());
} catch (Exception e) {
log.error("处理订单异常", e);
handlePendingList();
}
}
}
// 确认异常的订单再次处理
private void handlePendingList() {
while (true) {
try {
// 1.获取pending-list中的订单信息 XREAD GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS stream.orders 0
// ReadOffset.from("0") 从第一个消息开始
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1),
StreamOffset.create("stream.orders", ReadOffset.from("0"))
);
// 2.判断订单信息是否为空
if (list == null || list.isEmpty()) {
// 如果为null,说明没有异常消息,结束循环
break;
}
// 解析数据
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
OrderDTO orderDTO = BeanUtil.fillBeanWithMap(value, new OrderDTO(), true);
// 3.创建订单
crateOrder(orderDTO);
// 4.确认消息 XACK
stringRedisTemplate.opsForStream().acknowledge("stream.orders", "g1", record.getId());
} catch (Exception e) {
log.error("处理订单异常", e);
try {
Thread.sleep(100);
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
}
}
}
}
private void crateOrder(OrderDTO orderDTO) {
// // 创建锁对象
// RLock redisLock = redissonClient.getLock("lock:order:" + userId);
// // 尝试获取锁
// boolean isLock = redisLock.tryLock();
// // 判断
// if (!isLock) {
// // 获取锁失败,直接返回失败或者重试
// log.error("不允许重复下单!");
// return;
// }
try {
// 业务逻辑
String userId = orderDTO.getUserId();
String orderId = orderDTO.getId();
Date createTime = CommonUtil.getFormatterDate(orderDTO.getOrderDate(), "yyyy-MM-dd HH:mm:ss");
String[] productIdList = orderDTO.getProductIdList().split(",");
String[] productNameList = orderDTO.getProductNameList().split(",");
String[] productPhotoList = orderDTO.getProductPhotoList().split(",");
String[] productPriceList = orderDTO.getProductPriceList().split(",");
String[] cartQuantityList = orderDTO.getCartQuantity().split(",");
String[] cartIdList = orderDTO.getCartIdList().split(",");
BigDecimal totalPrice = new BigDecimal("0");
for(int i=0; i < productIdList.length; i++) {
// 减去商品库存
Product product = productMapper.selectByPrimaryKey(productIdList[i]);
product.setStock(product.getStock() - Integer.parseInt(cartQuantityList[i]));
// 增加商品销量
product.setSellNum(product.getSellNum() + Integer.parseInt(cartQuantityList[i]));
productMapper.updateByPrimaryKeySelective(product);
// 插入订单详情数据
OrderItem orderItem = new OrderItem();
orderItem.setOrderId(orderId);
orderItem.setId(UuidUtil.getShortUuid());
orderItem.setQuantity(Integer.valueOf(cartQuantityList[i]));
orderItem.setProductPrice(new BigDecimal(productPriceList[i]));
orderItem.setProductPhoto(productPhotoList[i]);
orderItem.setProductName(productNameList[i]);
orderItem.setProductId(productIdList[i]);
orderItemMapper.insertSelective(orderItem);
totalPrice = totalPrice.add(new BigDecimal(productPriceList[i]).multiply(new BigDecimal(cartQuantityList[i])));
}
// 插入订单数据
Order order = new Order();
order.setId(orderId);
order.setUserId(userId);
order.setCreateTime(createTime);
order.setState(OrderStateEnum.NO_PAY.getCode());
order.setTotalPrice(totalPrice);
orderMapper.insertSelective(order);
// 清除购物车数据
for(String cartId : cartIdList) {
cartMapper.deleteByPrimaryKey(cartId);
}
} finally {
// redisLock.unlock();
}
}
/**
* 提交订单操作处理
* @param orderDTO
* @return
*/
@Override
public ResponseDTO<OrderDTO> submitOrder(OrderDTO orderDTO) {
UserDTO userDTO = new UserDTO();
userDTO.setToken(orderDTO.getToken());
ResponseDTO<UserDTO> loginUserResponse = userService.getLoginUser(userDTO);
if(!CodeMsg.SUCCESS.getCode().equals(loginUserResponse.getCode())) {
return ResponseDTO.errorByMsg(CodeMsg.USER_SESSION_EXPIRED);
}
// 获取登录用户信息
userDTO = loginUserResponse.getData();
AddressExample addressExample = new AddressExample();
addressExample.createCriteria().andUserIdEqualTo(userDTO.getId());
if(addressMapper.selectByExample(addressExample).size() == 0) {
return ResponseDTO.errorByMsg(CodeMsg.ADDRESS_NOT_EXIST);
}
String[] cartIdList = orderDTO.getCartIdList().split(",");
CartExample cartExample = new CartExample();
cartExample.createCriteria().andIdIn(Arrays.stream(cartIdList).collect(Collectors.toList()));
List<Cart> cartList = cartMapper.selectByExample(cartExample);
List<String> productIdList = cartList.stream().map(Cart::getProductId).collect(Collectors.toList());
ProductExample productExample = new ProductExample();
productExample.createCriteria().andIdIn(productIdList);
List<Product> productList = productMapper.selectByExample(productExample);
List<String> productNameList = productList.stream().map(Product::getName).collect(Collectors.toList());
List<String> productPriceList = productList.stream().map(Product::getPrice).map(String::valueOf).collect(Collectors.toList());
List<String> productPhotoList = productList.stream().map(Product::getPhoto).map(String::valueOf).collect(Collectors.toList());
List<String> cartQuantityList = cartList.stream().map(Cart::getQuantity).map(String::valueOf).collect(Collectors.toList());
String orderId = UuidUtil.getShortUuid();
String orderDate = CommonUtil.getFormatterDate(new Date(), "yyyy-MM-dd HH:mm:ss");
// 执行lua脚本
String result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.singletonList(RedisConstant.STOCK_REDIS_KEY_TEMPLATE),
StringUtils.join(productIdList, ","),
StringUtils.join(productNameList, ","),
StringUtils.join(cartQuantityList, ","),
orderId,
userDTO.getId(),
orderDate,
StringUtils.join(productPriceList, ","),
StringUtils.join(productPhotoList, ","),
StringUtils.join(Arrays.asList(cartIdList), ",")
);
if(!"成功".equals(result)) {
CodeMsg codeMsg = CodeMsg.PRODUCT_STOCK_OVER;
codeMsg.setMsg(result);
return ResponseDTO.errorByMsg(codeMsg);
}
orderDTO.setId(orderId);
return ResponseDTO.success(orderDTO);
}
}