本文分享自华为云社区《秒杀系统架构设计都有哪些关键点?》,作者:JavaEdge 。

0、目标

  • 秒杀重复排队
    重复排队:一个人抢购商品,若未支付,不准重复排队抢购
  • 超卖问题
    1个商品卖给多个人:1商品多订单
  • 秒杀订单支付
    秒杀支付:支付流程调整
  • 超时未支付订单,库存回滚
    1.RabbitMQ延时队列
    2.利用延时队列实现支付订单的监听,根据订单支付状况进行订单数据库回滚

1、防止重复排队

用户每次抢单时,一旦排队,设置个自增值,让该值的初始值为1。

每次进入抢单时,对其递增,若值>1,则表明已排队,为禁止重复排队,直接对外抛异常信息xxx表示已在排队。

1.1 后台排队记录

修改SeckillOrderServiceImpl#add方法,新增递增值判断是否已排队:

//递增,判断是否排队
Long userQueueCount = redisTemplate.boundHashOps("UserQueueCount").increment(username, 1);
if(userQueueCount>1){// 有重复抢单throw new RuntimeException(String.valueOf(StatusCode.REPERROR));
}

2、超卖问题

多人抢购同一商品时,多人同时判断是否有库存,若只剩一个,则都会判断有库存,此时会导致超卖,即一个商品被下了多个订单。

2.1 思路分析

利用Redis list队列,给每件商品创建一个独立的商品个数队列,如:A商品有2个,A商品的ID为1001,则创建一个list,key=SeckillGoodsCountList_1001,往该队列中塞2次该商品ID。

每次给用户下单时,先从队列取数据:

  • 能取到数据

有库存

  • 取不到

无库存

这就防止了超卖。

操作Redis大部分都是先查出数据查,在内存中修改,然后存入Redis。高并发下就有数据错乱问题,为控制数量准确,单独将商品数量整个自增键,自增键是线程安全的,无需担心并发问题。

2.2 代码

每次将商品压入Redis时,创建一个商品队列。

修改SeckillGoodsPushTask,添加一个pushIds方法,用于将指定商品ID放入到指定数字:

/**** 将商品ID存入到数组中* @param len:长度* @param id :值* @return*/
public Long[] pushIds(int len,Long id){Long[] ids = new Long[len];for (int i = 0; i <ids.length ; i++) {ids[i]=id;}return ids;
}

SeckillGoodsPushTask#loadGoodsPushRedis,添加队列操作:

2.3 防止超卖

修改多线程下单方法,分别修改数量控制,以及售罄后用户抢单排队信息的清理:

3、订单支付

完成秒杀下订单后,进入支付页面,此时前端会每3s向后台发送一次请求,判断当前用户订单是否完成支付:

若完成支付,则清理排队信息,并修改订单状态。

3.1 创建支付二维码

下单成功后,会跳转到支付选择页面,在支付选择页面要显示订单编号和订单金额,所以我们需要在下单的时候,将订单金额以及订单编号信息存储到用户查询对象。

选择微信支付后,会跳转到微信支付页面,微信支付页面会根据用户名查看用户秒杀订单,并根据用户秒杀订单的ID创建预支付信息并获取二维码信息,展示给用户看,此时页面每3秒查询一次支付状态,如果支付成功,需要修改订单状态信息。

3.1.1 回显订单号、金额

下单后,进入支付选择页面,需显示订单号和订单金额,所以需要在用户下单后将该数据传入到pay.html页面,所以查询订单状态时,需要将订单号和金额封装到查询的信息中,修改查询订单装的方法加入他们。

修改SeckillOrderController#queryStatus:

测试:

3.1.2 创建二维码

用户创建二维码,可以先查询用户的秒杀订单抢单信息,然后再发送请求到支付微服务中创建二维码,将订单编号以及订单对应的金额传递到支付微服务:/weixin/pay/create/native。

使用Postman测试效果如下:

3.2 支付流程分析

  1. 用户抢单,经过秒杀系统实现抢单,下单后会将向MQ发送一个延时消息,包含抢单信息
  2. 秒杀系统同时启用延时消息监听,一旦监听到订单抢单信息,判断Redis缓存中是否存在订单信息,若存在,则回滚
  3. 秒杀系统启动监听支付回调信息。若支付完成,则将订单持久化到MySQL,如果没完成,清理排队信息,回滚库存
  4. 每次秒杀下单后调用支付系统,创建二维码。若用户支付成功,微信系统会将支付信息发给支付系统指定的回调地址,支付系统收到信息后,将信息发给MQ,第3个步骤就能监听到消息。

3.3 支付回调更新

支付回调这一块代码已经实现了,但之前实现的是订单信息的回调数据发送给MQ,指定了对应队列,不过现在需要实现的是秒杀信息发送给指定队列,所以之前的代码那块需要动态指定队列。

3.3.1 支付回调队列指定

  1. 创建支付二维码需要指定队列
  2. 回调地址回调时,获取支付二维码指定的队列,将支付信息发到指定队列

在微信支付统一下单API中,有个附加参数:

attach:附加数据,String(127),在查询API和支付通知中原样返回,可作为自定义参数使用。

可在创建二维码时,设置该参数以指定回调支付信息的对应队列,每次回调时,会获取该参数,然后将回调信息发到该参数对应的队列。

3.3.1.1 改造支付方法

修改支付微服务的WeixinPayController#createNative:

修改支付微服务的WeixinPayService#createNative:

修改支付微服务的WeixinPayServiceImpl#createNative:

创建二维码时,传递如下参数

  • username,用户名

可根据用户名,查询用户排队信息

  • outtradeno商户订单号

下单必需

  • money,支付金额

支付必需

  • queue,队列名字

回调时,可知将支付信息发送到哪个队列

修改WeixinPayApplication,添加对应队列以及对应交换机绑定,代码如下:

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class WeixinPayApplication {public static void main(String[] args) {SpringApplication.run(WeixinPayApplication.class,args);}@Autowiredprivate Environment env;// 创建DirectExchange交换机@Beanpublic DirectExchange basicExchange(){return new DirectExchange(env.getProperty("mq.pay.exchange.order"), true,false);}// 创建队列@Bean(name = "queueOrder")public Queue queueOrder(){return new Queue(env.getProperty("mq.pay.queue.order"), true);}// 创建秒杀队列@Bean(name = "queueSeckillOrder")public Queue queueSeckillOrder(){return new Queue(env.getProperty("mq.pay.queue.seckillorder"), true);}// 队列绑定到交换机上@Beanpublic Binding basicBindingOrder(){return BindingBuilder.bind(queueOrder()).to(basicExchange()).with(env.getProperty("mq.pay.routing.orderkey"));}// 队列绑定到交换机@Beanpublic Binding basicBindingSeckillOrder(){return BindingBuilder.bind(queueSeckillOrder()).to(basicExchange()).with(env.getProperty("mq.pay.routing.seckillorderkey"));}
}

修改application.yml,添加如下配置

#位置支付交换机和队列
mq:pay:exchange:order: exchange.orderseckillorder: exchange.seckillorderqueue:order: queue.orderseckillorder: queue.seckillorderrouting:key: queue.orderseckillkey: queue.seckillorder

3.3.1.2 测试

创建二维码测试

以后每次支付,都需要带上对应的参数,包括前面的订单支付。

3.3.1.3 改造支付回调方法

修改WeixinPayController#notifyUrl,获取自定义参数,并转成Map,获取queue地址,并将支付信息发送到绑定的queue:

3.3.2 支付状态监听

支付状态通过回调地址发给MQ后,需要在秒杀系统中监听支付信息:

  • 支付成功,修改用户订单状态
  • 支付失败,删除订单,回滚库存。

SeckillOrderPayMessageListener监听消息:

@Component
@RabbitListener(queues = "${mq.pay.queue.seckillorder}")
public class SeckillOrderPayMessageListener {// 监听消费消息@RabbitHandlerpublic void consumeMessage(@Payload String message) {System.out.println(message);// 将消息转换成Map对象Map<String,String> resultMap = JSON.parseObject(message,Map.class);System.out.println("监听到的消息:"+resultMap);}
}

修改SeckillApplication创建对应的队列以及绑定对应交换机。

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
@MapperScan(basePackages = {"com.changgou.seckill.dao"})
@EnableScheduling
@EnableAsync
public class SeckillApplication {public static void main(String[] args) {SpringApplication.run(SeckillApplication.class,args);}@Beanpublic IdWorker idWorker(){return new IdWorker(1,1);}@Autowiredprivate Environment env;/**** 创建DirectExchange交换机* @return*/@Beanpublic DirectExchange basicExchange(){return new DirectExchange(env.getProperty("mq.pay.exchange.order"), true,false);}/**** 创建队列* @return*/@Bean(name = "queueOrder")public Queue queueOrder(){return new Queue(env.getProperty("mq.pay.queue.order"), true);}/**** 创建秒杀队列* @return*/@Bean(name = "queueSeckillOrder")public Queue queueSeckillOrder(){return new Queue(env.getProperty("mq.pay.queue.seckillorder"), true);}/***** 队列绑定到交换机上* @return*/@Beanpublic Binding basicBindingOrder(){return BindingBuilder.bind(queueOrder()).to(basicExchange()).with(env.getProperty("mq.pay.routing.orderkey"));}/***** 队列绑定到交换机上* @return*/@Beanpublic Binding basicBindingSeckillOrder(){return BindingBuilder.bind(queueSeckillOrder()).to(basicExchange()).with(env.getProperty("mq.pay.routing.seckillorderkey"));}
}

添加配置:

#位置支付交换机和队列
mq:pay:exchange:order: exchange.orderseckillorder: exchange.seckillorderqueue:order: queue.orderseckillorder: queue.seckillorderrouting:key: queue.orderseckillkey: queue.seckillorder

3.3.3 修改订单状态

监听到支付信息后,根据支付信息判断,如果用户支付成功,则修改订单信息,并将订单入库,删除用户排队信息,如果用户支付失败,则删除订单信息,回滚库存,删除用户排队信息。

3.3.3.1 业务层

修改SeckillOrderService,添加修改订单方法:

/**** 更新订单状态*/
@Override
public void updatePayStatus(String out_trade_no, String transaction_id,String username) {//订单数据从Redis数据库查询出来SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.boundHashOps("SeckillOrder").get(username);//修改状态seckillOrder.setStatus("1");//支付时间seckillOrder.setPayTime(new Date());//同步到MySQL中seckillOrderMapper.insertSelective(seckillOrder);//清空Redis缓存redisTemplate.boundHashOps("SeckillOrder").delete(username);//清空用户排队数据redisTemplate.boundHashOps("UserQueueCount").delete(username);//删除抢购状态信息redisTemplate.boundHashOps("UserQueueStatus").delete(username);
}

3.3.3.2 修改订单对接

修改微信支付状态监听的代码,当用户支付成功后,修改订单状态:

3.3.4 删除订单回滚库存

如果用户支付失败,我们需要删除用户订单数据,并回滚库存。关闭订单:

/**** 关闭订单,回滚库存*/
@Override
public void closeOrder(String username) {//将消息转换成SeckillStatusSeckillStatus seckillStatus = (SeckillStatus) redisTemplate.boundHashOps("UserQueueStatus").get(username);//获取Redis中订单信息SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.boundHashOps("SeckillOrder").get(username);//如果Redis中有订单信息,说明用户未支付if(seckillStatus!=null && seckillOrder!=null){//删除订单redisTemplate.boundHashOps("SeckillOrder").delete(username);//回滚库存//1)从Redis中获取该商品SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.boundHashOps("SeckillGoods_"+seckillStatus.getTime()).get(seckillStatus.getGoodsId());//2)如果Redis中没有,则从数据库中加载if(seckillGoods==null){seckillGoods = seckillGoodsMapper.selectByPrimaryKey(seckillStatus.getGoodsId());}//3)数量+1  (递增数量+1,队列数量+1)Long surplusCount = redisTemplate.boundHashOps("SeckillGoodsCount").increment(seckillStatus.getGoodsId(), 1);seckillGoods.setStockCount(surplusCount.intValue());redisTemplate.boundListOps("SeckillGoodsCountList_" + seckillStatus.getGoodsId()).leftPush(seckillStatus.getGoodsId());//4)数据同步到Redis中redisTemplate.boundHashOps("SeckillGoods_"+seckillStatus.getTime()).put(seckillStatus.getGoodsId(),seckillGoods);//清理排队标示redisTemplate.boundHashOps("UserQueueCount").delete(seckillStatus.getUsername());//清理抢单标示redisTemplate.boundHashOps("UserQueueStatus").delete(seckillStatus.getUsername());}
}

3.3.4.1 调用删除订单

SeckillOrderPayMessageListener,在用户支付失败后调用关闭订单:

//支付失败,删除订单
seckillOrderService.closeOrder(attachMap.get("username"));

支付微服务

server:port: 9022
spring:application:name: paymain:allow-bean-definition-overriding: truerabbitmq:host: 127.0.0.1 #mq的服务器地址username: guest #账号password: guest #密码
eureka:client:service-url:defaultZone: http://127.0.0.1:6868/eurekainstance:prefer-ip-address: true
feign:hystrix:enabled: true
#hystrix 配置
hystrix:command:default:execution:timeout:#如果enabled设置为false,则请求超时交给ribbon控制enabled: trueisolation:strategy: SEMAPHORE#微信支付信息配置
weixin:appid: wx8397f8696b538317partner: 1473426802partnerkey: T6m9iK73b0kn9g5v426MKfHQH7X8rKwbnotifyurl: http://2cw4969042.wicp.vip:36446/weixin/pay/notify/url#位置支付交换机和队列
mq:pay:exchange:order: exchange.orderqueue:order: queue.orderseckillorder: queue.seckillorderrouting:orderkey: queue.orderseckillorderkey: queue.seckillorder

秒杀微服务配置

server:port: 18084
spring:application:name: seckilldatasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/changgou_seckill?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTCusername: rootpassword: itcastrabbitmq:host: 127.0.0.1 #mq的服务器地址username: guest #账号password: guest #密码main:allow-bean-definition-overriding: true
eureka:client:service-url:defaultZone: http://127.0.0.1:6868/eurekainstance:prefer-ip-address: true
feign:hystrix:enabled: true
mybatis:configuration:map-underscore-to-camel-case: truemapper-locations: classpath:mapper/*Mapper.xmltype-aliases-package: com.changgou.seckill.pojo#hystrix 配置
hystrix:command:default:execution:timeout:#如果enabled设置为false,则请求超时交给ribbon控制enabled: trueisolation:thread:timeoutInMilliseconds: 10000strategy: SEMAPHORE
#位置支付交换机和队列
mq:pay:exchange:order: exchange.orderqueue:order: queue.orderseckillorder: queue.seckillorderrouting:orderkey: queue.orderseckillorderkey: queue.seckillorder

4、库存回滚

4.1 秒杀流程回顾

1.用户抢单,经过秒杀系统实现抢单,下单后会将向MQ发送一个延时队列消息,包含抢单信息,延时半小时后才能监听到
2.秒杀系统同时启用延时消息监听,一旦监听到订单抢单信息,判断Redis缓存中是否存在订单信息,如果存在,则回滚
3.秒杀系统还启动支付回调信息监听,如果支付完成,则将订单吃句话到MySQL,如果没完成,清理排队信息回滚库存
4.每次秒杀下单后调用支付系统,创建二维码,如果用户支付成功了,微信系统会将支付信息发送给支付系统指定的回调地址,支付系统收到信息后,将信息发送给MQ,第3个步骤就可以监听到消息了。

延时队列实现订单关闭回滚库存:

1.创建一个过期队列  Queue1
2.接收消息的队列    Queue2
3.中转交换机
4.监听Queue21)SeckillStatus->检查Redis中是否有订单信息2)如果有订单信息,调用删除订单回滚库存->[需要先关闭微信支付]3)如果关闭订单时,用于已支付,修改订单状态即可4)如果关闭订单时,发生了别的错误,记录日志,人工处理

4.2 关闭支付

用户如果半个小时没有支付,我们会关闭支付订单,但在关闭之前,需要先关闭微信支付,防止中途用户支付。

修改支付微服务的WeixinPayService,添加关闭支付方法,代码如下:

/**** 关闭支付* @param orderId* @return*/
Map<String,String> closePay(Long orderId) throws Exception;

修改WeixinPayServiceImpl,实现关闭微信支付方法,代码如下:

/**** 关闭微信支付* @param orderId* @return* @throws Exception*/
@Override
public Map<String, String> closePay(Long orderId) throws Exception {//参数设置Map<String,String> paramMap = new HashMap<String,String>();paramMap.put("appid",appid); //应用IDparamMap.put("mch_id",partner);    //商户编号paramMap.put("nonce_str",WXPayUtil.generateNonceStr());//随机字符paramMap.put("out_trade_no",String.valueOf(orderId));   //商家的唯一编号//将Map数据转成XML字符String xmlParam = WXPayUtil.generateSignedXml(paramMap,partnerkey);//确定urlString url = "https://api.mch.weixin.qq.com/pay/closeorder";//发送请求HttpClient httpClient = new HttpClient(url);//httpshttpClient.setHttps(true);//提交参数httpClient.setXmlParam(xmlParam);//提交httpClient.post();//获取返回数据String content = httpClient.getContent();//将返回数据解析成Mapreturn  WXPayUtil.xmlToMap(content);
}

4.3 关闭订单回滚库存

4.3.1 配置延时队列

在application.yml文件中引入队列信息配置,如下:

#位置支付交换机和队列
mq:pay:exchange:order: exchange.orderqueue:order: queue.orderseckillorder: queue.seckillorderseckillordertimer: queue.seckillordertimerseckillordertimerdelay: queue.seckillordertimerdelayrouting:orderkey: queue.orderseckillorderkey: queue.seckillorder

配置队列与交换机,在SeckillApplication中添加如下方法

/*** 到期数据队列* @return*/
@Bean
public Queue seckillOrderTimerQueue() {return new Queue(env.getProperty("mq.pay.queue.seckillordertimer"), true);
}/*** 超时数据队列* @return*/
@Bean
public Queue delaySeckillOrderTimerQueue() {return QueueBuilder.durable(env.getProperty("mq.pay.queue.seckillordertimerdelay")).withArgument("x-dead-letter-exchange", env.getProperty("mq.pay.exchange.order"))        // 消息超时进入死信队列,绑定死信队列交换机.withArgument("x-dead-letter-routing-key", env.getProperty("mq.pay.queue.seckillordertimer"))   // 绑定指定的routing-key.build();
}/**** 交换机与队列绑定* @return*/
@Bean
public Binding basicBinding() {return BindingBuilder.bind(seckillOrderTimerQueue()).to(basicExchange()).with(env.getProperty("mq.pay.queue.seckillordertimer"));
}

4.3.2 发送延时消息

修改MultiThreadingCreateOrder,添加如下方法:

/**** 发送延时消息到RabbitMQ中* @param seckillStatus*/
public void sendTimerMessage(SeckillStatus seckillStatus){rabbitTemplate.convertAndSend(env.getProperty("mq.pay.queue.seckillordertimerdelay"), (Object) JSON.toJSONString(seckillStatus), new MessagePostProcessor() {@Overridepublic Message postProcessMessage(Message message) throws AmqpException {message.getMessageProperties().setExpiration("10000");return message;}});
}

在createOrder方法中调用上面方法,如下代码:

//发送延时消息到MQ中
sendTimerMessage(seckillStatus);

4.3.3 库存回滚

创建SeckillOrderDelayMessageListener实现监听消息,并回滚库存,代码如下:

@Component
@RabbitListener(queues = "${mq.pay.queue.seckillordertimer}")
public class SeckillOrderDelayMessageListener {@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate SeckillOrderService seckillOrderService;@Autowiredprivate WeixinPayFeign weixinPayFeign;/**** 读取消息* 判断Redis中是否存在对应的订单* 如果存在,则关闭支付,再关闭订单* @param message*/@RabbitHandlerpublic void consumeMessage(@Payload String message){//读取消息SeckillStatus seckillStatus = JSON.parseObject(message,SeckillStatus.class);//获取Redis中订单信息String username = seckillStatus.getUsername();SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.boundHashOps("SeckillOrder").get(username);//如果Redis中有订单信息,说明用户未支付if(seckillOrder!=null){System.out.println("准备回滚---"+seckillStatus);//关闭支付Result closeResult = weixinPayFeign.closePay(seckillStatus.getOrderId());Map<String,String> closeMap = (Map<String, String>) closeResult.getData();if(closeMap!=null && closeMap.get("return_code").equalsIgnoreCase("success") &&closeMap.get("result_code").equalsIgnoreCase("success") ){//关闭订单seckillOrderService.closeOrder(username);}}}
}

点击关注,第一时间了解华为云新鲜技术~

设计秒杀系统架构,这4个关键点要注意相关推荐

  1. 系统架构设计——秒杀系统架构设计

    摘要 秒杀大家都不陌生.自2011年首次出现以来,无论是双十一购物还是 12306 抢票,秒杀场景已随处可见.简单来说,秒杀就是在同一时刻大量请求争抢购买同一商品并完成交易的过程.从架构视角来看,秒杀 ...

  2. 00 如何设计一个秒杀系统——秒杀系统架构设计都有哪些关键点

    一.如何理解秒杀系统 秒杀系统其实主要解决两个问题,一个是并发读,一个是并发写.并发读的核心优化理念是尽量减少用户到服务端来"读"数据,或者让他们读更少的数据:并发写的处理原则也一 ...

  3. 阿里双十一秒杀系统架构设计,有哪些技术关键点?

    马上要到双11了,就来谈谈如何设计一个秒杀系统架构 技术挑战 1. 对原有业务形成冲击 秒杀活动只是网站营销的一个附加活动,特点是:时间短.并发访问量大,如果和网站原有应用部署在一起,必然会对现有业务 ...

  4. 高并发下的秒杀系统架构设计实战!

    1.秒杀业务分析 正常电子商务流程 (1)查询商品: (2)创建订单: (3)扣减库存: (4)更新订单: (5)付款: (6)卖家发货: 秒杀业务的特性 (1)低廉价格: (2)大幅推广: (3)瞬 ...

  5. 秒杀系统架构设计与分析

    秒杀系统架构分析与实战 2016-01-18陶邦仁Qunar技术沙龙 1 秒杀业务分析 正常电子商务流程 (1)查询商品:(2)创建订单:(3)扣减库存:(4)更新订单:(5)付款:(6)卖家发货 秒 ...

  6. 秒杀系统架构设计思路

    1 .秒杀业务分析 正常电子商务流程 (1)查询商品:(2)创建订单:(3)扣减库存:(4)更新订单:(5)付款:(6)卖家发货 秒杀业务的特性 (1)低廉价格:(2)大幅推广:(3)瞬时售空:(4) ...

  7. 浅谈秒杀系统架构设计

    秒杀是电子商务网站常见的一种营销手段. 原则 不要整个系统宕机. 即使系统故障,也不要将错误数据展示出来. 尽量保持公平公正. 实现效果 秒杀开始前,抢购按钮为活动未开始. 秒杀开始时,抢购按钮可以点 ...

  8. go 商品秒杀系统架构设计

    秒杀抢购背景 秒杀抢购架构设计和模块划分 秒杀抢购接入层实现 电商网站的常规架构 常规状态下电商网站的架构体系如下: 秒杀抢购业务分析 正常电子商务流程: 查询商品 > 创建订单 > 扣减 ...

  9. 网购秒杀系统架构设计

      秒杀是电子商务网站常见的一种营销手段:将少量的商品以极低的价格,在特定的时间点开始出售.秒杀对网站的推广有很多好处,也能给消费者带来利益,但是对网站技术却是极大的挑战:网站是为正常运营设计的,而秒 ...

最新文章

  1. SLAM十四讲笔记1
  2. 20145309 《信息安全系统设计基础》第5周学习总结
  3. 对现有的所能找到的DDOS代码(攻击模块)做出一次分析----自定义攻击篇
  4. Java学习笔记_数组
  5. C++ cin 实现循环读入
  6. 酒厂选址(codevs 1507)
  7. Spring Session 2.0.0.M1 发布,分布式解决方案
  8. river歌曲表达的意思_华晨宇新歌《斗牛》究竟表达了什么?
  9. java 支付宝转账_Java 支付宝支付,退款,单笔转账到支付宝账户(支付宝订单退款)...
  10. 【Data Guard】Oracle DataGuard 搭建
  11. 设计模式学习笔记之代理模式
  12. java项目源码分享网_分享二十套Java项目源码
  13. MarkDown基础语法笔记
  14. IPv6与VoIP——配置Cisco CME实现VoIP实验
  15. AKSHARE 上获取股票数据用于盘后分析以及自己的交易模型的测试。
  16. 通过QQ邮件发送文档到kindle,kindle收不到的问题
  17. 神经网络深度(Deepth)的影响
  18. 微信相框plus详细体验
  19. 初中人教版计算机具体课程,构建信息技术支持下的中学数学课程
  20. [ Azure | Az-900 ] 基础知识点总结(一) - Cloud云概念

热门文章

  1. linux kong_当Linux是善良的面Kong
  2. 程序| 只要使用这个功能,程序运行速度瞬间提升,高到离谱!
  3. 个人博客网站文章添加目录导航
  4. Bootstrap 响应式导航条
  5. CSS 设置列表格式
  6. es6 ArrayBuffer的应用
  7. linux crypto cbc 接口,Linux 2.6.38.4: User-space interface for Crypto API
  8. 看完这篇,你的老电脑能够快到起飞再也不卡!
  9. linux tomcal 看日志,cal命令 – 显示日历
  10. JavaScript小知识点(二):函数防抖和节流