ter = array_value($config, 'filter');
$arr = array_value($filter, $type);
$enable = array_value($arr, 'enable');
$wordarr = array_value($arr, 'keyword');
if (0 == $enable || empty($wordarr)) return FALSE;
foreach ($wordarr as $_keyword) {
if (!$_keyword) continue;
$r = strpos(strtolower($keyword), strtolower($_keyword));
if (FALSE !== $r) {
$error = $_keyword;
return TRUE;
}
}
return FALSE;
}
// return http://domain.com OR https://domain.com
function url_prefix()
{
$http = ((isset($_SERVER['HTTPS']) && 'on' == $_SERVER['HTTPS']) || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')) ? 'https://' : 'http://';
return $http . $_SERVER['HTTP_HOST'];
}
// 唯一身份ID
function uniq_id()
{
return uniqid(substr(md5(microtime(true) . mt_rand(1000, 9999)), 8, 8));
}
// 生成订单号 14位
function trade_no()
{
$trade_no = str_replace('.', '', microtime(1));
$strlen = mb_strlen($trade_no, 'UTF-8');
$strlen = 14 - $strlen;
$str = '';
if ($strlen) {
for ($i = 0; $i <= $strlen; $i++) {
if ($i < $strlen) $str .= '0';
}
}
return $trade_no . $str;
}
// 生成订单号 16位
function trade_no_16()
{
$explode = explode(' ', microtime());
$trade_no = $explode[1] . mb_substr($explode[0], 2, 6, 'UTF-8');
return $trade_no;
}
// 当前年的天数
function date_year($time = NULL)
{
$time = intval($time) ? $time : time();
return date('L', $time) + 365;
}
// 当前年份中的第几天
function date_z($time = NULL)
{
$time = intval($time) ? $time : time();
return date('z', $time);
}
// 当前月份中的第几天,没有前导零 1 到 31
function date_j($time = NULL)
{
$time = intval($time) ? $time : time();
return date('j', $time);
}
// 当前月份中的第几天,有前导零的2位数字 01 到 31
function date_d($time = NULL)
{
$time = intval($time) ? $time : time();
return date('d', $time);
}
// 当前时间为星期中的第几天 数字表示 1表示星期一 到 7表示星期天
function date_w_n($time = NULL)
{
$time = intval($time) ? $time : time();
return date('N', $time);
}
// 当前日第几周
function date_d_w($time = NULL)
{
$time = intval($time) ? $time : time();
return date('W', $time);
}
// 当前几月 没有前导零1-12
function date_n($time = NULL)
{
$time = intval($time) ? $time : time();
return date('n', $time);
}
// 当前月的天数
function date_t($time = NULL)
{
$time = intval($time) ? $time : time();
return date('t', $time);
}
// 0 o'clock on the day
function clock_zero()
{
return strtotime(date('Ymd'));
}
// 24 o'clock on the day
function clock_twenty_four()
{
return strtotime(date('Ymd')) + 86400;
}
// 8点过期 / expired at 8 a.m.
function eight_expired($time = NULL)
{
$time = intval($time) ? $time : time();
// 当前时间大于8点则改为第二天8点过期
$life = date('G') <= 8 ? (strtotime(date('Ymd')) + 28800 - $time) : clock_twenty_four() - $time + 28800;
return $life;
}
// 24点过期 / expired at 24 a.m.
function twenty_four_expired($time = NULL)
{
$time = intval($time) ? $time : time();
$twenty_four = clock_twenty_four();
$life = $twenty_four - $time;
return $life;
}
/**
* @param $url 提交地址
* @param string $post POST数组 / 空为GET获取数据 / $post='GET'获取连续跳转最终URL
* @param string $cookie cookie
* @param int $timeout 超时
* @param int $ms 设为1是毫秒
* @return mixed 返回数据
*/
function https_request($url, $post = '', $cookie = '', $timeout = 30, $ms = 0)
{
if (empty($url)) return FALSE;
if (version_compare(PHP_VERSION, '5.2.3', '<')) {
$ms = 0;
$timeout = 30;
}
is_array($post) and $post = http_build_query($post);
// 没有安装curl 使用http的形式,支持post
if (!extension_loaded('curl')) {
//throw new Exception('server not install CURL');
if ($post) {
return https_post($url, $post, $cookie, $timeout);
} else {
return http_get($url, $cookie, $timeout);
}
}
is_array($cookie) and $cookie = http_build_query($cookie);
$curl = curl_init();
// 返回执行结果,不输出
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
//php5.5跟php5.6中的CURLOPT_SAFE_UPLOAD的默认值不同
if (class_exists('\CURLFile')) {
curl_setopt($curl, CURLOPT_SAFE_UPLOAD, true);
} else {
defined('CURLOPT_SAFE_UPLOAD') and curl_setopt($curl, CURLOPT_SAFE_UPLOAD, false);
}
// 设定请求的RUL
curl_setopt($curl, CURLOPT_URL, $url);
// 设定返回信息中包含响应信息头
if (ini_get('safe_mode') && ini_get('open_basedir')) {
// $post参数必须为GET
if ('GET' == $post) {
// 安全模式时将头文件的信息作为数据流输出
curl_setopt($curl, CURLOPT_HEADER, true);
// 安全模式采用连续抓取
curl_setopt($curl, CURLOPT_NOBODY, true);
}
} else {
curl_setopt($curl, CURLOPT_HEADER, false);
// 允许跳转10次
curl_setopt($curl, CURLOPT_MAXREDIRS, 10);
// 使用自动跳转,返回最后的Location
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
}
$ua1 = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1';
$ua = empty($_SERVER["HTTP_USER_AGENT"]) ? $ua1 : $_SERVER["HTTP_USER_AGENT"];
curl_setopt($curl, CURLOPT_USERAGENT, $ua);
// 兼容HTTPS
if (FALSE !== stripos($url, 'https://')) {
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE);
//ssl版本控制
//curl_setopt($curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1);
curl_setopt($curl, CURLOPT_SSLVERSION, true);
}
$header = array('Content-type: application/x-www-form-urlencoded;charset=UTF-8', 'X-Requested-With: XMLHttpRequest');
$cookie and $header[] = "Cookie: $cookie";
curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
if ($post) {
// POST
curl_setopt($curl, CURLOPT_POST, true);
// 自动设置Referer
curl_setopt($curl, CURLOPT_AUTOREFERER, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
}
if ($ms) {
curl_setopt($curl, CURLOPT_NOSIGNAL, true); // 设置毫秒超时
curl_setopt($curl, CURLOPT_TIMEOUT_MS, intval($timeout)); // 超时毫秒
} else {
curl_setopt($curl, CURLOPT_TIMEOUT, intval($timeout)); // 秒超时
}
//优先解析 IPv6 超时后IPv4
//curl_setopt($curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
curl_setopt($curl, CURLOPT_ENCODING, 'gzip');
// 返回执行结果
$output = curl_exec($curl);
// 有效URL,输出URL非URL页面内容 CURLOPT_RETURNTRANSFER 必须为false
'GET' == $post and $output = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL);
curl_close($curl);
return $output;
}
function save_image($img)
{
$ch = curl_init();
// 设定请求的RUL
curl_setopt($ch, CURLOPT_URL, $img);
// 设定返回信息中包含响应信息头 启用时会将头文件的信息作为数据流输出
//curl_setopt($ch, CURLOPT_HEADER, false);
//curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER["HTTP_USER_AGENT"]);
// true表示$html,false表示echo $html
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
//curl_setopt($ch, CURLOPT_BINARYTRANSFER, 1);
//curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
curl_setopt($ch, CURLOPT_ENCODING, 'gzip');
$output = curl_exec($ch);
curl_close($ch);
return $output;
}
// 计算字串宽度:剧中对齐(字体大小/字串内容/字体链接/背景宽度/倍数)
function calculate_str_width($size, $str, $font, $width, $multiple = 2)
{
$box = imagettfbbox($size, 0, $font, $str);
return ($width - $box[4] - $box[6]) / $multiple;
}
// 搜索目录下的文件 比对文件后缀
function search_directory($path)
{
if (is_dir($path)) {
$paths = scandir($path);
foreach ($paths as $val) {
$sub_path = $path . '/' . $val;
if ('.' == $val || '..' == $val) {
continue;
} else if (is_dir($sub_path)) {
//echo '目录名:' . $val . '
';
search_directory($sub_path);
} else {
//echo ' 最底层文件: ' . $path . '/' . $val . '
';
$ext = strtolower(file_ext($sub_path));
if (in_array($ext, array('php', 'asp', 'jsp', 'cgi', 'exe', 'dll'), TRUE)) {
echo '异常文件:' . $sub_path . '
';
}
}
}
}
}
// 一维数组转字符串 $sign待签名字符串 $url为urlencode转码GET参数字符串
function array_to_string($arr, &$sign = '', &$url = '')
{
if (count($arr) != count($arr, 1)) throw new Exception('Does not support multi-dimensional array to string');
// 注销签名
unset($arr['sign']);
// 排序
ksort($arr);
reset($arr);
// 转字符串做签名
$url = '';
$sign = '';
foreach ($arr as $key => $val) {
if (empty($val) || is_array($val)) continue;
$url .= $key . '=' . urlencode($val) . '&';
$sign .= $key . '=' . $val . '&';
}
$url = substr($url, 0, -1);
$url = htmlspecialchars($url);
$sign = substr($sign, 0, -1);
}
// 私钥生成签名
function rsa_create_sign($data, $key, $sign_type = 'RSA')
{
if (!function_exists('openssl_sign')) throw new Exception('OpenSSL extension is not enabled');
if (!defined('OPENSSL_ALGO_SHA256')) throw new Exception('Only versions above PHP 5.4.8 support SHA256');
$key = wordwrap($key, 64, "\n", true);
if (FALSE === $key) throw new Exception('Private Key Error');
$key = "-----BEGIN RSA PRIVATE KEY-----\n$key\n-----END RSA PRIVATE KEY-----";
if ('RSA2' == $sign_type) {
openssl_sign($data, $sign, $key, OPENSSL_ALGO_SHA256);
} else {
openssl_sign($data, $sign, $key, OPENSSL_ALGO_SHA1);
}
// 加密
return base64_encode($sign);
}
// 公钥验证签名
function rsa_verify_sign($data, $sign, $key, $sign_type = 'RSA')
{
$key = wordwrap($key, 64, "\n", true);
if (FALSE === $key) throw new Exception('Public Key Error');
$key = "-----BEGIN PUBLIC KEY-----\n$key\n-----END PUBLIC KEY-----";
// 签名正确返回1 签名不正确返回0 错误-1
if ('RSA2' == $sign_type) {
$result = openssl_verify($data, base64_decode($sign), $key, OPENSSL_ALGO_SHA256);
} else {
$result = openssl_verify($data, base64_decode($sign), $key, OPENSSL_ALGO_SHA1);
}
return $result === 1;
}
// Array to xml array('appid' => 'appid', 'code' => 'success')
function array_to_xml($arr)
{
if (!is_array($arr) || empty($arr)) throw new Exception('Array Error');
$xml = "";
foreach ($arr as $key => $val) {
if (is_numeric($val)) {
$xml .= "<" . $key . ">" . $val . "" . $key . ">";
} else {
$xml .= "<" . $key . ">" . $key . ">";
}
}
$xml .= "";
return $xml;
}
// Xml to array
function xml_to_array($xml)
{
if (!$xml) throw new Exception('XML error');
$old = libxml_disable_entity_loader(true);
// xml解析
$result = (array)simplexml_load_string($xml, null, LIBXML_NOCDATA | LIBXML_COMPACT);
// 恢复旧值
if (FALSE === $old) libxml_disable_entity_loader(false);
return $result;
}
// 逐行读取
function well_import($file)
{
if ($handle = fopen($file, 'r')) {
while (!feof($handle)) {
yield trim(fgets($handle));
}
fclose($handle);
}
}
// 计算总行数
function well_import_total($file, $key = 'well_import_total')
{
static $cache = array();
if (isset($cache[$key])) return $cache[$key];
$count = cache_get($key);
if (NULL === $count) {
$count = 0;
$globs = well_import($file);
while ($globs->valid()) {
++$count;
$globs->next(); // 指向下一个
}
$count and cache_set($key, $count, 300);
}
return $cache[$key] = $count;
}
$g_dir_file = FALSE;
function well_search_dir($path)
{
global $g_dir_file;
FALSE === $g_dir_file and $g_dir_file = array();
if (is_dir($path)) {
$paths = scandir($path);
foreach ($paths as $val) {
$sub_path = $path . '/' . $val;
if ('.' == $val || '..' == $val) {
continue;
} else if (is_dir($sub_path)) {
well_search_dir($sub_path);
} else {
$g_dir_file[] = $sub_path;
}
}
}
return $g_dir_file;
}
?>SpringBoot 结合RabbitMQ与Redis实现商品的并发下单【SpringBoot系列12】-阿南达文事网
SpringBoot 结合RabbitMQ与Redis实现商品的并发下单【SpringBoot系列12】
编程日记420
更新时间:2025-05-10 13:13:33
SpringBoot 结合RabbitMQ与Redis实现商品的并发下单【SpringBoot系列12】
SpringCloud 大型系列课程正在制作中,欢迎大家关注与提意见。
程序员每天的CV 与 板砖,也要知其所以然,本系列课程可以帮助初学者学习 SpringBooot 项目开发 与 SpringCloud 微服务系列项目开发
1 项目准备
- SpringBoot 整合 RabbitMQ 消息队列【SpringBoot系列11】本文章 基于这个项目来开发
本文章是系列文章 ,每节文章都有对应的代码,每节的源码都是在上一节的基础上配置而来,对应的视频讲解课程正在火速录制中。
订单系统,用户下单,即要保存即时性,也要保证流畅性,同时还要防止超卖,本文章是基于 RabbitMQ 消息队列 + Redis 实现的下单,当然后续还会的秒杀系统设计 以及后续的微服务以及熔断控制等等
如这里 我的商品 库存有 10 个
然后我使用 apache-jmeter-5.5 压测,200个用户1秒内请求完成,每个用户请求2次,也就是1秒有400次下单请求
测试完成后,商品库存为0,然后订单生成10个,完美解决并发问题
这是实现的普通订单,基本实现逻辑是
1、redis 校验库存,预下单
2、消息队列减库存 生成 订单 (数据库、redis、es)
3、用户查询到订单成功,发起支付
4、支付回调 修改订单数据 (数据库、redis 、es)
1 预下单接口
@Api(tags="订单模块")
@RestController()
@RequestMapping("/orders")
@Slf4j
public class OrderController {@Autowiredprivate OrderService orderService;/*** 下单* @param goodsId 商品ID* @param userId* @return*/@GetMapping("/create/{id}")public R createOrder(@PathVariable("id") Long goodsId,@RequestHeader Long userId) {return orderService.createPreOrder(goodsId,userId);}
}
@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate OrderMQSender mqSender;@Overridepublic R createPreOrder(Long goodsId, Long userId) {log.info("预下单处理 userId:{} goodsId:{} ",userId,goodsId);//获取redis中的商品库存 先判断商品是否有库存Boolean aBoolean = redisTemplate.hasKey("goodStock:" + goodsId);if(Boolean.FALSE.equals(aBoolean)){return R.error("下单失败 商品库存不足");}//获取商品库存int goodsStock = Integer.valueOf(redisTemplate.opsForValue().get("goodStock:" +goodsId).toString());if(goodsStock==0){return R.error("下单失败 商品库存不足");}//发送下单消息SecKillMessage message = new SecKillMessage(userId, goodsId);mqSender.sendCommonOrderMessage(JsonUtils.toJson(message));return R.okData("预下单成功");}
redisTemplate 的 hasKey 可以直接判断key是否存在,在这里如果商品的key不存在,则商品无库存,redis 的商品库存是在服务启动后,自动同步进入的
@Service
@Slf4j
public class OrderServiceImpl implements OrderService , InitializingBean {@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate GoodsService goodsService;/*** 初始化秒杀商品数量到 redis 中** @return*/@Overridepublic R startSeckillInit() {List<SeckillGoods> goods = secKillGoodsService.findAllSecKillGoods();if (CollectionUtils.isEmpty(goods)) {return R.error("无秒杀商品");}goods.forEach(g -> {log.info("初始化秒杀商品 goodsId:{} stock: {}", g.getGoodsId(), g.getStockCount());redisTemplate.opsForValue().set("goodStock:" + g.getGoodsId(), g.getStockCount());});return R.ok("初始化完成");}@Overridepublic void afterPropertiesSet() throws Exception {this.startSeckillInit();}
InitializingBean 当一个类实现这个接口之后,Spring启动后,初始化Bean时,若该Bean实现InitialzingBean接口,会自动调用afterPropertiesSet()方法,完成一些用户自定义的初始化操作。
2 消息队列的定义
在这里单独定义普通下单使用的队列与交换机
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class OrderRabbitMQTopicConfig {private static final String commonOrderQueue = "commonOrderQueue";private static final String commonExchange = "commonOrderExchange";@Beanpublic Queue commonOrderQueue() {return new Queue(commonOrderQueue);}@Beanpublic TopicExchange commonExchange() {return new TopicExchange(commonExchange);}@Beanpublic Binding commonOrderBinding() {return BindingBuilder.bind(commonOrderQueue()).to(commonExchange()).with("commonOrder.#");}
}
然后就是订单的发送者
@Service
@Slf4j
public class OrderMQSender {@Autowiredprivate RabbitTemplate rabbitTemplate;/*** 普通订单走的队列* @param msg*/public void sendCommonOrderMessage(String msg) {log.info("预下单发送消息:{}", msg);rabbitTemplate.convertAndSend("commonOrderExchange", "commonOrder.message", msg);}
}
然后定义普通订单的消息接收者
@Service
@Slf4j
public class OrderMQReceiver {@Autowiredprivate OrderService orderService;@RabbitListener(queues = "commonOrderQueue")public void receiveCommonOrderMessage(String message) {log.info("接收的秒杀订单消息:{}", message);SecKillMessage secKillMessage = JsonUtils.toObj(message, SecKillMessage.class);Long userId = secKillMessage.getUserId();Long goodsId = secKillMessage.getGoodsId();//普通下单orderService.createOrder(goodsId, userId);}}
普通下单里,就是减库存,生成订单的过程
@Override@Transactionalpublic R createOrder(Long goodsId, Long userId) {log.info("下单处理 userId:{} goodsId:{} ",userId,goodsId);//查询商品详情Goods goods = goodsService.findGoods(goodsId);//商品的实际库存if (goods.getGoodsStock() < 1) {// 设置该商品库存为空redisTemplate.opsForValue().set("goodStock:" + goods.getId(), "0");log.info("库存不足 下单失败");return R.error("商品库存不足");}//减库存 int currentStock = goods.getGoodsStock() -1;//更新数据库 库存goods.setGoodsStock(currentStock);int update = goodsService.updateGoodsStock(goods);if(update<=0){log.info("更新库存失败 下单失败");return R.error("商品库存不足");}//更新redis 缓存redisTemplate.opsForValue().set("goodStock:" + goods.getId(), currentStock);// 下订单Order order = new Order();order.setUserId(userId);order.setGoodsId(goodsId);order.setDeliveryAddrId(0L);order.setGoodsName(goods.getGoodsName());order.setGoodsCount(1);order.setGoodsPrice(goods.getGoodsPrice());order.setOrderChannel(1);order.setStatus(0); // 订单创建中order.setCreateDate(new Date());orderMapper.insert(order);log.info("下单成功 userId:{} goodsId:{} orderId:{}",userId,goodsId,order.getId());//缓存普通订单redisTemplate.opsForValue().set("order:" +userId + ":" + goodsId, order);//保存数据到ES中//后续实现return R.okData(order);}
本文章是系列文章 ,每节文章都有对应的代码,每节的源码都是在上一节的基础上配置而来,对应的视频讲解课程正在火速录制中。
本文章只有核心代码,全部代码请查看对应源码
项目源码在这里 :.long/spring-boot-study/tree/master/biglead-api-10-seckill
有兴趣可以关注一下公众号:biglead
- 创建SpringBoot基础项目
- SpringBoot项目集成mybatis
- SpringBoot 集成 Druid 数据源【SpringBoot系列3】
- SpringBoot MyBatis 实现分页查询数据【SpringBoot系列4】
- SpringBoot MyBatis-Plus 集成 【SpringBoot系列5】
- SpringBoot mybatis-plus-generator 代码生成器 【SpringBoot系列6】
- SpringBoot MyBatis-Plus 分页查询 【SpringBoot系列7】
- SpringBoot 集成Redis缓存 以及实现基本的数据缓存【SpringBoot系列8】
- SpringBoot 整合 Spring Security 实现安全认证【SpringBoot系列9】
- SpringBoot Security认证 Redis缓存用户信息【SpringBoot系列10】
- SpringBoot 整合 RabbitMQ 消息队列【SpringBoot系列11】
本文发布于:2024-11-10,感谢您对本站的认可!
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:SpringBoot 结合RabbitMQ与Redis实现商品的并发下单SpringBoot系列12
发布评论