女朋友问阿里双十一实时大屏如何实现,我惊呆一会,马上手把手教她背后的大数据技术(二)
2、Flink简单介绍2009年Flink 诞生于柏林工业大学的一个大数据研究项目 StratoSphere。2014 年孵化出 Flink捐献给Apache,并成为 Apache 顶级项目,同时 Flink 的主流方向被定位为流式计算并大数据行业内崭露头角。2015 年阿里巴巴开始使用 Flink 并持续贡献社区(阿里内部还基于Flink做了一套Blink)2019年1月8日,阿里巴巴以 9000 万欧元(7亿元人民币)收购了创业公司 Data Artisans。 从此Flink开始了新一轮的乘风破浪!在国内流行的一发不可收拾!3、Flink官网介绍:https://flink.apache.org/四、Flink实现双十一实时大屏 在大数据的实时处理中,实时的大屏展示已经成了一个很重要的展示项,比如最有名的双十一大屏实时销售总价展示。 ?今天就做一个最简单的模拟电商统计大屏的小例子,需求如下:1.实时计算出当天零点截止到当前时间的销售总额2.计算出各个分类的销售top33.每秒钟更新一次统计结果实现代码 package cn.lanson.action;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple1;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.functions.windowing.WindowFunction;
import org.apache.flink.streaming.api.windowing.assigners.TumblingProcessingTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.triggers.ContinuousProcessingTimeTrigger;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
/**
* Author Lansonli
* Desc 模拟双十一电商实时大屏显示:
* 1.实时计算出当天零点截止到当前时间的销售总额
* 2.计算出各个分类的销售top3
* 3.每秒钟更新一次统计结果
*/
public class DoubleElevenBigScreem {
public static void main(String[] args) throws Exception {
//1.env
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//2.source
DataStream<Tuple2<String, Double>> dataStream = env.addSource(new MySource());
//3.transformation
DataStream<CategoryPojo> result = dataStream
.keyBy(0)
.window(
//定义大小为一天的窗口,第二个参数表示中国使用的UTC+08:00时区比UTC时间早8小时
TumblingProcessingTimeWindows.of(Time.days(1), Time.hours(-8))
)
.trigger(
ContinuousProcessingTimeTrigger.of(Time.seconds(1))//定义一个1s的触发器
)
.aggregate(new PriceAggregate(), new WindowResult());
//看一下聚合结果
//result.print("初步聚合结果");
//4.使用上面聚合的结果,实现业务需求:
// * 1.实时计算出当天零点截止到当前时间的销售总额
// * 2.计算出各个分类的销售top3
// * 3.每秒钟更新一次统计结果
result.keyBy("dateTime")
.window(TumblingProcessingTimeWindows.of(Time.seconds(1)))//每秒钟更新一次统计结果
.process(new WindowResultProcess());//在ProcessWindowFunction中实现该复杂业务逻辑
env.execute();
}
/**
* 自定义价格聚合函数,其实就是对price的简单sum操作
*/
private static class PriceAggregate implements AggregateFunction<Tuple2<String, Double>, Double, Double> {
@Override
public Double createAccumulator() {
return 0D;
}
@Override
public Double add(Tuple2<String, Double> value, Double accumulator) {
return accumulator + value.f1;
}
@Override
public Double getResult(Double accumulator) {
return accumulator;
}
@Override
public Double merge(Double a, Double b) {
return a + b;
}
}
/**
* 自定义WindowFunction,实现如何收集窗口结果数据
*/
private static class WindowResult implements WindowFunction<Double, CategoryPojo, Tuple, TimeWindow> {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
public void apply(Tuple key, TimeWindow window, Iterable<Double> input, Collector<CategoryPojo> out) throws Exception {
BigDecimal bg = new BigDecimal(input.iterator().next());
double p = bg.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();//四舍五入
CategoryPojo categoryPojo = new CategoryPojo();
categoryPojo.setCategory(((Tuple1<String>) key).f0);
categoryPojo.setTotalPrice(p);
categoryPojo.setDateTime(simpleDateFormat.format(new Date()));
out.collect(categoryPojo);
}
}
/**
* 实现ProcessWindowFunction
* 在这里我们做最后的结果统计,
* 把各个分类的总价加起来,就是全站的总销量金额,
* 然后我们同时使用优先级队列计算出分类销售的Top3,
* 最后打印出结果,在实际中我们可以把这个结果数据存储到hbase或者redis中,以供前端的实时页面展示。
*/
private static class WindowResultProcess extends ProcessWindowFunction<CategoryPojo, Object, Tuple, TimeWindow> {
@Override
public void process(Tuple tuple, Context context, Iterable<CategoryPojo> elements, Collector<Object> out) throws Exception {
String date = ((Tuple1<String>) tuple).f0;
//优先级队列
//实际开发中常使用PriorityQueue实现大小顶堆来解决topK问题
//求最大k个元素的问题:使用小顶堆
//求最小k个元素的问题:使用大顶堆
//https://blog.csdn.net/hefenglian/article/details/81807527
Queue<CategoryPojo> queue = new PriorityQueue<>(3,
(c1, c2) -> c1.getTotalPrice() >= c2.getTotalPrice() ? 1 : -1);//小顶堆
double price = 0D;
Iterator<CategoryPojo> iterator = elements.iterator();
int s = 0;
while (iterator.hasNext()) {
CategoryPojo categoryPojo = iterator.next();
//使用优先级队列计算出top3
if (queue.size() < 3) {
queue.add(categoryPojo);
} else {
//计算topN的时候需要小顶堆,也就是要去掉堆顶比较小的元素
CategoryPojo tmp = queue.peek();//取出堆顶元素
if (categoryPojo.getTotalPrice() > tmp.getTotalPrice()) {
queue.poll();//移除
queue.add(categoryPojo);
}
}
price += categoryPojo.getTotalPrice();
}
//按照TotalPrice逆序
List<String> list = queue.stream().sorted((c1, c2) -> c1.getTotalPrice() <= c2.getTotalPrice() ? 1 : -1)//逆序
.map(c -> "(分类:" + c.getCategory() + " 销售额:" + c.getTotalPrice() + ")")
.collect(Collectors.toList());
System.out.println("时间 :" + date);
System.out.println("总价 : " + new BigDecimal(price).setScale(2, RoundingMode.HALF_UP));
System.out.println("Top3 : \n" + StringUtils.join(list, ",\n"));
System.out.println("-------------");
}
}
/**
* 用于存储聚合的结果
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class CategoryPojo {
private String category;//分类名称
private double totalPrice;//该分类总销售额
private String dateTime;// 截止到当前时间的时间
}
/**
* 模拟生成某一个分类下的订单
*/
public static class MySource implements SourceFunction<Tuple2<String, Double>> {
private volatile boolean isRunning = true;
private Random random = new Random();
String category[] = {
"女装", "男装",
"图书", "家电",
"洗护", "美妆",
"运动", "游戏",
"户外", "家具",
"乐器", "办公"
};
@Override
public void run(SourceContext<Tuple2<String, Double>> ctx) throws Exception {
while (isRunning) {
Thread.sleep(10);
//随机生成一个分类
String c = category[(int) (Math.random() * (category.length - 1))];
//随机生成一个该分类下的随机金额的成交订单
double price = random.nextDouble() * 100;
ctx.collect(Tuple2.of(c, price));
}
}
@Override
public void cancel() {
isRunning = false;
}
}
}五、Flink实现超时订单自动好评在电商领域会有这么一个场景,如果用户买了商品,在订单完成之后一定时间之内没有做出评价,系统自动给与五星好评, 接下来我使用Flink的定时器来实现这一功能。实现代码package cn.lanson.action;
import org.apache.flink.api.common.state.MapState;
import org.apache.flink.api.common.state.MapStateDescriptor;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.util.Collector;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
/**
* Author Lansonli
* Desc 在电商领域会有这么一个场景,如果用户买了商品,在订单完成之后,一定时间之内没有做出评价,系统自动给与五星好评,
* 今天我们使用Flink的定时器来实现这一功能。
*/
public class OrderAutomaticFavorableComments {
public static void main(String[] args) throws Exception {
//env
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment().setParallelism(1);
env.enableCheckpointing(5000);
//source
DataStream<Tuple2<String, Long>> dataStream = env.addSource(new MySource());
//经过interval毫秒用户未对订单做出评价,自动给与好评.为了演示方便,设置了5s的时间
long interval = 5000L;
//分组后使用自定义KeyedProcessFunction完成定时判断超时订单并自动好评
dataStream.keyBy(0).process(new TimerProcessFuntion(interval));
env.execute();
}
/**
* 定时处理逻辑
* 1.首先我们定义一个MapState类型的状态,key是订单号,value是订单完成时间
* 2.在processElement处理数据的时候,把每个订单的信息存入状态中,这个时候不做任何处理,
* 并且注册一个定时器(在订单完成时间+间隔时间(interval)时触发).
* 3.注册的时器到达了订单完成时间+间隔时间(interval)时就会触发onTimer方法,我们主要在这个里面进行处理。
* 我们调用外部的接口来判断用户是否做过评价,
* 如果没做评价,调用接口给与五星好评,如果做过评价,则什么也不处理,最后记得把相应的订单从MapState删除
*/
public static class TimerProcessFuntion extends KeyedProcessFunction<Tuple, Tuple2<String, Long>, Object> {
//定义MapState类型的状态,key是订单号,value是订单完成时间
private MapState<String, Long> mapState;
//超过多长时间(interval,单位:毫秒) 没有评价,则自动五星好评
private long interval = 0L;
public TimerProcessFuntion(long interval) {
this.interval = interval;
}
//创建MapState
@Override
public void open(Configuration parameters) {
MapStateDescriptor<String, Long> mapStateDesc =
new MapStateDescriptor<>("mapStateDesc", String.class, Long.class);
mapState = getRuntimeContext().getMapState(mapStateDesc);
}
//注册定时器
@Override
public void processElement(Tuple2<String, Long> value, Context ctx, Collector<Object> out) throws Exception {
mapState.put(value.f0, value.f1);
ctx.timerService().registerProcessingTimeTimer(value.f1 + interval);
}
//定时器被触发时执行
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<Object> out) throws Exception {
Iterator iterator = mapState.iterator();
while (iterator.hasNext()) {
Map.Entry<String, Long> entry = (Map.Entry<String, Long>) iterator.next();
String orderid = entry.getKey();
boolean evaluated = isEvaluation(entry.getKey()); //调用方法判断订单是否已评价?
mapState.remove(orderid);
if (evaluated) {
System.out.println("订单(orderid: "+orderid+")在"+interval+"毫秒时间内已经评价,不做处理");
}
if (evaluated) {
//如果用户没有做评价,在调用相关的接口给与默认的五星评价
System.out.println("订单(orderid: "+orderid+")超过"+interval+"毫秒未评价,调用接口自动给与五星好评");
}
}
}
//自定义方法实现查询用户是否对该订单进行了评价,我们这里只是随便做了一个判断
//在生产环境下,可以去查询相关的订单系统.
private boolean isEvaluation(String key) {
return key.hashCode() % 2 == 0;
}
}
/**
* 自定义source模拟生成一些订单数据.
* 在这里,我们生了一个最简单的二元组Tuple2,包含订单id和订单完成时间两个字段.
*/
public static class MySource implements SourceFunction<Tuple2<String, Long>> {
private volatile boolean isRunning = true;
@Override
public void run(SourceContext<Tuple2<String, Long>> ctx) throws Exception {
Random random = new Random();
while (isRunning) {
Thread.sleep(1000);
//订单id
String orderid = UUID.randomUUID().toString();
//订单完成时间
long orderFinishTime = System.currentTimeMillis();
ctx.collect(Tuple2.of(orderid, orderFinishTime));
}
}
@Override
public void cancel() {
isRunning = false;
}
}
}六、大数据行业趋势分析1、新基建和数字化转型助力大数据+AI多场景落地新型基础建设场景分析各行业数字化转型场景分析发展新机遇,产业新高度 2、多行业场景大数据应用占比 3、从传统物流到智慧物流演变之旅 赋能新零售:这种收集物流数据、利用物流数据的手段普及到整个零售行业。加持传统物流:物流不等同于快递,与传统物流联手,一定是未来智慧物流的必经之路。 4、智慧物流大数据行业级解决方案基于大型物流公司研发的智慧物流大数据平台,日订单上千万围绕订单、运输、仓储、搬运装卸、包装以及流通加工等物流环节中涉及的数据信息等提高运输以及配送效率、减少物流成本、更有效地满足客户服务要求,并针对数据分析结果,提出具有中观指导意义的解决方案5、大数据行业趋势分析 6、大数据技术框架应用7、大数据开发岗位七、大数据技术知识体系
为什么MySQL字符串不加引号索引失效?《死磕MySQL系列 十一》
群里一个小伙伴在问为什么MySQL字符串不加单引号会导致索引失效,这个问题估计很多人都知道答案。没错,是因为MySQL内部进行了隐式转换。本期文章就聊聊什么是隐式转换,为什么会发生隐式转换。一、几大索引失效原因你肯定在网上看到过非常多关于索引失效原因的文章,但是一定要自己亲手尝试一下,因为版本不同引发的结果不会一致。1.带头大哥不能死这局经典语句是说创建索引要符合最左侧原则。例如表结构为u_id,u_name,u_age,u_sex,u_phone,u_time创建索引为idx_user_name_age_sex。查询条件必须带上u_name这一列。2.不在索引列上做任何操作不在索引列上做任何计算、函数、自动或者手动的类型转换,否则会进行全表扫描。简而言之不要在索引列上做任何操作。3.俩边类型不等例如建立了索引idx_user_name,name字段类型为varchar在查询时使用where name = kaka,这样的查询方式会直接造成索引失效。正确的用法为where name = “kaka”。4.不适当的like查询会导致索引失效创建索引为idx_user_name执行语句为select * from user where name like “kaka%”;可以命中索引。执行语句为select name from user where name like “%kaka”;可以使用到索引(仅在8.0以上版本)。执行语句为select * from user where name like ‘’%kaka";会直接导致索引失效5.范围条件之后的索引会失效创建索引为idx_user_name_age_sex执行语句select * from user where name = ‘kaka’ and age > 11 and sex = 1;上面这条sql语句只会命中name和age索引,sex索引会失效。复合索引失效需要查看key_len的长度即可。总结:%在后边会命令索引,当使用了覆盖索引时任何查询方式都可命中索引。以上就是咔咔关于索引失效会出现的原因总结,在很多文章中没有标注MySQL版本,所以你有可能会看到is null 、or索引会失效的结论。二、从规则方面说明索引失效的原因问题的答案就是第3点,两边类型不一致导致索引失效。下图是表结构,目前这个表存在两个索引,一个主键索引,一个普通索引phone。分别执行以下两条SQL语句explain select * from evt_sms where phone = 13020733815;explain select * from evt_sms where phone = '13020733815';从上图可看出,执行第一条SQL没有使用到索引,第二条SQL却使用到了索引。不错,你也发现了两条SQL的不同,第二条SQL跟第一条SQL逻辑一致,不同的是一个查询条件有引号,一个没有。问题:为什么逻辑相同的SQL却是用不了索引选择索引是优化器大哥的工作,大哥做事肯定轮不到咱们去教,因为大哥有自己的一套规则。对于优化器来说,如果等号两边的数据类型不一致,则会发生隐式转换。例如,explain select * from evt_sms where phone = 13020733815;这条SQL语句就会变为explain select * from evt_sms where cast(phone as signed int) = 13020733815;由于对索引列进行了函数操作,从而导致索引失效。问题:为什么会把左侧的列转为int类型呢?优化器大哥就是根据这个规则进行判断,是把字符串转为数字,还是把数字转为字符串。若返回1,则把字符串转为数字。若返回0,则把数字转为字符串。问题:select * from evt_sms where id = "193014410456945216"这条SQL语句能用上索引吗?如果你忘记了表结构,可以翻到文章开头再看下表evt_sms的索引。可以知道列id添加了主键索引,类型为int类型。根据规则得到,MySQL8.0以上的版本是将字符串转为数字。所以说,函数操作的是等号右边的数据,跟索引列没有关系,所以可以用上索引。那么来到数据库验证一下结论,你答对了吗?三、从索引结构说明索引失效原因有这样一个需求,要统计每年双11注册用户数量。可以看到在evt_sms表中是没有给create_time创建索引的,于是你会执行alter table evt_sms add index idx_ctime(create_time),给create_time添加上索引。接着你就执行了下面的SQL语句。explain select count(*) from evt_sms where month(create_time) = 11;上线没一会数据库出现了大量的慢查询,导致非常多的SQL返回失败。此时公司大牛肯定会直接指出问题,索引列进行函数操作。问题:为什么索引列使用函数就用不上索引了呢?你现在看到的create_time索引结构图。若此时执行的是where create_time = ‘2021-11-16’,那么MySQL就会非常快的等位到对应位置,并返回结果。但是,做了函数操作,例如month(2021-11-16)得到的值是11。当MySQL拿到返回的这个11时,在索引结构中根据就不知道怎么办。MySQL之所以能使用快速定位,是因为B+树的有序性。而使用了函数对索引列进行操作后就会破坏索引的有序性,因此优化器大哥会选择执行代价最低的索引来继续执行。四、结论本期文章给大家介绍了两个案例,一个隐式转换,一个对索引列进行函数操作。两种情况的本质是一样的,都是在索引列上进行了函数操作,导致全表扫描。类似于这两种情况的还是字符集问题,不过一般这个问题会会很少发生,如有新业务需要新创建表,都会设置为之前的字符集。两张表的字符集不同在进行join时也会导致隐式字符集转换,导致索引失效。
数据分析入门系列教程-决策树原理
决策树原理决策树是通过一系列规则对数据进行分类的过程。它提供一种在什么条件下会得到什么值的类似规则的方法。决策树分为分类树和回归树两种,分类树对离散变量做决策树,回归树对连续变量做决策树。也就是说,决策树既可以用来解决分类问题,也可以解决回归问题。决策树其实是一棵倒树,它的根在最上面,叶子在下面。决策树由节点(node)和有向边(directed edge)组成。节点有两种类型:内部节点(internal node)和叶节点(leaf node)。内部节点表示一个特征或属性,叶节点表示一个类。下面我们通过一个小例子来看下,这棵树到底怎么用决定我们是否打篮球的因素有三个,是否下雨,温度是否适合,是否是室内。我们一步一步,通过判断不同的条件,最后得出不同的结论。我们首先选取了决定是否打篮球的条件,并且以是否下雨为根节点开始分裂,然后根据不同情况构造了一棵树,最后根据该树的分枝,来决定是否打篮球。概括来说,就是根据特征的值做判断,最后得出不同的分类。决策树学习过程那么决策树要如何构建呢?通常,这一过程可以概括为3个步骤:特征选择、决策树生成和修剪。特征选择:特征选择是指从训练数据中众多的特征中选择一个特征作为当前节点的分裂标准,如何选择特征有着很多不同量化评估标准标准,从而衍生出不同的决策树算法。决策树生成:根据选择的特征评估标准,从上至下递归地生成子节点,直到数据集不可分则停止决策树生长。剪枝:决策树容易过拟合,一般来需要剪枝,缩小树结构规模、缓解过拟合。剪枝技术有预剪枝和后剪枝两种。特征选择特征选择是为了选择出对于训练数据具有分类能力的特征。比如说,使用某一个特征进行决策树分类的结果与随机分类的结果没有任何区别,那么该特征就不具备分类能力,可以直接丢弃掉该特征。当然我们要选择能够分类出更好的类别的特征,作为根节点。那么一般情况下该如何选择特征呢,业界通常会使用信息增益的方式来选择。先来看几个概念:信息熵条件熵信息增益信息熵信息熵是信息的期望值,是用来衡量信息不确定性的指标。信息的不确定性越大,熵越大。比如说对于抛硬币事件,每次抛硬币的结果就是一个不确定的信息。如果根据我们的经验,一个均匀的硬币正面和反面出现的概率是相等的,都是50%。所以我们很难判断下一次是出现正面还是反面,所以这个事件的信息熵值很高。但是如果该硬币不是均匀的,并且根据历史数据显示,100次试验中,出现正面的次数有98次,那么下一次抛硬币,我们就很容易判断结果了,故而该事件的信息熵很低。其计算公式为:其中:P(X=i)为随机变量 X 取值为 i 的概率n 代表事件的 n 种情况我们还是以上面抛硬币的例子来求解下两种情况下的信息熵各为多少注意,由于编辑器的原因,以下所有 log(2) 均表示以2为底的对数均匀硬币硬币P正面0.5反面0.5信息熵 = -0.5 * log(2)0.5 - 0.5 * log(2)0.5 = 1非均匀硬币硬币P正面0.98反面0.02信息熵 = -0.98 * log(2)0.98 - 0.02 * log(2)0.02 = 0.14思考:如果正面的概率是100%,那么此时的信息熵是多少呢答:此时信息熵是0,也可以说明,信息熵是0到1之间的数值。条件熵条件熵是通过获得更多的信息来减小不确定性。当我们知道的信息越多时,信息的不确定性越小。比如说某个用户曾经购买了一种商品,但是我们没有办法仅仅根据这个信息来判断,该用户是否会再次购买该商品。但是如果我们增加一些条件,比如双十一促销活动信息之后,我们就能够更加容易的发现用户购买商品与促销活动之间的联系,并通过不同促销力度时用户购买的概率来降低不确定性。其计算公式为:其中:Y 为条件P(Y = v)表示 Y 有多种情况,当 Y 处于 v 的概率H(X|Y = v)表示 Y 处于 v 条件时的信息熵信息增仪信息增益的定义就是信息熵减去条件熵这里只是数学上的定义,那么该如何使用信息增益来创建决策树呢,还是举例来看。在上图中,我先按照某种条件,把红点和黄框分成了如图所示的两类,接下来我们计算下熵值父节点信息熵 = -(6/10log(2)6/10)-(4/10log(2)4/10) = 0.97上面子节点信息熵 = -(2/6log(2)2/6)-(4/6log(2)4/6) = 0.91下面子节点信息熵 = -(2/4log(2)2/4)-(2/4log(2)2/4) = 1此处两个子节点其实就是某种条件下的信息熵,因为在分类的时候,是隐含了某种分类条件的。下面根据条件熵的定义,可以得到条件熵条件熵 = 上面子节点出现的概率 * 该条件下的信息熵 + 下面子节点出现的概率 * 该条件下的信息熵= 6/10 * 0.91 + 4/10 * 1= 0.946再根据信息增益的定义,可以得到信息增益信息增益 = 父节点信息熵 - 条件熵= 0.97 - 0.946= 0.024再来看另一种分类方式父节点信息熵 = -(6/10log(2)6/10)-(4/10log(2)4/10) = 0.442上面子节点信息熵 = -(6/6log(2)6/6)-(0/6log(2)0/6) = 0下面子节点信息熵 = -(4/4log(2)4/4)-(0/4log(2)0/4) = 0条件熵 = 0信息增益 = 0.442从图中明显可以看出,第二种分类是好于第一种的,而此时第二种的信息增益是较大的,由此可以得到,在通过信息增益来确定节点时,需要使用信息增益最大的那个特征。? ?也就是说,信息增益是相对于特征而言的,信息增益越大,特征对最终的分类结果影响也就越大,我们就应该选择对最终分类结果影响最大的那个特征作为我们的分类特征。决策树生成构建决策树的工作原理:拿到原始数据集,然后基于最好的属性值划分数据集,由于特征值可能多于两个,因此此时可能存在大于两个分支的数据集划分。第一次划分之后,数据集被向下传递到树的分支的下一个结点。在这个结点上,我们可以再次划分数据。因此我们可以采用递归的原则处理数据集。构建决策树的算法有很多,比如 C4.5、ID3 和 CART,这些算法在运行时并不总是在每次划分数据分组时都会消耗特征。ID3 和 C4.5 算法可以生成二叉树或多叉树,而 CART 只支持二叉树。基于ID3算法ID3 算法其实就是计算信息增益,在决策树各个结点上对应信息增益准则(选取信息增益最大的)选择特征,递归地构建决策树。具体方法是:从根结点(root node)开始,对结点计算所有可能的特征的信息增益,选择信息增益最大的特征作为结点的特征,由该特征的不同取值建立子节点;再对子结点递归地调用以上方法,构建决策树;直到所有特征的信息增益均很小或没有特征可以选择为止,最后得到一个决策树。我们使用如下的数据集来演示下该如何通过 ID3 算法构建决策树,该数据集记录的是某位客户,在不同天气下打高尔夫的情况。weathertempwindplay golfrainyhotfalsenorainyhotturenoovercasthotfalseyessunnymildfalseyessunnycoolfalseyessunnycoolturenoovercastcooltureyesrainymildfalsenorainycoolfalseyessunnymildfalseyesrainymildtureyesovercastmildtureyesovercasthotfalseyessunnymildtureno计算根节点我们首先来计算是否打高尔夫球的信息熵是否打高尔夫频度概率信息熵No55/14 = 0.36-0.531Yes99/14 = 0.64-0.410根据信息熵的计算公式可以得出,Play Golf 的信息熵为H(play golf) = -[-0.531 + (-0.410) ] = 0.940下面计算3种天气状况下的条件熵以天气状况为节点各种天气状况出现的概率weatherYesNo天气状况各情况出现频度概率rainy2355/14 = 0.36overcast4044/14 = 0.29sunny3255/14 = 0.36各种天气状况下的条件熵weatherYes(打高尔夫的概率)No(不打高尔夫的概率)信息熵rainy2/5 = 0.43/5 = 0.60.971overcast4/4 = 10/4 = 00sunny3/5 = 0.62/5 = 0.40.971以 weather 为节点的条件熵 0.36 * 0.971 + 0.29 * 0 + 0.36 * 0.971 = 0.69此时的信息增益为 0.94 - 0.69 = 0.25以温度为节点各种温度出现的概率tempYesNo温度个情况出现频度概率hot2244/14 = 0.29mild4266/14 = 0.43cool3144/14 = 0.29各种温度下的条件熵tempYes(打高尔夫的概率)No(不打高尔夫的概率)信息熵hot2/4 = 0.52/4 = 0.51mild4/6 = 0.672/6 = 0.330.918cool3/4 = 0.751/4 = 0.250.811以 temp 为节点的条件熵 0.29 * 1 + 0.43 * 0.918 + 0.29 * 0.811 = 0.92此时的信息增益为 0.94 - 0.92 = 0.02以风力为节点各种风力出现的概率windYesNo风力各情况出现频度概率false6288/14 = 0.57ture3366/14 = 0.43各种风力下的条件熵windYes(打高尔夫的概率)No(不打高尔夫的概率)信息熵false6/8 = 0.752/8 = 0.250.811ture3/6 = 0.53/6 = 0.51以 wind 为节点的条件熵 0.57 * 0.811 + 0.43 * 1 = 0.89此时的信息增益为 0.94 - 0.89 = 0.05对比上面的信息增益可知,以 weather 为节点分类为最佳,此时可以得到如下的一棵树因为通过观察数据可是,只要是 overcast,一定会去打高尔夫球,所以我们已经得到了一个叶子节点对于 rainy 和 sunny,下面到底需要使用 temp 还是 wind 来最为节点分类,需要继续求解信息增益,以此来确定节点信息。计算第二层在 sunny 为节点的情况下sunny打高尔夫频度概率信息熵Yes33/5 = 0.6-0.442No22/5 = 0.4-0.529信息熵为 0.971以风力为节点在 sunny 情况下各种风力出现的概率windYesNo风力各情况出现频度概率false3033/5 = 0.6ture0222/5 = 0.4各种风力下的条件熵windYes(打高尔夫的概率)No(不打高尔夫的概率)信息熵false3/3 = 10/3 = 00ture0/2 = 02/2 = 10以 wind 为节点的条件熵 0.6 * 0 + 0.4 * 0 = 0此时的信息增益为 0.971 - 0 = 0.971在 sunny 情况下各种温度出现的概率tempYesNo风力各情况出现频度概率mild2133/5 = 0.6cool1122/5 = 0.4各种温度下的条件熵tempYes(打高尔夫的概率)No(不打高尔夫的概率)信息熵mild2/3 = 0.671/3 = 0.330.915cool1/2 = 0.51/2 = 0.51以 temp为节点的条件熵 0.6 * 0.915 + 0.4 * 1 = 0.949此时的信息增益为 0.971 - 0.949 = 0.022因为以 wind 为节点时信息增益大,所以在 sunny 节点下选择以 wind 为节点继续分叉同理我们再来看看 rainy 情况下是怎样的在 rainy 为节点的情况下rainy打高尔夫频度概率信息熵Yes22/5 = 0.4-0.529No33/5 = 0.6-0.442信息熵为 0.971以风力为节点在 rainy 情况下各种风力出现的概率windYesNo风力各情况出现频度概率false1233/5 = 0.6ture1122/5 = 0.4各种风力下的条件熵windYes(打高尔夫的概率)No(不打高尔夫的概率)信息熵false1/3 = 0.332/3 = 0.670.915ture1/2 = 0.51/2 = 0.51以 wind 为节点的条件熵 0.6 * 0.915 + 0.4 * 1 = 0.949此时的信息增益为 0.971 - 0.949 = 0.022在 rainy 情况下各种温度出现的概率tempYesNo风力各情况出现频度概率hot0222/5 = 0.4mild1122/5 = 0.4cool1011/5 = 0.2各种温度下的条件熵tempYes(打高尔夫的概率)No(不打高尔夫的概率)信息熵hot0/2 = 02/2 = 10mild1/2 = 0.51/2 = 0.51cool1/1 = 10/1 = 00以 temp为节点的条件熵 0.4 * 0 + 0.4 * 1 + 0.2 * 0 = 0.4此时的信息增益为 0.971 - 0.4 = 0.571因为以 temp 为节点时信息增益大,所以在 rainy 节点下选择以 temp 为节点继续分叉最后再把 mild 情况继续划分即可最终我们可以得到一棵如下的决策树基于CART算法其实 CRT 算法是基于 ID3 算法而来的,ID3 是基于信息增益做判断,而 CART 选择属性的指标则是基尼系数。基尼系数在经济学中非常出名,它是用来衡量一个国家收入差距的常用指标。当基尼系数大于0.4时,说明财富差异悬殊,当基尼系数在0.2-0.4之间说明分配合理,财富差距不大。在数学上,基尼系数本身反应了样本的不确定度。当基尼系数越小时,说明样本之间的差异性越小,不确定程度低。在构造决策树的过程,其实质就是降低不确定度的过程,所以可以选择基尼系数最小的属性作为属性的划分。其计算公式为:其中:p(Ck|t) 表示节点 t 属于类别 Ck 的概率剪枝那么决策树构造出来后,如果该树的节点过多,树的结构过于复杂,则有可能该模型存在过拟合的风险,此时就需要对决策树进行剪枝的操作。剪枝就是给树瘦身,这一步想实现的目标就是,让树不需要太多的判断,同样可以得到不错的结果。造成过拟合的原因有很多,其中一个原因就是训练集的样本过小。如果决策树选择的属性足够多,那么构造出来的决策树一定可以完美的把训练集中的样本分类,但是这样有可能模型把训练集中一些数据的特点当作了所有数据的特点,但是这些特点并不一定是全部数据的特点,这就使得决策树在真实的数据分类中产生错误,该模型的“泛化能力”就比较差。所谓的泛化能力就是指分类器通过训练集抽象出来的分类能力,即举一反三的能力。决策树常用的剪枝方法有两种:预剪枝(Pre-Pruning)和后剪枝(Post-Pruning)。预剪枝是根据一些原则及早的停止树增长,如树的深度达到用户所要的深度、节点中样本个数少于用户指定个数、不确定度下降的最大幅度小于用户指定的幅度等。预剪枝的核心问题是如何事先指定树的最大深度,如果设置的最大深度不恰当,那么将会导致过于限制树的生长,使决策树的表达式规则趋于一般,不能更好地对新数据集进行分类和预测。另外一个方法来实现预剪枝操作,那就是采用检验技术对当前结点对应的样本集合进行检验,如果该样本集合的样本数量已小于事先指定的最小允许值,那么停止该结点的继续生长,并将该结点变为叶子结点,否则可以继续扩展该结点。后剪枝则是通过在完全生长的树上剪去分枝实现的,通过删除节点的分支来剪去树节点,可以使用的后剪枝方法有多种,比如:代价复杂性剪枝、最小误差剪枝、悲观误差剪枝等等。后剪枝操作是一个边修剪边检验的过程,一般规则标准是:在决策树的不断剪枝操作过程中,将原样本集合或新数据集合作为测试数据,检验决策树对测试数据的预测精度,并计算出相应的错误率,如果剪掉某个子树后的决策树对测试数据的预测精度或其他测度不降低,那么剪掉该子树。为了使决策树达到最好的效果,一般情况下预剪枝和后剪枝都是同时使用。决策树优缺点优点可解释性高,可以可视化能够处理非线性的数据不需要做数据的规范化缺点容易过拟合不稳定,一个微小的变动,都会导致整个树的改变对类别不平衡的数据,支持度不好得到的结果并不一定是最优解总结今天我们讲了决策树的原理,以及信息熵、条件熵和信息增益的原理及详细工作原理。当然,上面的公式太多了,还不是很好了解,需要怎么记忆呢,其实我们可以在一些数据挖掘工具中直接使用封装号的工具来调用它们,比如 sklearn 中就封装了决策树的实现。在这里我们了解决策树的工作原理,才能更好的明白其优缺点,从而在以后的工作中更加灵活的使用。决策树包括根节点,子节点和叶子节点,而决定节点的方法有多种,最常用的就是 ID3 算法和 CART 算法。ID3 算法是基于信息增益来判断分类节点的优劣性的,而 CART 是通过尼基系数来判断。之后我们还介绍了为了方式决策树过拟合,可以采用的剪枝技术,预剪枝和后剪枝。练习题如下的约会数据集,你能否手动推导,生成一棵决策树呢?候选人有房有车帅否是否约会1有无否约2无无是否3有无是约4无有否否5无无无否6无有是约
阿里巴巴在 Envoy Gateway 的演进历程浅析
最近阅读 《Envoy Gateway 来了》这篇文章,深感 Envoy 强大的可扩展性和基于 Envoy Gateway 带来的易用性,在 K8s 架构下,Envoy 重新定义了网关的定位和能力,被誉为云原生网关,甚至被称之为下一代网关。阿里巴巴早在2018年就启动了下一代网关的探索之路,本文将对这个探索历程做一个简单介绍。《Envoy Gateway 来了》中文译文:https://mp.weixin.qq.com/s/7zxu2P789jTr6oE19Pe21g《Envoy Gateway 来了》英文原文:https://www.cncf.io/blog/2022/05/16/introducing-envoy-gateway/Envoy Gateway 1.0(孵化期)上云过程中,我们期望统一应用架构技术栈,但是蚂蚁和阿里巴巴的 RPC 协议不同,存在互调链路长、协议转换消耗大、Tengine Reload 访问有损(接入生效快就需要不断 reload 有损,如果控制 reload 影响,就要减少 reload 次数,接入服务生效慢)、Nginx 内核服务治理能力较弱等问题。因此,需要一个面对未来的网关解决方案。当时,我们有两个技术演进思路,一个是基于 Tengine 进行优化,一个是基于 Envoy 内核来扩展网关场景,考虑到 Tengine 解决这些场景架构变动太大,Envoy 作为网关的第二选项,能够简单的解决上述痛点,因此,我们选择了 Envoy 内核作为下一代的网关演进方向,而且从 CNCF Ingress Provider 的统计数据来看,Envoy 也是增长最快的,社区接受度高。在2020年5月,我们启动了 Envoy Gateway 1.0 的研发,同年成功支撑了双11大促,且成为核心重保的业务链路。Envoy Gateway 1.0主要是应用于东西向流量的 RPC 互通,其架构部署如下图:这个时期,我们面对未来演进了 Dubbo3.0 的 Triple 协议,基于 Envoy,演进了网关的服务管理能力,支撑了当年双十一本地生活战役数十万 TPS 的流量洪峰。Envoy Gateway 2.0(成长期)随着阿里巴巴上云战役的推进,越来越多的场景找到我们,如云上云下业务互通,由于 Tengine 服务管理弱导致阿里内部大量二层微服务网关需要收敛,因此从业务上我们需要做 Tengine+Envoy 两层网关的演进,承担南北向网关流量。在2020年12月份我们开始了2.0架构的演进,下面以优酷场景为例说明演进过程如下图:Envoy Gateway 2.0 南北向的架构图如下:在两层架构中,Envoy 网关更多承担了微服务网关和微服务治理的需求,和 Tengine 流量网关完成了整合。在这个过程中,我们提升了服务治理和高可用能力,并且支撑优酷内部多个二层微服务网关统一,大幅提升性能和运维效率。2.0阶段,Envoy Gateway 完成了东西向、南北向全域流量的调度分发,东西向上不仅支持跨业务域的蚂蚁 RPC 互通,更是扩展到了混合云的云上云下的 RPC 互通场景,包括钉钉文档、阿里视频云、达摩院的店小蜜、智慧数字人等,2.0阶段的业务大图如下(云上云下互通场景,以钉钉为例说明):随着 Envoy Gateway 业务的快速铺开,在跟优酷持续合作时大家不约而同的提出了一个问题:Tengine Gateway(承担流量网关角色) + Envoy Gateway(承担微服务网关角色)的两层网关是否可以合并,使用 Envoy Gateway?答案是肯定的,而且我们也合作设计了新的架构图,如下:这个方案的演进,让我们看到了网关新的发展态势,尤其在以 K8s 主导的容器化背景下,K8s 集群内外网络的天然隔离性,用户也需要一款兼顾高性能与安全性、以及强大服务治理能力的入口网关,这也为我们走向3.0提供了很好的积累。Envoy Gateway 3.0(成熟期)随着阿里巴巴大量场景的打磨,Envoy 网关性能、稳定性都获得了很好的发展。2021年,阿里巴巴开启了中间件三位一体战役,用云产品支撑集团业务,因此我们也将孵化成熟的技术通过 MSE 云原生网关来服务集团。此时,我们通过 Envoy 将流量网关 + 微服务网关合二为一的同时,还通过硬件加速、内核优化等手段,在性能不打折的情况下,持续优化网关的资源部署成本。技术架构决定技术优势,Envoy 天然的可扩展性,还能将丰富的安全认证和微服务治理能力进行集成,体现了云原生网关高聚合的优势,例如:网关直连业务 PodIP,不经过传统 Cluster IP,RT 更低 支持 HTTPS 硬件加速,QPS 提升80%支持 Wasm 插件市场,插件热加载,满足多语言自定义插件需求自研 Multi-Ingress Controller 组件支持多集群 Ingress 复用同一个网关实例原生兼容 K8s Ingress 规范,且支持 Nginx Ingress 核心功能注解的无缝转化回馈社区我们在对 Envoy Gateway 进行演进的过程中,也提了很多社区 issue,包括:dubbo_proxy、wasm、cryptomb等,未来我们会陆续回馈社区,作出更多贡献,和社区共同打造下一代网关。
Seata 企业版正式开放公测
作者:清铭,Seata 创始人Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。Seata 在阿里内部一直扮演着应用架构层数据一致性中间件的角色,几乎每笔交易都要使用 Seata,帮助业务平稳的度过历年的双 11 洪荒流量。开源的短短 3 年间,Seata 社区已经收获了 22k star 和树立了一大批企业标杆用户,成为了分布式事务领域的事实标准。Seata 近期正式发布了 1.5.0 里程碑版本,该版本共有 61 名 contributor 贡献了近 7w+代码,发布一系列重大特性支持如控制台的支持。同时,基于 1.5.0 版本在阿里云上正式开放了 Seata 企业版的公测,企业版可以让用户免去运维的烦恼,内核性能上也有较大幅度的提升,让用户可以快速体验 Seata 的功能和享受云原生时代带来的红利。公测期间用户可以免费开通 Seata 企业版,欢迎大家的试用,也希望您对我们产品提出宝贵的建议。Seata 企业版服务开通入口:https://help.aliyun.com/document_detail/427295.html目前只支持华北 2(北京)地域,公测期间免费使用。Seata 的前世今生Seata 开源之前其内部产品 TXC(taobao transaction constructor)在阿里内部经过了长时间的打磨,在中间件产品矩阵中承担着"服务一致性"的重要角色。它最早诞生于去“IOE”的互联网中间件时代,用于解决阿里内部大规模服务化遇到的数据一致性问题,其后又经历了 ”高可用和统一电商”,“中间件上云”,“云原生中间件”等重要技术架构的演进。在过去几年,开源社区推出了多个为人熟知的开源项目,支持核心业务的中间件系统从封闭走向开放。阿里在 2019 年完成了全站的核心系统上云,三位一体的理念,即“自研”、“开源”、“商业化” 形成统一的体系,成为了现实,最大化发挥了技术的价值。Seata 成为了分布式应用从 0-1 落地过程中的重要基础组件。Seata 的核心业务价值当应用演进为分布式架构后,其分布式事务问题尤为突出,主要产生的原因有:1. 跨数据库操作业务初始阶段往往规模比较小,大多情况下,单库就可以满足需求。随着业务规模变得大而复杂,会出现分库的情况,这时原有的单机事务往往会变成分布式事务。2. 跨系统的分布式事务在和第三方系统(含企业内外)集成时,本地的操作强依赖于第三方系统的响应结果,并且业务要么一起成功,要么一起失效,这时会自然地产生分布式事务的场景。3. 跨服务的分布式事务业务完成服务化改造后,资源与客户端调用解耦,同时又要保证多个服务调用间资源的变化保持强一致,否则会造成业务数据的不完整。4. 跨数据库与消息的分布式事务在某些业务场景中,需要进行多个 DB 操作的同时,还会调用消息系统,DB 操作成功、消息发送失败或者反过来都会造成业务的不完整。Seata 可以轻松解决上述场景中遇到的分布式事务问题,成为分布式架构下资源层数据一致性的连接点,在内部业务中有着广泛的应用,与“三大件” 做了无缝的集成,开发者可以透明的使用 Seata 解决应用架构层的数据一致性问题。与 HSF / Dubbo 集成,用于解决跨服务的数据一致性;与 TDDL / DRDS 集成,用于解决分库分表间的数据一致性;与 MetaQ / RocketMQ 集成,用于解决分布式事务与消息发送成功的一致性。Seata 在其内部日均处理百亿级的的事务量,可用性和性能 SLA 均达到 99.99%,全年无故障。其独创的 AT 事务模式,实现了毫秒级的事务处理,3 节点集群可达近 10w tps 的并发事务处理。Seata 通过框架层面解决业务过程中产生的分布式事务问题,使架构师更聚焦于业务架构本身,无需再关注数据一致性的设计。通过简单易用的无侵入方案,降低了开发者的学习成本,提升了开发的效率。Seata 开放公测核心优势Seata 企业版 100% 兼容开源,开源自建切换至企业版 0 迁移成本,用户只需简单的更改 endpoint 就可以使用 Seata 企业版。相比开源自建, Seata 企业版核心优势如下:免搭建:人工维护成本低。易用性:白屏化运维操作。高可用:多可用区部署、故障自动检测及恢复、弹性伸缩、SLA 保障。性能:企业版相比开源在内核层性能提升 30%+,综合考虑其他方面提升约 100%。监控:具备专业的 APM 监控和报警。安全:支持鉴权,内核安全加固优化、数据加密。开源自建与企业版性能对比企业版内核在性能和稳定性上进行了深度优化。企业版相比开源版内核 rt 降低 20% 以上,tps 提升 30%,考虑到相比自建的参数调优、依赖组件调优和基础设施等优势,同规格整体性能预计提升约 100%+。同时,企业版解决了高并发场景下的事务处理“毛刺”问题。服务开通通过开源自建的方式搭建 Seata 集群需要搭建 Server 依赖的存储、注册中心、配置中心、修改和同步配置、修改元数据、添加事务分组和启动 Server 七个步骤来完成。Seata 企业版提供了 Server 节点、依赖的存储和 NamingServer(注册中心和配置中心)的托管,免去用户搭建的烦恼,只需在阿里云微服务引擎 MSE 分布式事务控制台点击“创建 Seata 实例”和“创建事务分组”就可完成创建操作,目前只支持华北 2(北京)区域开通。步骤一:创建 Seata 实例步骤二:创建事务分组目前 Seata 企业版不能在非阿里云环境下直连,如遇本地开发调试可安装 CloudToolkit 插件,启用 “端云互联” 功能进行网络连通,详细介绍参照分布式事务服务开通文档。答疑支持如果您在开通使用过程中遇到任何问题,可以钉钉扫码加入以下 “MSE-Seata 企业用户支持群”,我们将第一时间处理您的问题。
JavaScript实现“双11”秒杀,你也可以
个人简介??个人主页:微风洋洋?♂?博客领域:编程基础,后端写作风格:干货,干货,还是tmd的干货精选专栏:【JavaScript】支持洋锅:点赞、收藏?、留言好久不见,甚是想念!大家好!我是微风洋洋 每年的“双11”啊,都是大家的剁手节。大家都在晚上12点,捧着手机看着倒计时,在他倒数到0的时候疯狂点击下单。可是你有没想过限时秒杀是怎么实现的呢?简单点倒计时是怎么实现的呢?今天,洋锅就给你揭秘,看了今天这篇文章,你也可以给自己来个“限时秒杀”@TOC【案例】限时秒杀代码实现思路:① 设计限时秒杀页面。② 指定限时秒杀的结束时间,及其对应的毫秒数。③ 获取当前时间的毫秒数。④ 计算当前与秒杀结束的时间差,大于0,计算剩余的天时分秒;否则结束秒杀。⑤ 利用定时器完成秒杀的倒计时功能。⑥ 利用两位数字显示秒杀的时间。代码实现
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>限时秒杀</title>
<style>
.box{margin:0 auto;position:relative;background:url(?label/sc/de-3-100148/images/flash_sale.png);width:702px;height:378px;}
.box div{float:left;width:50px;height:50px;border:1px solid #ccc;margin-left:5px;line-height:50px;text-align: center;color:red;position:relative;top:260px;left:305px;}
</style>
</head>
<body>
<div class="box">
<div id="d"></div> <!-- 剩余的天数 -->
<div id="h"></div> <!-- 剩余的小时 -->
<div id="m"></div> <!-- 剩余的分钟 -->
<div id="s"></div> <!-- 剩余的秒数 -->
</div>
<script>
// 设置秒杀结束时间
// var endtime = new Date('2017-11-10 18:51:00'), endseconds = endtime.getTime();
// 设置据当前时间开始,秒杀的结束时间
var endtime = new Date(), endseconds = endtime.getTime() + 60 * 1000;
// 声明变量保存剩余的时间
var d = h = m = s = 0;
// 设置定时器,实现限时秒杀效果
var id = setInterval(seckill, 1000);
function seckill() {
var nowtime = new Date(); // 获取当前时间
// 获取时间差,单位秒
var remaining = parseInt((endseconds - nowtime.getTime()) / 1000);
// 判断秒杀是否过期
if (remaining > 0) {
d = parseInt(remaining / 86400); // 计算剩余天数(除以60*60*24取整,获取剩余的天数)
h = parseInt((remaining / 3600) % 24); // 计算剩余小时(除以60*60转换为小时,与24取模,获取剩余的小时)
m = parseInt((remaining / 60) % 60); // 计算剩余分钟(除以60转为分钟,与60取模,获取剩余的分钟)
s = parseInt(remaining % 60); // 计算剩余秒(与60取模,获取剩余的秒数)
// 统一利用两位数表示剩余的天、小时、分钟、秒
d = d < 10 ? '0' + d : d;
h = h < 10 ? '0' + h : h;
m = m < 10 ? '0' + m : m;
s = s < 10 ? '0' + s : s;
} else {
clearInterval(id); // 秒杀过期,取消定时器
d = h = m = s = '00';
}
// 将剩余的天、小时、分钟和秒显示到指定的网页中
document.getElementById('d').innerHTML = d + '天';
document.getElementById('h').innerHTML = h + '时';
document.getElementById('m').innerHTML = m + '分';
document.getElementById('s').innerHTML = s + '秒';
}
</script>
</body>
</html>为了方便一部分同学想要快速学会用来装逼 ==人前显圣==,洋锅贴心的把最后的案例调到了最前面,不用感谢我,来个三连支持就拿去用吧。看到这还没有离开的筒子们,想必是想理解其中原理吧,那就认真的往下看吧,看完后你绝对能够==显圣的更加地道==。一、全局作用域window对象:是BOM中所有对象的核心,同时也是BOM中所有对象的父对象。定义在全局作用域中的变量、函数以及JavaScript中的内置函数都可以被window对象调用。定义在全局作用域中的getArea()函数,函数体内的this关键字指向window对象。对于window对象的属性和方法在调用时可以省略window,直接访问其属性和方法即可。注意在JavaScript中直接使用一个未声明的变量会报语法错误,但是使用“window.变量名”的方式则不会报错,而是获得一个undefined结果。除此之外,delete关键字仅能删除window对象自身的属性,对于定义在全局作用域下的变量不起作用。二、弹出对话框和窗口window对象中除了前面提过的alert()和prompt()方法外,还提供了很多弹出对话框和窗口的方法,以及相关的操作属性。所有的属性和方法在常见的浏览器(如IE、Chrome等)中全部支持。prompt()方法作用:用于生成用户输入的对话框。第1个参数:用于设置用户输入的提示信息。第2个参数:用于设置输入框中的默认信息。代码实现confirm()方法作用:弹出一个确认对话框,该对话框中包含提示消息以及确认和取消按钮。参数:用于设置确认的提示信息。返回值:点击“确定”按钮,返回true。点击“取消”按钮,返回false。open()方法作用:用于打开一个新的浏览器窗口,或查找一个已命名的窗口。语法:open(URL, name, specs, replace)。第1个参数:打开指定页面的URL地址,若没有指定,则打开一个新的空白窗口。第2个参数:指定target属性或窗口的名称。第3个参数:用于设置浏览器窗口的特征(如大小、位置、滚动条等),多个特征之间使用逗号分隔。第4个参数:设置为true,表示替换浏览历史中的当前条目,设置false(默认值),表示在浏览历史中创建新的条目。注意与open()方法功能相反的是close()方法,用于关闭浏览器窗口,调用该方法的对象就是需要关闭的窗口对象。?♂?举个例子代码实现
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>打开和关闭窗口</title>
</head>
<body>
<p><input type="button" value="打开窗口" onclick="openWin()"></p>
<p><input type="button" value="关闭窗口" onclick="closeWin()"></p>
<p><input type="button" value="检测窗口是否关闭" onclick="checkWin()"></p>
<p id="msg"></p>
<script>
var myWindow;
function openWin() {
myWindow = window.open('', 'newWin', 'width=400,height=200,left=200');
myWindow.document.write('<p>窗口名称为:' + myWindow.name + '</p>');
myWindow.document.write('<p>当前窗口的父窗口地址:' + window.parent.location + '</p>');
}
function closeWin() {
myWindow.close();
}
function checkWin() {
if (myWindow) {
var str = myWindow.closed ? '窗口已关闭!' : '窗口未关闭!';
} else {
var str = '窗口没有被打开!';
}
document.getElementById('msg').innerHTML = str;
}
</script>
</body>
</html>三、窗口位置和大小BOM中用来获取或更改window窗口位置,窗口高度与宽度,文档区域高度与宽度的相关属性和方法有很多。目前只有window.open()方法打开的的窗口和选项卡(Tab),FireFox和Chrome浏览器才支持口位置和大小的调整。?♂?举个例子代码实现
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>窗口位置和大小</title>
</head>
<body>
<input type="button" value="打开窗口" onclick="openWin()">
<input type="button" value="调整窗口位置和大小" onclick="changeWin()">
<input type="button" value="再调整窗口位置和大小" onclick="changeWin1()">
<script>
var myWindow;
function openWin() {
myWindow = window.open('', 'newWin', 'width=250,height=300');
getPosSize(); // 获取窗口信息
}
function changeWin() {
myWindow.moveBy(250, 250); // 将newWin窗口下移250像素,右移250像素
myWindow.focus(); // 获取移动后newWin窗口的焦点
myWindow.resizeTo(500, 350); // 修改newWin窗口的宽度为500,高度为350
getPosSize(); // 获取窗口信息
}
function changeWin1() {
myWindow.moveTo(250, 250); // 将newWin窗口下移250像素,右移250像素
myWindow.focus(); // 获取移动后newWin窗口的焦点
myWindow.resizeBy(500, 350); // 修改newWin窗口的宽度为500,高度为350
getPosSize(); // 获取窗口信息
}
function getPosSize() {
// 获取相对于屏幕窗口的坐标
var x = myWindow.screenLeft, y = myWindow.screenTop;
// 获取窗口和文档的高度和宽度
var inH = myWindow.innerHeight, inW = myWindow.innerWidth;
var outH = myWindow.outerHeight, outW = myWindow.outerWidth;
myWindow.document.write('<p>相对屏幕窗口的坐标:(' + x + ',' + y + ')</p>');
myWindow.document.write('<p>文档的高度和宽度:' + inH + ',' + inW + '</p>');
myWindow.document.write('<p>窗口的高度和宽度:' + outH + ',' + outW + '</p><hr>');
}
</script>
</body>
</html>四、框架操作window对象提供的frames属性可通过集合的方式,获取HTML页面中所有的框架,length属性就可以获取当前窗口中frames的数量。除此之外,还可以利用parent获取当前window对象所在的父窗口。五、定时器JavaScript中可通过window对象提供的方法实现在指定时间后执行特定操作,也可以让程序代码每隔一段时间执行一次,实现间歇操作。setTimeout()和setInterval()方法区别:相同点:都可以在一个固定时间段内执行JavaScript程序代码。不同点:setTimeout()只执行一次代码,setInterval()会在指定的时间后,自动重复执行代码。提示setTimeout()方法在执行一次后即停止了操作;setInterval()方法一旦开始执行,在不加干涉的情况下,间歇调用将会一直执行到页面关闭为止。若要在定时器启动后,取消该操作,可以将setTimeout()的返回值(定时器ID)传递给clearTimeout()方法;或将setInterval()的返回值传递给clearInterval()方法。?♂?举个例子代码实现
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>计数器</title>
</head>
<body>
<input type="button" value="开始计数" onclick="startCount()">
<input id="num" type="text">
<input type="button" value="停止计数" onclick="stopCount()">
<script>
var timer = null, c = 0;
function timedCount() { // 在文本框中显示数据
document.getElementById('num').value = c;
++c; // 显示数据加1
}
function startCount() { // 开始间歇调用
timer = setInterval(timedCount, 1000);
}
function stopCount() { // 清除间歇调用
clearInterval(timer);
}
</script>
</body>
</html>如果觉得这篇文章对你有一丢丢启发的话,不妨 点赞、收藏?、留言支持一下,你的支持将是我继续创作的最大动力??????由于作者水平有限,如有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!if (学会了){点赞收藏给个好评,我祝福您学啥会啥;}else{点赞收藏给个好评,我相信您定能学会;}
打了BOM,来了DOM?
个人简介??个人主页:微风洋洋?♂?博客领域:编程基础,后端写作风格:干货,干货,还是tmd的干货精选专栏:【JavaScript】支持洋锅:点赞、收藏?、留言好久不见,甚是想念!大家好!我是微风洋洋 之前洋锅花了两篇文章来介绍BOM,没想到刚讲完啦BOM,就来了DOM。就好像是打了小的,来了老的,是不是玩不起?==那DOM有时何许人也?是BOM的爹地嘛?==传送门什么,BOM指的是物料清单?JavaScript实现“双11”秒杀,你也可以@TOC一、DOM对象简介 什么是DOMDOM:==Document Object Model,文档对象模型。==作用:是一套规范文档内容的通用型标准。DOM发展历史1.时间:1998年10月。事件:DOM正式成为W3C的推荐标准。名称:第1级DOM(DOM Level 1,或DOM1)。作用:为XML和HTML文档中的元素、节点、属性等提供了必备的属性和方法。参考:最初结合了Netscape及微软公司开发的DHTML(动态HTML)思想。2.时间:2000年11月。事件:发布了第2级DOM(DOM Level 2,或DOM2)。作用:在DOM1的基础上增加了样式表对象模型。3.名称:第3级DOM(DOM Level 3,或DOM3) 。作用:在DOM2基础上增加了内容模型、文档验证以及键盘鼠标事件等功能。提示:直到目前为止,DOM几乎被所有浏览器所支持。 DOM HTML节点树DOM HTML指的是DOM中为操作HTML文档提供的属性和方法。文档(Document)表示HTML文件。文档中的标签称为元素(Element)。文档中的所有内容称为节点(Node)。因此,一个HTML文件可以看作是所有元素组成的一个节点树,各元素节点之间有级别的划分HTML文档根据节点作用,分为标签节点、文本节点、属性节点和注释节点。各节点之间的关系,又可分为以下几个方面:根节点:标签是整个文档的根节点,有且仅由一个。子节点:指的是某一个节点的下级节点。父节点:指的是某一个节点的上级节点。兄弟节点:两个节点同属于一个父节点。 DOM对象的继承关系通过前面的学习可以知道,在JavaScript中要对网页中的元素进行操作,可以利用document对象的getElementById()方法实现,但是此方法的返回值类型是什么?下面通过代码进行查看。 总结document和Element是两种不同类型的节点(Node)对象。它们不仅能够使用Node对象的一系列属性和方法完成节点操作。也可以使用特有的属性和方法完成不同类型节点的操作。除了document和Element对象,还有其他几种类型的节点对象也继承Node对象,如文本(Text)、注释(Comment)等。举个例子代码实现
<div id="test"></div>
<script>
var test = document.getElementById('test');
console.log(test); // 输出结果:<div id="test"></div>
console.log(test.__proto__); // 输出结果:HTMLDivElement { …… }
</script>
<div id="test"></div>
<script>
var test = document.getElementById('test');
console.log(test.nodeName); // 通过节点方式获取节点名,输出结果:DIV
console.log(test.tagName); // 通过元素方式获取标签名,输出结果:DIV
console.log(document.nodeName); // document属于节点,输出结果:#document
console.log(document.tagName); // document不属于元素,输出结果:undefined
</script>
<script>
var test = document.getElementById('test');
test.nodeType === Node.ELEMENT_NODE; // 比较结果:true
document.nodeType === Node.DOCUMENT_NODE; // 比较结果:true
<script>二、DOM节点操作 获取节点由于HTML文档可以看做是一个节点树,因此,可以利用操作节点的方式操作HTML中的元素。==childNodes属性与前面学习过的children属性的区别。==相同点:都可以获取某元素的子元素。不同点: childNodes属性用于节点操作,返回值中还会包括文本节点等其他类型的节点,是一个NodeList对象的集合。 children属性用于元素操作,返回的是HTMLCollection对象的集合举个例子代码实现
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>节点操作</title>
</head>
<body>
<ul id="ul">
<li>JS</li>
<li>BOM</li>
<li>DOM</li>
<!--注释-->
</ul>
<script>
var ul = document.getElementById('ul'); // 根据id获取ul的元素对象
console.log(ul.childNodes); // 查看ul下的所有节点
</script>
</body>
</html> 注意childNodes属性在IE6~8不会获取文本节点,在IE9及以上版本和主流浏览器中则可以获取文本节点。此外,由于document对象继承自Node节点对象,因此document对象也可以进行以上的节点操作。 节点追加在获取元素的节点后,还可以利用DOM提供的方法实现节点的添加,如创建一个li元素节点,为li元素节点创建一个文本节点等。create系列的方法是由document对象提供的,与Node对象无关。举个例子代码实现
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>节点追加</title>
</head>
<body>
<script>
var h2 = document.createElement('h2'); // 创建h2元素节点
var text = document.createTextNode('Hello JavaScript'); // 创建文本节点
var attr = document.createAttribute('align'); // 创建属性节点
attr.value = 'center'; // 为属性节点赋值
h2.setAttributeNode(attr); // 为h2元素添加属性节点
h2.appendChild(text); // 为h2元素添加文本节点
document.body.appendChild(h2); // 将h2节点追加为body元素的子节点
console.log(document.getElementsByTagName('h2')[0]);
</script>
</body>
</html> 节点删除语法:removeChild()和removeAttributeNode()方法实现。返回值:是被移出的元素节点或属性节点。举个例子代码实现
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>删除节点和节点属性</title>
</head>
<body>
<ul>
<li>PHP</li><li>JavaScript</li><li class="strong">UI</li>
</ul>
<script>
var child = document.getElementsByTagName('li')[2]; // 获取第3个li元素
var attr = child.getAttributeNode('class'); // 获取元素的class属性值
console.log(child.removeAttributeNode(attr)); // 删除元素的class属性值
console.log(child.parentNode.removeChild(child)); // 删除元素
</script>
</body>
</html>如果觉得这篇文章对你有一丢丢启发的话,不妨 点赞、收藏?、留言支持一下,你的支持将是我继续创作的最大动力??????由于作者水平有限,如有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!if (学会了){点赞收藏给个好评,我祝福您学啥会啥;}else{点赞收藏给个好评,我相信您定能学会;}
企业级RDS MySQL数据库产品优势与价值
企业级RDS MySQL数据库产品优势与价值: 1. 可用性业界第一 最高保障99.99%可用性,包括:三可用区部署的企业版,双可用区部署的高可用本地盘版独享和独占实例。 秒级高可用,最快10秒完成主备切换,透明代理服务极大限度降低切换给应用带来的影响。 2. 企业级数据库安全 事前、事中、事后全方位数据安全保护矩阵。 企业版提供RPO=0数据库解决方案,数据100%零丢失。 按时间点秒级恢复,异地恢复,闪回保障数据安全。 3. 易扩展 最快秒级升级能力满足动态业务需求。 只读实例、分析实例随业务需求动态扩展。 自动化读写分离功能,业务零改造。 4. 高性能 AliSQL内核优化,性能比开源高80%。 动态自适应线程池,高并发状态下性能保持一致稳定。 Query Cache提高至少一倍读性能。 经历双十一的内核大并发更新写能力,最高TPS5w。
《性能优化》并发与并行
前言性能优化系列第一篇主要给大家科普了一些性能相关的数字,为大家建立性能的初步概念。第二篇给大家介绍了支撑淘宝双十一这种达到百万QPS项目所需的相关核心技术。本文带来的是性能优化中的第一利器:并发与并行。除了核心原理介绍外,我将结合我自身的过去的实战经验,给出一些自己在使用上的建议,希望对大家有帮助。不多废话,直接开怼。正文1、并发和并行?并发和并行最关键的区别是:并行是同时执行,而并发不是同时。这边使用Joe Armstrong 排队使用咖啡机的例子来看并行和并发的区别,如下图所示:上半部分为并发:两个队伍交替使用咖啡机下半部分为并行:两个队伍同时使用咖啡机从和我们更相关的CPU的角度来看两者的区别。并发是这样的:同一时刻只有一个任务执行。并行是这样的:同一时刻有多个任务执行。并发和并行结合起来是这样的:2、并发一定能提升性能吗?并行能提升性能大家不会有太多的疑问,但是并发是否一定能提升性能,估计还是有不少同学会有疑问。答案是否定的,并发不一定能提升性能,但是在绝大多数场景都能提升性能。什么场景下并发不能提升性能?我们举个简单的例子:假设我们的服务器配置为单核CPU,要执行10个任务,10个任务都是CPU计算密集型任务,此时单线程执行效率理论上要比开10个线程执行要快。在执行的整个过程中,基本都是CPU在运行,但是开十个线程会涉及到线程上下文切换,需要花费一些时间,导致反而更慢。再举个更形象的例子:囧辉上语文开小差,被老师罚抄10篇课文,此时囧辉脑子里想到了两种方法。方法1:先抄完第一篇,再抄第二篇,再抄第三篇,直到抄完第十篇。方法2:先抄第一篇的第一段,再抄第二篇的第一段,...,再抄第十篇的第一段,再抄第一篇的第二段,直到抄完全部。方法1为串行执行,方法2为并发执行,相信大家都能很容易看出方法二反而会更慢,因为我们在从切换不同文章时,需要先放好原来的文章,然后找新文章抄到哪个位置了,这个过程需要花费一些时间,这个过程就类似于线程上下文切换。那什么场景下并发会提升性能了?再举个例子:囧辉要烧10壶水,一壶水烧开的时间为1分钟。串行执行:囧辉先烧第一壶,第一壶烧开了后接着烧第二壶,直到烧完第十壶,这个方法烧完十壶水大概需要10分钟。并发执行:囧辉先烧第一壶,没等第一壶烧开,接着烧第二壶,就这样,囧辉一下子将十壶水都放到灶台上同时烧,这个方法烧完十壶水大概需要1分钟。在这个场景里,并发执行就体现了很大的优化,性能提升了接近10倍。在我们实际项目中,大部分应用场景都是第二类,因此并发大多时候能提升性能,而哪些动作是“烧开水”了,这个其实在性能优化第一篇里提到了,最常见的“烧开水”操作就是I/O操作,最常见的如:调用其他服务的RPC接口查询数据、查询MySQL数据库获取数据等等。3、实现方式并发/并行的实现方式通常有两种,如下。1)开线程直接怼,每循环一次都会新建一个线程来执行,例如下面代码,public static void test() throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
// 烧水
boilingWater();
countDownLatch.countDown();
}).start();
}
// 等待处理结束
countDownLatch.await();
}2)使用线程池,例如下面代码。public static void test() {
List<Future> futureList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
futureList.add(THREAD_POOL_EXECUTOR.submit(() -> {
// 烧开水
boilingWater();
}));
}
for (Future future : futureList) {
try {
// 等待处理结束
future.get();
} catch (Exception e) {
e.printStackTrace();
}
}
}1是反例,实际项目中不要使用,就算只开1个线程,也要用线程池,因为每次创建和回收线程都是需要开销的。下面用一个简单的demo来模拟“烧开水”的例子public class BoilingWaterTest {
?
/**
* CPU的核数
*/
private static final int NCPUS = Runtime.getRuntime().availableProcessors();
?
/**
* 创建线程池
*/
private static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
NCPUS,
NCPUS * 2,
30,
TimeUnit.MINUTES,
new LinkedBlockingDeque<>(1000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
?
public static void main(String[] args) throws Exception {
serial();
concurrent();
}
?
public static void serial() {
// 串行执行
long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
boilingWater();
}
System.out.println("serial cost:" + (System.currentTimeMillis() - start));
}
?
public static void concurrent() throws InterruptedException {
// 并发执行
CountDownLatch countDownLatch = new CountDownLatch(10);
long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
THREAD_POOL_EXECUTOR.execute(() -> {
boilingWater();
countDownLatch.countDown();
});
}
// 等待任务全部执行完毕
countDownLatch.await();
System.out.println("concurrent cost:" + (System.currentTimeMillis() - start));
}
?
public static void boilingWater() {
try {
// 烧开一壶水需要1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}执行该方法输出如下,符合我们的预期。serial cost:10091
concurrent cost:1048此时并发执行的流程就如下图,从一个task拆出多个task,然后由每个CPU负责处理1个,因此处理时间接近于1个任务的处理时间。4、线程池的参数设置1)线程数之前的线程池面试文章里有介绍过线程数的设置,这边直接复制过来:要想合理的配置线程池大小,首先我们需要区分任务是计算密集型还是I/O密集型。对于计算密集型,设置线程数 = CPU数 + 1,通常能实现最优的利用率。对于I/O密集型,网上常见的说法是设置线程数 = CPU数 * 2 ,这个做法是可以的,但个人觉得不是最优的。在我们日常的开发中,我们的任务几乎是离不开I/O的,常见的网络I/O(RPC调用)、磁盘I/O(数据库操作),并且I/O的等待时间通常会占整个任务处理时间的很大一部分,在这种情况下,开启更多的线程可以让 CPU 得到更充分的使用,一个较合理的计算公式如下:线程数= CPU数 * CPU利用率 * (任务等待时间/ 任务计算时间 + 1)例如我们有个定时任务,部署在4核的服务器上,该任务有100ms在计算,900ms在I/O等待,则线程数约为:4 * 1 * (1 + 900 / 100) = 40个。当然,具体我们还要结合实际的使用场景来考虑。如果要求比较精确,可以通过压测来获取一个合理的值。上述是比较理想的线程数计算方式,在实际项目使用中,如果无法很准确的计算,那么可以先用我上面的线程池配置,也就是:corePoolSize?= CPU核数maximumPoolSize =?CPU核数 * 2这个参数设置可能不是最理想的,但在大多数情况下都是一个还不错的选择,比较合适。2)keepAliveTime、TimeUnit这两个参数一起决定了非核心线程空闲后的存活时间。这两个参数说实话并不是非常重要,实际使用过程中不要设置太离谱的值一般问题不大,我个人一般使用5分钟或30分钟。3)workQueue工作队列,当核心线程处理不过来时,任务会堆积在队列里。常见的队列有ArrayBlockingQueue 和?LinkedBlockingQueue,两者的主要区别在于?ArrayBlockingQueue?占用空间会更小,而?LinkedBlockingQueue?在生产者和消费者使用了不同的锁性能会好一点。通常情况下,两者的区别微乎其微,除非你要处理的任务量非常非常大,此时你需要仔细考虑使用哪个更合适,否则通常情况下两个随便选都可以。常见的坑:使用LinkedBlockingQueue 时没设置队列大小,也就是使用了无界队列(Integer.MAX_VALUE),任务处理不过来,不断积压在队列里,最终造成内存溢出。线程池使用不当导致内存溢出的case我已经见过很多次了,这个经验大家一定要铭记在心:使用LinkedBlockingQueue 一定要设置队列大小。另外,这边给大家介绍下另一个我常用的工作队列:SynchronousQueue。SynchronousQueue 不是一个真正的队列,而是一种在线程之间移交的机制。要将一个元素放入SynchronousQueue 中,必须有另一个线程正在等待接受这个元素。如果没有线程等待,并且线程池的当前大小小于 maximumPoolSize,那么线程池将创建一个线程,否则根据拒绝策略,这个任务将被拒绝。使用直接移交将更高效,因为任务会直接移交给执行它的线程,而不是被放在队列中,然后由工作线程从队列中提取任务。只有当线程池是无界的或者可以拒绝任务时,该队列才有实际价值,Executors.newCachedThreadPool使用了该队列。上述内容里提到了:当线程池是无界的或者可以拒绝任务时,该队列才有实际价值。使用无界的线程池说实话挺危险的,我强烈建议不要使用,特别是经验不太丰富的新人。因此我们在使用 SynchronousQueue 的时候可以理解为一定会出现任务被拒绝的情况,因此要选择好合适的拒绝策略。SynchronousQueue 我一般会搭配 CallerRunsPolicy 使用,个人觉得这2个是个绝佳组合,这个组合起到的效果是:当线程池处理不过来时,直接交由调用者线程(往线程池里添加任务的主线程)来执行,此时任务不会被积压在队列里,同时调用者线程无法继续提交任务。简单来说:任务处理非常高效,没有任务积压的概念不会有内存溢出的风险,同时在线程池处理不过来时具有控制任务提交速度的效果。4)ThreadFactory线程工厂,这个没啥好说的,通常使用默认的就行。常见的改动场景是:给线程设置个自定义的名字,方便区分。这种场景下,可以使用一些工具类提供的现有方法,也可以将 DefaultThreadFactory 拷贝出来自己修改一下。5)RejectedExecutionHandler拒绝策略,线程池处理不过来时的策略。默认有4种策略,其中3种我个人比较常用到。AbortPolicy:默认的策略,直接抛出异常,没有特殊需求直接使用该策略即可。CallerRunsPolicy:调用者线程执行策略,该策略上面提到了,我一般是配合SynchronousQueue 使用,起到一个控制任务提交速度的效果。DiscardPolicy:抛弃策略,直接丢掉要提交的任务,这个策略一般在线程池执行的是不太重要的任务时使用。5、并发并行适用于哪种场景典型的适合使用并发并行的场景通常有以下特点:1)存在I/O操作,并且I/O操作有多次,最典型的就是RPC调用和查询数据库2)I/O操作比较耗时,越耗时越有优化价值3)多次I/O操作之间没有依赖关系,可以同时调用总结并发和并行是性能优化中非常常用的手段,使用起来非常简单,并且带来的性能提升通常非常明显,很容易就有几倍几倍的提升,快在自己的项目中用起来吧。最后我是囧辉,一个坚持分享原创技术干货的程序员,如果觉得本文对你有帮助,欢迎一键三连。推荐阅读Java 基础高频面试题(2021年最新版)Java 集合框架高频面试题(2021年最新版)面试必问的 Spring,你懂了吗?面试必问的 MySQL,你懂了吗?
百万级QPS,支撑淘宝双11需要哪些技术
目录前言正文1、MySQL硬抗2、分布式缓存(Tair)硬抗3、客户端分布式缓存4、缓存预热5、客户端本地缓存6、访问DB加锁7、热点探测8、限流9、全链路压测10、预案11、降级部分非核心功能12、监控大盘13、加机器14、其他最后推荐阅读前言又到一年双11,相信大部分同学都曾经有这个疑问:支撑起淘宝双11这么大的流量,需要用到哪些核心技术?性能优化系列的第二篇我想跟大家探讨一下这个话题。完整的双11链路是非常长的,我当前也没这个实力跟大家去探讨完整的链路,本文只跟大家探讨其中的一个核心环节:商品浏览。商品浏览是整个链路中流量最大的或者至少是之一,这个大家应该不会有疑问,因为几乎每个环节都需要商品浏览。阿里云公布的2020年双11订单创建峰值是58.3万笔/秒,而我们在下单前经常会点来点去看这个看那个的,因此商品浏览肯定至少在百万QPS级别。废话不多说,直接开怼。正文1、MySQL硬抗不知道有没有老铁想过用MySQL硬抗双11百万QPS,反正我是没有,不过我们还是用数据来说说为什么不可能这么做。根据MySQL官方的基准测试,MySQL在通常模式下的性能如下图所示:当然这个数据仅供参考,实际性能跟使用的机器配置、数据量、读写比例啥的都有影响。首先,淘宝的数据量肯定是比较大的,这个毋庸置疑,但是无论怎么分库分表,由于成本等原因,肯定每个库还是会有大量的数据。我当前所在的业务刚好数据量也比较大,我们DBA给的建议是单库QPS尽量控制在5000左右,实际上有可能到1万也没问题,但是此时可能存在潜在风险。DBA给的这个建议值是比较稳健保守的,控制在这个值下基本不会出问题,因此我们尽量按DBA的建议来,毕竟他们在这方面会更专业一些。如果按照单库抗5000来算,即使多加几个从库,也就抗个十来万QPS顶天了,要抗百万QPS就根本不可能了,流量一进来,DB肯定马上跪成一片灰烬。有同学可能会想,能不能无限加从库硬怼?这个是不行的,因为从库是需要占用主库资源的,看过我之前MySQL面试题的同学应该知道,主库需要不断和从库进行通信,传输binlog啥的,从库多了主库会受影响,无限加从库最后的结果肯定是将主库怼挂了,我们这边的建议值是从库数量尽量不要超过20个,超了就要想其他法子来优化。2、分布式缓存(Tair)硬抗上分布式缓存硬抗应该是大部分老哥会想到的,我们也用数据来分析一下可行性。阿里用的分布式缓存是自研的 Tair,不知道的可以理解为 Redis 吧,对外其实也是说的 Redis 企业版。Tair官方自称性能约为同规格社区版实例的3倍。阿里云官网上,Tair企业版性能增强-集群版当前的实例规格如下图所示:右下角最猛的【4096GB集群性能增强版】的QPS参考值超过6000万+,没错,我数了好几遍,就是6000万,我的龟龟,太变态了。直接把【4096GB集群性能增强版】怼上去就解决了,还要啥优化。如果一个解决不了,大不了就两个嘛。分布式缓存确实是大多数情况下抗读流量的主力,所以用Tair硬抗的方案肯定是没大问题的,但是我们需要思考下是否存在以一些细节问题,例如:·???????分布式缓存通常放在服务端,上游通过RPC来调用获取商品信息,百万级的流量瞬间打进来,是否会直接将RPC的线程池打挂?·???????缓存里的信息通常是先查询DB再写到缓存里,百万级的流量瞬间打进来,是否会直接将DB打挂?·???????是否存在热点商品,导致Tair单节点扛不住?·???????...这些问题我们接下来一一讨论。3、客户端分布式缓存分布式缓存放在服务端,我们称之为服务端分布式缓存,但是要使用服务端分布式缓存需要上游进行RPC调用,请求量太大会带来隐患,同时带来了额外的网络请求耗时。为了解决这个问题,我们引入客户端分布式缓存,所谓客户端分布式缓存就是将请求Tair的流程集成在SDK里,如果Tair存在数据,则直接返回结果,无需再请求到服务端。这样一来,商品信息只要在Tair缓存里,请求到客户端就会结束流程,服务端的压力会大大降低,同时实现也比较简单,只是将服务端的Tair请求流程在SDK里实现一遍。4、缓存预热为了解决缓存为空穿透到DB将DB打挂的风险,可以对商品进行预热,提前将商品数据加载到Tair缓存中,将请求直接拦截在Tair,避免大量商品数据同时穿透DB,打挂DB。具体预热哪些商品了?这个其实不难选择,将热点商品统计出来即可,例如以下几类:1)在双11零点付款前,大部分用户都会将要买的商品放到购物车,因此可以对购物车的数据进行一个统计,将其中的热点数据计算出来即可。2)对一些有参与优惠或秒杀活动的商品进行统计,参与活动的商品一般也会被抢购的比较厉害。3)最近一段时间销量比较大的商品,或者浏览量比较大的商品。4)有参与到首页活动的商品,最近一段时间收藏夹的商品等等...淘宝背后有各种各样的数据,统计出需要预热的商品并不难。通过预热,可以大大降低DB被穿透的风险。5、客户端本地缓存阿里云官网的数据【4096GB集群性能增强版】的QPS参考值超过6000万+,但是这个值是在请求分布比较均匀的情况下的参考值,256个分片上每个分片二三十万这样。通常个别分片高一点也没事,五六十万估计也ok,但是一般不能高太多,否则可能出现带宽被打满、节点处理不过来等情况,导致整个集群被打垮。这个时候就需要祭出我们的最终神器了,也就是本地缓存。本地缓存的性能有多牛逼了,我们看下这张图。这张图是caffeine(一个高性能Java缓存框架)官方提供的本地测试结果,并不是服务器上的测试结果。测试运行在 MacBook Pro i7-4870HQ CPU @ 2.50GHz (4 core) 16 GB Yosemite系统,简单来说,比较一般的配置,大部分服务器配置应该都会比这个高。在这个基准测试中, 8 线程对一个配置了最大容量的缓存进行并发读。可以看到,caffeine支持每秒操作数差不多是1.5亿,而另一个常见的缓存框架Guava差不多也有2000多万的样子。而在服务器上测试结果如下:服务器配置是单插槽 Xeon E5-2698B v3 @ 2.00GHz (16 核, 禁用超线程),224 GB,Ubuntu 15.04。可以看到caffeine在使用16线程时支持每秒操作数已经达到3.8亿次,其他的框架也基本都是千万级以上。通过上面的数据,大家应该都明白了,本地缓存在抗读流量上理论上是无敌的。当然本地缓存有一些缺点,例如需要占用服务器的本地内存,因此通常我们只会存储少量的热点数据,严格配置好参数,控制好本地缓存的占用内存上限,避免影响服务器本身的使用。因此,我们会对之前的热点数据,再进行一次筛选,选出“热点中的热点”,将这些数据提前预热到本地缓存中。可能有同学会问,如果本地缓存里的商品数据发生了变更,怎么办?一个办法是使用类似ZK的监听方式,当本地缓存的商品发生变更时,触发更新操作,本地缓存去拉取最新数据,因为本地缓存的商品数较少,所以ZK整体压力不会太大。另一个办法是本地缓存定期拉取最新数据,例如每隔N秒后,就主动查询一次DB,将数据更新为最新数据,具体延迟几秒,根据业务上能接受的来控制。具体选哪种看业务的选择吧,这些被筛选进入本地缓存的数据基本都是最热的那些商品,无论是商家还是运营都心里有数,肯定在活动前会再三确认,所以出现变更的几率其实不大。6、访问DB加锁尽管我们对热点数据进行了预热,但是我们必须考虑到可能会有这么一些缓存击穿的场景:1)某个热点数据在缓存里失效了,大量流量瞬间打到DB,导致DB被打垮。2)某个商品并不属于热点商品,所以并没有预热,但是在活动开始后成为热点商品,导致流量大量打到DB,DB被瞬间打垮。等等,这些场景都可能会导致DB瞬间被打垮,DB是生命线,DB一挂就凉了,因此我们必须要有相应的措施来应对。解决方案在之前讲缓存击穿的文章里其实提过了,就是在访问DB时加锁,保证单台服务器上对于同一个商品在同一时刻,只会有一个线程去请求DB,其他的全部原地阻塞等待该线程返回结果。注意,这边我们是不会加分布式锁的,只会加JVM锁,因为JVM锁保证了在单台服务器上只有一个请求走到数据库,通常来说已经足够保证数据库的压力大大降低,同时在性能上比分布式锁更好。这个在Guava中就有现成的实现,有兴趣的可以看看。7、热点探测我们上述所说的热点商品都是基于已有数据的分析,属于静态数据,难免会有漏掉的,因此也需要有办法能实时的探测出热点数据,从而进行缓存,保护系统稳定。8、限流无论你想的多么齐全,真正面临线上考验的时候,经常会出现一些你没考虑到的情况,因此,我们必须要有最终的保护措施。限流降级作为最后一道防御墙,不到万不得已我们不希望使用到他,但是我们必须做好准备,万一发生没预料到的情况,可以保证大部分用户不会受到影响。9、全链路压测模拟双11当天的流量进行测试,系统到底能抗多少,只有压测一下才知道,同时压测出来的指标,也会作为我们设置限流值很重要的参考依据。10、预案预案是指根据评估分析或经验,对潜在的或可能发生的突发事件的类别和影响程度而事先制定的应急处置方案。简单来说就是关键时刻一键拉闸,直接切换某些功能或者关闭降级某些功能,以保障核心功能不会受到影响。11、降级部分非核心功能在双11高峰期将一些非核心功能进行降级,避免影响核心流程,例如我记得订单是超过几个月就不让查。12、监控大盘各项核心指标都需要有监控和告警,例如:缓存命中率、服务端请求QPS、DB读写QPS等等。同时要设置合理的告警值,万一出现异常可以及时感知及时解决。同时要有一个核心业务监控大盘,放置最核心的业务指标监控,方便大家及时了解业务核心指标情况。13、加机器虽然很多人会看不起加机器解决,但是实际上加机器是必不可少的,简单粗暴,花钱就能解决。14、其他以上列的只是我当前能想到的,实际上肯定比这要多的多。最后我是囧辉,一个坚持分享原创技术干货的程序员。由于本人实力有限难免会有错误,欢迎大家指教,如果有疑问,也欢迎留言探讨。推荐阅读Java 基础高频面试题(2021年最新版)Java 集合框架高频面试题(2021年最新版)面试必问的 Spring,你懂了吗?面试必问的 MySQL,你懂了吗?