哨兵模式实现原理?

2020-08-28 08:57发布

哨兵模式实现原理?

哨兵模式实现原理?

3条回答
冬瓜
1楼 · 2020-08-28 09:14.采纳回答

  哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。

  通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。
然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
用文字描述一下故障切换(failover)的过程。假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。

敦敦宁
2楼 · 2020-08-28 10:45

  Sentinel 是阿里中间件团队开源的,面向分布式服务架构的轻量级高可用流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助用户保护服务的稳定性。

大家可能会问:Sentinel 和之前常用的熔断降级库 Netflix Hystrix 有什么异同呢?Sentinel官网有一个对比的文章,这里摘抄一个总结的表格,具体的对比可以点此 链接 查看。

对比内容SentinelHystrix隔离策略信号量隔离线程池隔离/信号量隔离熔断降级策略基于响应时间或失败比率基于失败比率实时指标实现滑动窗口滑动窗口(基于 RxJava)规则配置支持多种数据源支持多种数据源扩展性多个扩展点插件的形式基于注解的支持支持支持限流基于 QPS,支持基于调用关系的限流不支持流量整形支持慢启动、匀速器模式不支持系统负载保护支持不支持控制台开箱即用,可配置规则、查看秒级监控、机器发现等不完善常见框架的适配Servlet、Spring Cloud、Dubbo、gRPC 等Servlet、Spring Cloud Netflix

从对比的表格可以看到,Sentinel比Hystrix在功能性上还要强大一些,本文让我们一起来了解下Sentinel的源码,揭开Sentinel的神秘面纱。

项目结构

将Sentinel的源码fork到自己的github库中,接着把源码clone到本地,然后开始源码阅读之旅吧。

首先我们看一下Sentinel项目的整个结构:



  • sentinel-core 核心模块,限流、降级、系统保护等都在这里实现

  • sentinel-dashboard 控制台模块,可以对连接上的sentinel客户端实现可视化的管理

  • sentinel-transport 传输模块,提供了基本的监控服务端和客户端的API接口,以及一些基于不同库的实现

  • sentinel-extension 扩展模块,主要对DataSource进行了部分扩展实现

  • sentinel-adapter 适配器模块,主要实现了对一些常见框架的适配

  • sentinel-demo 样例模块,可参考怎么使用sentinel进行限流、降级等

  • sentinel-benchmark 基准测试模块,对核心代码的精确性提供基准测试

运行样例

基本上每个框架都会带有样例模块,有的叫example,有的叫demo,sentinel也不例外。

那我们从sentinel的demo中找一个例子运行下看看大致的情况吧,上面说过了sentinel主要的核心功能是做限流、降级和系统保护,那我们就从“限流”开始看sentinel的实现原理吧。

可以看到sentinel-demo模块中有很多不同的样例,我们找到basic模块下的flow包,这个包下面就是对应的限流的样例,但是限流也有很多种类型的限流,我们就找根据qps限流的类看吧,其他的限流方式原理上都大差不差。

public class FlowQpsDemo {

 private static final String KEY = "abc";

 private static AtomicInteger pass = new AtomicInteger();
 private static AtomicInteger block = new AtomicInteger();
 private static AtomicInteger total = new AtomicInteger();

 private static volatile boolean stop = false;

 private static final int threadCount = 32;

 private static int seconds = 30;

 public static void main(String[] args) throws Exception {
        initFlowQpsRule();

        tick();
 // first make the system run on a very low condition
        simulateTraffic();

 System.out.println("===== begin to do flow control");
 System.out.println("only 20 requests per second can pass");

    }

 private static void initFlowQpsRule() {
 List rules = new ArrayList();
 FlowRule rule1 = new FlowRule();
        rule1.setResource(KEY);
 // set limit qps to 20
        rule1.setCount(20);
 // 设置限流类型:根据qps
        rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
        rule1.setLimitApp("default");
        rules.add(rule1);
 // 加载限流的规则
 FlowRuleManager.loadRules(rules);
    }

 private static void simulateTraffic() {
 for (int i = 0; i < threadCount; i++) {
 Thread t = new Thread(new RunTask());
            t.setName("simulate-traffic-Task");
            t.start();
        }
    }

 private static void tick() {
 Thread timer = new Thread(new TimerTask());
        timer.setName("sentinel-timer-task");
        timer.start();
    }

 static class TimerTask implements Runnable {

 @Override
 public void run() {
 long start = System.currentTimeMillis();
 System.out.println("begin to statistic!!!");

 long oldTotal = 0;
 long oldPass = 0;
 long oldBlock = 0;
 while (!stop) {
 try {
 TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                }
 long globalTotal = total.get();
 long oneSecondTotal = globalTotal - oldTotal;
                oldTotal = globalTotal;

 long globalPass = pass.get();
 long oneSecondPass = globalPass - oldPass;
                oldPass = globalPass;

 long globalBlock = block.get();
 long oneSecondBlock = globalBlock - oldBlock;
                oldBlock = globalBlock;

 System.out.println(seconds + " send qps is: " + oneSecondTotal);
 System.out.println(TimeUtil.currentTimeMillis() + ", total:" + oneSecondTotal
                    + ", pass:" + oneSecondPass
                    + ", block:" + oneSecondBlock);

 if (seconds-- <= 0) {
                    stop = true;
                }
            }

 long cost = System.currentTimeMillis() - start;
 System.out.println("time cost: " + cost + " ms");
 System.out.println("total:" + total.get() + ", pass:" + pass.get()
                + ", block:" + block.get());
 System.exit(0);
        }
    }

 static class RunTask implements Runnable {
 @Override
 public void run() {
 while (!stop) {
 Entry entry = null;

 try {
                    entry = SphU.entry(KEY);
 // token acquired, means pass
 pass.addAndGet(1);
                } catch (BlockException e1) {
                    block.incrementAndGet();
                } catch (Exception e2) {
 // biz exception
                } finally {
                    total.incrementAndGet();
 if (entry != null) {
                        entry.exit();
                    }
                }

 Random random2 = new Random();
 try {
 TimeUnit.MILLISECONDS.sleep(random2.nextInt(50));
                } catch (InterruptedException e) {
 // ignore
                }
            }
        }
    }
}

执行上面的代码后,打印出如下的结果:

可以看到,上面的结果中,pass的数量和我们的预期并不相同,我们预期的是每秒允许pass的请求数是20个,但是目前有很多pass的请求数是超过20个的。

原因是,我们这里测试的代码使用了多线程,注意看 threadCount 的值,一共有32个线程来模拟,而在RunTask的run方法中执行资源保护时,即在 SphU.entry 的内部是没有加锁的,所以就会导致在高并发下,pass的数量会高于20。

可以用下面这个模型来描述下,有一个TimeTicker线程在做统计,每1秒钟做一次。有N个RunTask线程在模拟请求,被访问的business code被资源key保护着,根据规则,每秒只允许20个请求通过。

由于pass、block、total等计数器是全局共享的,而多个RunTask线程在执行SphU.entry申请获取entry时,内部没有锁保护,所以会存在pass的个数超过设定的阈值。

那为了证明在单线程下限流的正确性与可靠性,那我们的模型就应该变成了这样:

那接下来我把 threadCount 的值改为1,只有一个线程来执行这个方法,看下具体的限流结果,执行上面的代码后打印的结果如下:

可以看到pass数基本上维持在20,但是第一次统计的pass值还是超过了20。这又是什么原因导致的呢?

其实仔细看下Demo中的代码可以发现,模拟请求是用的一个线程,统计结果是用的另外一个线程,统计线程每1秒钟统计一次结果,这两个线程之间是有时间上的误差的。从TimeTicker线程打印出来的时间戳可以看出来,虽然每隔一秒进行统计,但是当前打印时的时间和上一次的时间还是有误差的,不完全是1000ms的间隔。

要真正验证每秒限制20个请求,保证数据的精准性,需要做基准测试,这个不是本篇文章的重点,有兴趣的同学可以去了解下jmh,sentinel中的基准测试也是通过jmh做的。

深入原理

通过一个简单的示例程序,我们了解了sentinel可以对请求进行限流,除了限流外,还有降级和系统保护等功能。那现在我们就拨开云雾,深入源码内部去一窥sentinel的实现原理吧。

首先从入口开始: SphU.entry() 。这个方法会去申请一个entry,如果能够申请成功,则说明没有被限流,否则会抛出BlockException,表面已经被限流了。

从 SphU.entry() 方法往下执行会进入到 Sph.entry() ,Sph的默认实现类是 CtSph,在CtSph中最终会执行到 entry(ResourceWrapperresourceWrapper,intcount,Object...args)throwsBlockException 这个方法。

我们来看一下这个方法的具体实现:

public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
 Context context = ContextUtil.getContext();
 if (context instanceof NullContext) {
 // Init the entry only. No rule checking will occur.
 return new CtEntry(resourceWrapper, null, context);
    }

 if (context == null) {
        context = MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType());
    }

 // Global switch is close, no rule checking will do.
 if (!Constants.ON) {
 return new CtEntry(resourceWrapper, null, context);
    }

 // 获取该资源对应的SlotChain
 ProcessorSlot chain = lookProcessChain(resourceWrapper);

 /*
     * Means processor cache size exceeds {@link Constants.MAX_SLOT_CHAIN_SIZE}, so no
     * rule checking will be done.
     */
 if (chain == null) {
 return new CtEntry(resourceWrapper, null, context);
    }

 Entry e = new CtEntry(resourceWrapper, chain, context);
 try {
 // 执行Slot的entry方法
        chain.entry(context, resourceWrapper, null, count, args);
    } catch (BlockException e1) {
        e.exit(count, args);
 // 抛出BlockExecption
 throw e1;
    } catch (Throwable e1) {
 RecordLog.info("Sentinel unexpected exception", e1);
    }
 return e;
}

这个方法可以分为以下几个部分:

  • 1.对参数和全局配置项做检测,如果不符合要求就直接返回了一个CtEntry对象,不会再进行后面的限流检测,否则进入下面的检测流程。

  • 2.根据包装过的资源对象获取对应的SlotChain

  • 3.执行SlotChain的entry方法

    • 3.1.如果SlotChain的entry方法抛出了BlockException,则将该异常继续向上抛出

    • 3.2.如果SlotChain的entry方法正常执行了,则最后会将该entry对象返回

  • 4.如果上层方法捕获了BlockException,则说明请求被限流了,否则请求能正常执行

其中比较重要的是第2、3两个步骤,我们来分解一下这两个步骤。

创建SlotChain

首先看一下lookProcessChain的方法实现:

private ProcessorSlot lookProcessChain(ResourceWrapper resourceWrapper) {
 ProcessorSlotChain chain = chainMap.get(resourceWrapper);
 if (chain == null) {
 synchronized (LOCK) {
            chain = chainMap.get(resourceWrapper);
 if (chain == null) {
 // Entry size limit.
 if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
 return null;
                }

 // 具体构造chain的方法
                chain = Env.slotsChainbuilder.build();
 Map newMap = new HashMap(chainMap.size() + 1);
                newMap.putAll(chainMap);
                newMap.put(resourceWrapper, chain);
                chainMap = newMap;
            }
        }
    }
 return chain;
}



该方法使用了一个HashMap做了缓存,key是资源对象。这里加了锁,并且做了 doublecheck 。具体构造chain的方法是通过: Env.slotsChainbuilder.build() 这句代码创建的。


那就进入这个方法看看吧。

public ProcessorSlotChain build() {
 ProcessorSlotChain chain = new DefaultProcessorSlotChain();
    chain.addLast(new NodeSelectorSlot());
    chain.addLast(new ClusterBuilderSlot());
    chain.addLast(new LogSlot());
    chain.addLast(new StatisticSlot());
    chain.addLast(new SystemSlot());
    chain.addLast(new AuthoritySlot());
    chain.addLast(new FlowSlot());
    chain.addLast(new DegradeSlot());

 return chain;
}



Chain是链条的意思,从build的方法可看出,ProcessorSlotChain是一个链表,里面添加了很多个Slot。具体的实现需要到DefaultProcessorSlotChain中去看。

public class DefaultProcessorSlotChain extends ProcessorSlotChain {

 AbstractLinkedProcessorSlot first = new AbstractLinkedProcessorSlot() {
 @Override
 public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, Object... args)
 throws Throwable {
 super.fireEntry(context, resourceWrapper, t, count, args);
        }
 @Override
 public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
 super.fireExit(context, resourceWrapper, count, args);
        }
    };

 AbstractLinkedProcessorSlot end = first;

 @Override
 public void addFirst(AbstractLinkedProcessorSlot protocolProcessor) {
        protocolProcessor.setNext(first.getNext());
        first.setNext(protocolProcessor);
 if (end == first) {
 end = protocolProcessor;
        }
    }

 @Override
 public void addLast(AbstractLinkedProcessorSlot protocolProcessor) {
 end.setNext(protocolProcessor);
 end = protocolProcessor;
    }
}

DefaultProcessorSlotChain中有两个AbstractLinkedProcessorSlot类型的变量:first和end,这就是链表的头结点和尾节点。

创建DefaultProcessorSlotChain对象时,首先创建了首节点,然后把首节点赋值给了尾节点,可以用下图表示:


将第一个节点添加到链表中后,整个链表的结构变成了如下图这样:

将所有的节点都加入到链表中后,整个链表的结构变成了如下图所示:

这样就将所有的Slot对象添加到了链表中去了,每一个Slot都是继承自AbstractLinkedProcessorSlot。而AbstractLinkedProcessorSlot是一种责任链的设计,每个对象中都有一个next属性,指向的是另一个AbstractLinkedProcessorSlot对象。其实责任链模式在很多框架中都有,比如Netty中是通过pipeline来实现的。

知道了SlotChain是如何创建的了,那接下来就要看下是如何执行Slot的entry方法的了。

执行SlotChain的entry方法

lookProcessChain方法获得的ProcessorSlotChain的实例是DefaultProcessorSlotChain,那么执行chain.entry方法,就会执行DefaultProcessorSlotChain的entry方法,而DefaultProcessorSlotChain的entry方法是这样的:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, Object... args)
 throws Throwable {
    first.transformEntry(context, resourceWrapper, t, count, args);
}

也就是说,DefaultProcessorSlotChain的entry实际是执行的first属性的transformEntry方法。

而transformEntry方法会执行当前节点的entry方法,在DefaultProcessorSlotChain中first节点重写了entry方法,具体如下:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, Object... args)
 throws Throwable {
 super.fireEntry(context, resourceWrapper, t, count, args);
}

first节点的entry方法,实际又是执行的super的fireEntry方法,那继续把目光转移到fireEntry方法,具体如下:

@Override
public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args)
 throws Throwable {
 if (next != null) {
 next.transformEntry(context, resourceWrapper, obj, count, args);
    }
}

从这里可以看到,从fireEntry方法中就开始传递执行entry了,这里会执行当前节点的下一个节点transformEntry方法,上面已经分析过了,transformEntry方法会触发当前节点的entry,也就是说fireEntry方法实际是触发了下一个节点的entry方法。

具体的流程如下图所示:



从图中可以看出,从最初的调用Chain的entry()方法,转变成了调用SlotChain中Slot的entry()方法。从上面的分析可以知道,SlotChain中的第一个Slot节点是NodeSelectorSlot。

执行Slot的entry方法

现在可以把目光转移到SlotChain中的第一个节点NodeSelectorSlot的entry方法中去了,具体的代码如下:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args)
 throws Throwable {

 DefaultNode node = map.get(context.getName());
 if (node == null) {
 synchronized (this) {
            node = map.get(context.getName());
 if (node == null) {
                node = Env.nodeBuilder.buildTreeNode(resourceWrapper, null);
 HashMap cacheMap = new HashMap(map.size());
                cacheMap.putAll(map);
                cacheMap.put(context.getName(), node);
                map = cacheMap;
            }
 // Build invocation tree
            ((DefaultNode)context.getLastNode()).addChild(node);
        }
    }

    context.setCurNode(node);
 // 由此触发下一个节点的entry方法
    fireEntry(context, resourceWrapper, node, count, args);
}

从代码中可以看到,NodeSelectorSlot节点做了一些自己的业务逻辑处理,具体的大家可以深入源码继续追踪,这里大概的介绍下每种Slot的功能职责:

  • NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;

  • ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;

  • StatistcSlot 则用于记录,统计不同纬度的 runtime 信息;

  • FlowSlot 则用于根据预设的限流规则,以及前面 slot 统计的状态,来进行限流;

  • AuthorizationSlot 则根据黑白名单,来做黑白名单控制;

  • DegradeSlot 则通过统计信息,以及预设的规则,来做熔断降级;

  • SystemSlot 则通过系统的状态,例如 load1 等,来控制总的入口流量;

执行完业务逻辑处理后,调用了fireEntry()方法,由此触发了下一个节点的entry方法。此时我们就知道了sentinel的责任链就是这样传递的:每个Slot节点执行完自己的业务后,会调用fireEntry来触发下一个节点的entry方法。

所以可以将上面的图完整了,具体如下:


至此就通过SlotChain完成了对每个节点的entry()方法的调用,每个节点会根据创建的规则,进行自己的逻辑处理,当统计的结果达到设置的阈值时,就会触发限流、降级等事件,具体是抛出BlockException异常。


总结

sentinel主要是基于7种不同的Slot形成了一个链表,每个Slot都各司其职,自己做完分内的事之后,会把请求传递给下一个Slot,直到在某一个Slot中命中规则后抛出BlockException而终止。

前三个Slot负责做统计,后面的Slot负责根据统计的结果结合配置的规则进行具体的控制,是Block该请求还是放行。

控制的类型也有很多可选项:根据qps、线程数、冷启动等等。

然后基于这个核心的方法,衍生出了很多其他的功能:

  • 1、dashboard控制台,可以可视化的对每个连接过来的sentinel客户端 (通过发送heartbeat消息)进行控制,dashboard和客户端之间通过http协议进行通讯。

  • 2、规则的持久化,通过实现DataSource接口,可以通过不同的方式对配置的规则进行持久化,默认规则是在内存中的

  • 3、对主流的框架进行适配,包括servlet,dubbo,rRpc等


我是大脸猫
3楼 · 2020-08-28 10:58

所谓“哨兵”就是用一个特殊值来作为数组的边界,使用“哨兵”可以少用一条判断语句,所以可以提高程序的效率。

比如从整数数组arr中,查找有没有整数num。

应用:假设一个乱序数组,需要查找一个元素是否在该数组中,这时需要用到顺序查找,也就是遍历数组。

一般情况下我们会写下如下代码:

int Sequential_Search(int *a,int n,int key)

{

//数组从1开始

int i;

for(int i=1;i<=n;i++)

{

if(a[i]==key)

return i;

}

return 0;//查找失败

}

有的数据结构书上,会运用哨兵元素,改成这样的代码:

int Sequential_Search2(int *a int n,int key)

{

int i=0;

a[0]=key;//哨兵

i=n;

while(a[i]!=key)

{

i--;

}

return i;//返回0就是查找失败

}


相关问题推荐

  • 什么是大数据时代?2021-01-13 21:23
    回答 100

    大数据(big data)一词越来越多地被提及,人们用它来描述和定义信息爆炸时代产生的海量数据,而这个海量数据的时代则被称为大数据时代。随着云时代的来临,大数据(Big data)也吸引了越来越多的关注。大数据(Big data)通常用来形容一个公司创造的大量非结...

  • 回答 84

    Java和大数据的关系:Java是计算机的一门编程语言;可以用来做很多工作,大数据开发属于其中一种;大数据属于互联网方向,就像现在建立在大数据基础上的AI方向一样,他两不是一个同类,但是属于包含和被包含的关系;Java可以用来做大数据工作,大数据开发或者...

  • 回答 52
    已采纳

    学完大数据可以从事很多工作,比如说:hadoop 研发工程师、大数据研发工程师、大数据分析工程师、数据库工程师、hadoop运维工程师、大数据运维工程师、java大数据工程师、spark工程师等等都是我们可以从事的工作岗位!不同的岗位,所具备的技术知识也是不一样...

  • 回答 29

    简言之,大数据是指大数据集,这些数据集经过计算分析可以用于揭示某个方面相关的模式和趋势。大数据技术的战略意义不在于掌握庞大的数据信息,而在于对这些含有意义的数据进行专业化处理。大数据的特点:数据量大、数据种类多、 要求实时性强、数据所蕴藏的...

  • 回答 14

    tail -f的时候,发现一个奇怪的现象,首先 我在一个窗口中 tail -f test.txt 然后在另一个窗口中用vim编辑这个文件,增加了几行字符,并保存,这个时候发现第一个窗口中并没有变化,没有将最新的内容显示出来。tail -F,重复上面的实验过程, 发现这次有变化了...

  • 回答 18

    您好针对您的问题,做出以下回答,希望有所帮助!1、大数据行业还是有非常大的人才需求的,对于就业也有不同的岗位可选,比如大数据工程师,大数据运维,大数据架构师,大数据分析师等等,就业难就难在能否找到适合的工作,能否与你的能力和就业预期匹配。2、...

  • 回答 17

    最小的基本单位是Byte应该没多少人不知道吧,下面先按顺序给出所有单位:Byte、KB、MB、GB、TB、PB、EB、ZB、YB、DB、NB,按照进率1024(2的十次方)计算:1Byte = 8 Bit1 KB = 1,024 Bytes 1 MB = 1,024 KB = 1,048,576 Bytes 1 GB = 1,024 MB = 1,048,576...

  • 回答 33

    大数据的定义。大数据,又称巨量资料,指的是所涉及的数据资料量规模巨大到无法通过人脑甚至主流软件工具,在合理时间内达到撷取、管理、处理、并整理成为帮助企业经营决策更积极目的的资讯。大数据是对大量、动态、能持续的数据,通过运用新系统、新工具、新...

  • 回答 5

    MySQL是一种关系型数据库管理系统,关系数据库将数据保存在不同的表中,而不是将所有数据放在一个大仓库内,这样就增加了速度并提高了灵活性。MySQL的版本:针对不同的用户,MySQL分为两种不同的版本:MySQL Community Server社区版本,免费,但是Mysql不提供...

  • mysql安装步骤mysql 2022-05-07 18:01
    回答 2

    mysql安装需要先使用yum安装mysql数据库的软件包 ;然后启动数据库服务并运行mysql_secure_installation去除安全隐患,最后登录数据库,便可完成安装

  • 回答 5

    1.查看所有数据库showdatabases;2.查看当前使用的数据库selectdatabase();3.查看数据库使用端口showvariableslike&#39;port&#39;;4.查看数据库编码showvariableslike‘%char%’;character_set_client 为客户端编码方式; character_set_connection 为建立连接...

  • 回答 5

    CREATE TABLE IF NOT EXISTS `runoob_tbl`(    `runoob_id` INT UNSIGNED AUTO_INCREMENT,    `runoob_title` VARCHAR(100) NOT NULL,    `runoob_author` VARCHAR(40) NOT NULL,    `submission_date` DATE,    PRI...

  • 回答 9

    学习多久,我觉得看你基础情况。1、如果原来什么语言也没有学过,也没有基础,那我觉得最基础的要先选择一种语言来学习,是VB,C..,pascal,看个人的喜好,一般情况下,选择C语言来学习。2、如果是有过语言的学习,我看应该一个星期差不多,因为语言的理念互通...

  • 回答 7

    添加语句 INSERT插入语句:INSERT INTO 表名 VALUES (‘xx’,‘xx’)不指定插入的列INSERT INTO table_name VALUES (值1, 值2,…)指定插入的列INSERT INTO table_name (列1, 列2,…) VALUES (值1, 值2,…)查询插入语句: INSERT INTO 插入表 SELECT * FROM 查...

  • 回答 5

    看你什么岗位吧。如果是后端,只会CRUD。应该是可以找到实习的,不过公司应该不会太好。如果是数据库开发岗位,那这应该是不会找到的。

  • 回答 7

    查找数据列 SELECT column1, column2, … FROM table_name; SELECT column_name(s) FROM table_name 

没有解决我的问题,去提问