开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

你真的知道Spring注解驱动的前世今生吗?这篇文章让你豁然

发表于 2021-11-26

「这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战」

图怪兽_40b0b39fdbb3d29e40b4b52282058329_23747

本篇文章,从Spring1.x到Spring 5.x的迭代中,站在现在的角度去思考Spring注解驱动的发展过程,这将有助于我们更好的理解Spring中的注解设计。

Spring Framework 1.x

在SpringFramework1.x时代,其中在1.2.0是这个时代的分水岭,当时Java5刚刚发布,业界正兴起了使用Annotation的技术风,Spring Framework自然也提供了支持,比如当时已经支持了@Transactional等注解,但是这个时候,XML配置方式还是唯一选择。

  • 在xml中添加Bean的声明
1
xml复制代码<bean name="testService" class="com.gupaoedu.controller.TestService"/>
  • 测试
1
2
3
4
5
6
7
java复制代码public class XmlMain {
public static void main(String[] args) {
ApplicationContext context=new FileSystemXmlApplicationContext("classpath:applicationContext.xml");
TestService testService=(TestService)context.getBean("testService");
System.out.println(testService);
}
}

Spring Framework 2.x

Spring Framework2.x时代,2.0版本在Annotation中添加了@Required、@Repository以及AOP相关的@Aspect等注解,同时也提升了XML配置能力,也就是可扩展的XML,比如Dubbo这样的开源框架就是基于Spring XML的扩展来完美的集成Spring,从而降低了Dubbo使用的门槛。

在2.x时代,2.5版本也是这个时代的分水岭, 它引入了一些很核心的Annotation

  • Autowired 依赖注入
  • @Qualifier 依赖查找
  • @Component、@Service 组件声明
  • @Controller、@RequestMappring等spring mvc的注解

尽管Spring 2.x时代提供了不少的注解,但是仍然没有脱离XML配置驱动,比如context:annotation-config context:componet-scan , 前者的职责是注册Annotation处理器,后者是负责扫描classpath下指定包路径下被Spring模式注解标注的类,将他们注册成为Spring Bean

  • 在applicationContext.xml中定义context:componet-scan
1
xml复制代码<context:component-scan base-package="com.gupaoedu.controller"/>
  • 添加注解声明
1
2
3
java复制代码@Service
public class TestService {
}
  • 测试类
1
2
3
4
5
6
7
java复制代码public class XmlMain {
public static void main(String[] args) {
ApplicationContext context=new FileSystemXmlApplicationContext("classpath:applicationContext.xml");
TestService testService=(TestService)context.getBean("testService");
System.out.println(testService);
}
}

Spring Framework 3.x

Spring Framework3.0是一个里程碑式的时代,他的功能特性开始出现了非常大的扩展,比如全面拥抱Java5、以及Spring Annotation。更重要的是,它提供了配置类注解@Configuration, 他出现的首要任务就是取代XML配置方式,不过比较遗憾的是,Spring Framework3.0还没有引入替换XML元素context:componet-scan的注解,而是选择了一个过渡方式@ImportResource。

@ImportResource允许导入遗留的XML配置文件,比如

1
2
3
4
5
java复制代码@ImportResource("classpath:/META-INF/spring/other.xml")
@Configuration
public class SpringConfiguration{

}

并且在Spring Frameworkd提供了AnnotationConfigApplicationContext注册,用来注册@Configuration Class,通过解析Configuration类来进行装配。

在3.1版本中,引入了@ComponentScan,替换了XML元素Context:component-scan , 这个注解虽然是一个小的升级,但是对于spring 来说在注解驱动领域却是一个很大的进步,至此也体现了Spring 的无配置化支持。

Configuration配置演示

  • Configuration这个注解大家应该有用过,它是JavaConfig形式的基于Spring IOC容器的配置类使用的一种注解。因为SpringBoot本质上就是一个spring应用,所以通过这个注解来加载IOC容器的配置是很正常的。所以在启动类里面标注了@Configuration,意味着它其实也是一个IoC容器的配置类。

举个非常简单的例子

  • 测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码ConfigurationDemo
@Configuration
public class ConfigurationDemo {
@Bean
public DemoClass demoClass(){
return new DemoClass();
}
}
DemoClass
public class DemoClass {

public void say(){
System.out.println("say: Hello Mic");
}
}
ConfigurationMain
public class ConfigurationMain {

public static void main(String[] args) {
ApplicationContext applicationContext=
new AnnotationConfigApplicationContext
(ConfigurationDemo.class);
DemoClass demoClass=applicationContext.getBean(DemoClass.class);
demoClass.say();
}
}

Component-scan

ComponentScan这个注解是大家接触得最多的了,相当于xml配置文件中的context:component-scan。 它的主要作用就是扫描指定路径下的标识了需要装配的类,自动装配到spring的Ioc容器中。

标识需要装配的类的形式主要是:@Component、@Repository、@Service、@Controller这类的注解标识的类。

  • 在spring-mvc这个工程中,创建一个单独的包路径,并创建一个OtherServcie。
1
2
3
java复制代码@Service
public class OtherService {
}
  • 在Controller中,注入OtherService的实例,这个时候访问这个接口,会报错,提示没有otherService这个实例。
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@RestController
public class HelloController {

@Autowired
OtherService otherService;

@GetMapping("/hello")
public String hello(){
System.out.println(otherService);
return "Hello Gupaoedu";
}
}
  • 添加conpoment-scan注解,再次访问,错误解决。
1
java复制代码@ComponentScan("com.gupaoedu")

ComponentScan默认会扫描当前package下的的所有加了相关注解标识的类到IoC容器中;

Import注解

import注解是什么意思呢? 联想到xml形式下有一个<import resource/> 形式的注解,就明白它的作用了。import就是把多个分来的容器配置合并在一个配置中。在JavaConfig中所表达的意义是一样的。

  • 创建一个包,并在里面添加一个单独的configuration
1
2
3
4
5
6
7
8
9
10
java复制代码public class DefaultBean {
}
@Configuration
public class SpringConfig {

@Bean
public DefaultBean defaultBean(){
return new DefaultBean();
}
}
  • 此时运行测试方法,
1
2
3
4
5
6
7
8
9
10
java复制代码public class MainDemo {

public static void main(String[] args) {
ApplicationContext ac=new AnnotationConfigApplicationContext(SpringConfig.class);
String[] defNames=ac.getBeanDefinitionNames();
for(String name:defNames){
System.out.println(name);
}
}
}
  • 在另外一个包路径下在创建一个配置类。此时再次运行前面的测试方法,打印OtherBean实例时,这个时候会报错,提示没有该实例
1
2
3
4
5
6
7
8
9
10
java复制代码public class OtherBean {
}
@Configuration
public class OtherConfig {

@Bean
public OtherBean otherBean(){
return new OtherBean();
}
}
  • 修改springConfig,把另外一个配置导入过来
1
2
3
4
5
6
7
8
9
java复制代码@Import(OtherConfig.class)
@Configuration
public class SpringConfig {

@Bean
public DefaultBean defaultBean(){
return new DefaultBean();
}
}
  • 再次运行测试方法,即可看到对象实例的输出。

至此,我们已经了解了Spring Framework在注解驱动时代,完全替代XML的解决方案。至此,Spring团队就此止步了吗?你们太单纯了。虽然无配置化能够减少配置的维护带来的困扰,但是,还是会存在很对第三方组建的基础配置声明。同样很繁琐,所以Spring 退出了@Enable模块驱动。这个特性的作用是把相同职责的功能组件以模块化的方式来装配,更进一步简化了Spring Bean的配置。

Enable模块驱动

我们通过spring提供的定时任务机制来实现一个定时任务的功能,分别拿演示在使用Enable注解和没使用Enable的区别。让大家感受一些Enable注解的作用。

使用EnableScheduing之前

  • 在applicationContext.xml中添加定时调度的配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:task="http://www.springframework.org/schema/task"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task-3.2.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

<context:component-scan base-package="com.gupaoedu.controller"/>
<!--AnnotationDrivenBeanDefinitionParser-->
<task:annotation-driven scheduler="scheduler"/> <!-- 定时器开关-->
<task:scheduler id="scheduler" pool-size="5"/>
</beans>
  • 编写任务处理类
1
2
3
4
5
6
7
8
java复制代码@Service
public class TaskService {

@Scheduled(fixedRate = 5000) //通过@Scheduled声明该方法是计划任务,使用fixedRate属性每隔固定时间执行
public void reportCurrentTime(){
System.out.println("每隔5秒执行一次 "+new Date());
}
}
  • 编写测试类
1
2
3
4
5
6
7
java复制代码public class TestTask {

public static void main(String[] args) {
ApplicationContext applicationContext=new FileSystemXmlApplicationContext("classpath:applicationContext.xml");

}
}

使用EnableScheding之后

  • 创建一个配置类
1
2
3
4
5
java复制代码@Configuration
@ComponentScan("com.gupaoedu.controller")
@EnableScheduling
public class SpringConfig {
}
  • 创建一个service
1
2
3
4
5
6
7
java复制代码@Service
public class TaskService {
@Scheduled(fixedRate = 5000) //通过@Scheduled声明该方法是计划任务,使用fixedRate属性每隔固定时间执行
public void reportCurrentTime(){
System.out.println("每隔5秒执行一次 "+new Date());
}
}
  • 创建一个main方法
1
2
3
4
5
java复制代码public class TaskMain {
public static void main(String[] args) {
ApplicationContext context=new AnnotationConfigApplicationContext(SpringConfig.class);
}
}
  • 启动服务即可实现定时调度的功能。

思考使用Enable省略了哪个步骤呢?

首先我们看没使用Enable的代码,它里面会有一个

1
xml复制代码<task:annotation-driven scheduler="scheduler"/>

这个scheduler是一个注解驱动,会被AnnotationDrivenBeanDefinitionParser 这个解析器进行解析。

在parse方法中,会有如下代码的定义

1
2
java复制代码 builder = BeanDefinitionBuilder.genericBeanDefinition("org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor");
builder.getRawBeanDefinition().setSource(source);

这个类是用来解析@Scheduled注解的。

ok,我们再看一下EnableScheduling注解,我们可以看到,它会自动注册一个ScheduledAnnotationBeanPostProcessor的bean。所以,通过这个例子,就是想表达Enable注解的作用,它可以帮我们省略一些第三方模块的bean的声明的配置。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class SchedulingConfiguration {
public SchedulingConfiguration() {
}

@Bean(
name = {"org.springframework.context.annotation.internalScheduledAnnotationProcessor"}
)
@Role(2)
public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
return new ScheduledAnnotationBeanPostProcessor();
}
}

Spring Framework 4.x

Spring 4.x版本,是注解的完善时代,它主要是提升条件装配能力,引入了@Conditional注解,通过自定义Condition实现配合,弥补了之前版本条件化配置的短板。

简单来说,Conditional提供了一个Bean的装载条件判断,也就是说如果这个条件不满足,那么通过@Bean声明的对象,不会被自动装载进来,具体是怎么用的呢?,先来简单带大家了解一下它的基本使用。

Conditional的概述

@Conditional是一个注解,我们观察一下这个注解的声明, 它可以接收一个Condition的数组。

1
2
3
4
5
6
java复制代码@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
Class<? extends Condition>[] value();
}
1
2
3
4
java复制代码@FunctionalInterface
public interface Condition {
boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}

这个Condition是一个函数式接口,提供了一个matchers的方法,简单来说,它就是提供了一个匹配的判断规则,返回true表示可以注入bean,返回false表示不能注入。

Conditional的实战

  • 自定义个一个Condition,逻辑比较简单,如果当前操作系统是Windows,则返回true,否则返回false
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class GpCondition implements Condition{
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata
annotatedTypeMetadata) {
//此处进行条件判断,如果返回 true,表示需要加载该配置类或者 Bean
//否则,表示不加载
String os=conditionContext.getEnvironment().getProperty("os.name");
if(os.contains("Windows")){
return true;
}
return false;
}
}
  • 创建一个配置类,装载一个 BeanClass
1
2
3
4
5
6
7
8
java复制代码@Configuration
public class ConditionConfig {
@Bean
@Conditional(GpCondition.class)
public BeanClass beanClass(){
return new BeanClass();
}
}
  • 在 BeanClass 的 bean 声明方法中增加@Conditional(GpCondition.class),其中具体的条件是我们自定义的 GpCondition 类。上述代码所表达的意思是,如果 GpCondition 类中的 matchs 返回 true,则将 BeanClass 装载到 Spring IoC 容器中
  • 运行测试方法
1
2
3
4
5
6
7
8
java复制代码public class ConditionMain {
public static void main(String[] args) {
AnnotationConfigApplicationContext context=new
AnnotationConfigApplicationContext(ConditionConfig.class);
BeanClass beanClass=context.getBean(BeanClass.class);
System.out.println(beanClass);
}
}

总结

经过对Spring注解驱动的整体分析,不难发现,我们如今之所以能够非常方便的基于注解来完成Spring中大量的功能,得益于Spring团队不断解决用户痛点而做的各种努力。
而Spring Boot的自动装配机制,也是在Spring 注解驱动的基础上演化而来,在后续的内容中,我会专门分析Spring Boot的自动装配机制。

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Mic带你学架构!
如果本篇文章对您有帮助,还请帮忙点个关注和赞,您的坚持是我不断创作的动力。欢迎关注同名微信公众号获取更多技术干货!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Java】动态绑定机制 Java 动态绑定机制

发表于 2021-11-26
  • 这是我参与11月更文挑战的第21天,活动详情查看:2021最后一次更文挑战

Java 动态绑定机制

  • Java 重要机制:动态绑定机制
  • Java 动态绑定机制:
  1. 当调用对象方法的时候,该方法会和该对象的内存地址/运行类型绑定
  2. 当调用对象属性时,没有动态绑定机制,哪里声明,哪里使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
java复制代码package com.xdr630.dynamic_;

public class DynamicBinding {
public static void main(String[] args) {
// a 的编译类型是 A ,运行类型是 B
//属性看编译类型是哪个,方法看运行类型是哪个。
A a = new B();
System.out.println(a.sum()); //40
System.out.println(a.sum1()); //30
}
}

class A { //父类
public int i = 10;

public int sum(){
return getI() + 10;
}

public int sum1(){
return i + 10;
}

public int getI(){
return i;
}

}

class B extends A {

public int i = 20;

public int sum() {
return i + 20;
}

public int getI(){
return i;
}

public int sum1(){
return i + 10;
}

}

在这里插入图片描述

  • 演示结论1
  • 把上面子类中的 sum() 方法注释掉,main 方法中的 a.sum() 输出什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码public class DynamicBinding {
public static void main(String[] args) {
// a 的编译类型是 A ,运行类型是 B
A a = new B();
System.out.println(a.sum()); //30
System.out.println(a.sum1());
}
}

class A { //父类
public int i = 10;
//动态绑定机制:
public int sum(){
return getI() + 10;// 20 + 10
}

public int sum1(){
return i + 10;
}

public int getI(){//父类 getI()
return i;
}

}

class B extends A { //子类

public int i = 20;

public int getI(){// 子类 getI()
return i;
}

public int sum1(){
return i + 10;
}

}
  • 代码分析:
  1. 这个时候 a 的运行类型是 B,a.sum() 就会先找子类中的 sum() 方法,而此时子类中的 sum() 方法被注销了,所以只能去父类找 sum() 方法,sum 方法中再去调 getI() 方法,而此时父类和子类都有 getI() 方法。
  2. 因为a.sum() 方法的运行类型B,根据动态绑定机制,所以找到子类中的 getI() 方法,而在 getI() 中的return i 这个 i 是属性,没有绑定机制,在子类中声明的,直接返回 20 ,然后回到父类中的 sum() 方法 ,20 + 10,所以最后 a.sum 的输出值为 30

在这里插入图片描述

  • 演示结论2
  • 把上面子类中的 sum1() 方法注释掉,main 方法中的 a.sum1() 输出什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码public class DynamicBinding {
public static void main(String[] args) {
// a 的编译类型是 A ,运行类型是 B
A a = new B();
System.out.println(a.sum());//30
System.out.println(a.sum1());//20
}
}

class A { //父类
public int i = 10;
//动态绑定机制:
public int sum(){
return getI() + 10;//20 + 10
}

public int sum1(){
return i + 10;
}

public int getI(){//父类 getI()
return i;
}

}

class B extends A { //子类

public int i = 20;

public int getI(){// 子类 getI()
return i;
}

}
  • 代码分析:
  1. a 的运行类型是 B,所以从子类B开始找,而此时 sum1 方法被注释了,只能去找父类A中的 sum1 方法,在 sum1 方法中 return i + 10,而 i 是属性,没有动态绑定机制,哪里声明,哪里使用,此时 i 属性在父类A中声明,所以 i 为10,所以 a.sum1 的输出值为 20
    在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MySQL 中 datetime 和 timestamp 类

发表于 2021-11-26

  DATETIME、TIMESTAMP 是 MySQL 中常用的日期/时间数据类型。其中 DATETIME 和 TIMESTAMP 在表象上有相似的地方,但在本质上却有许多不同的地方,如果再涉及到时区(timezone),则又有一些不同的地方。

⒈ 格式

  MySQL 中的日期/时间的格式可以是字符串类型或数值类型,具体取决于上下文。如果上下文中期望的输入是一个日期类型,那么 '2015-07-21' 、'20150721' 、20150721 都会被解释为 date 类型。

  在标准 SQL 中,日期/时间类型必须由一个类型关键字和一个字符串指定。

1
2
3
sql复制代码DATE 'str' 
TIME 'str'
TIMESTAMP 'str'

  MySQL 继承了标准 SQL 的规范,同时也做了一些扩展。首先,在 MySQL 中不需要指定类型关键字;另外,MySQL 还可以识别对应于标准 SQL 规范的 ODBC 规范。

1
2
3
sql复制代码{ d 'str' }
{ t 'str' }
{ ts 'str' }

在 MySQL 中,TIMESTAMP 语法最终生成的是一个 DATETIME 类型的值,因为 MySQL 中的 DATETIME 的范围更接近标准 SQL 中的 TIMESTAMP 类型(MySQL 中 TIMESTAMP 类型从 1970 年到 2038 年,标准 SQL 中 TIMESTAMP 类型从 0001 年到 9999 年)

⓵ MySQL 支持的 DATETIME 和 TIMESTAMP 的格式:

  • 字符串格式 ‘YYYY-MM-DD hh:mm:ss' 或 'YY-MM-DD hh:mm:ss' 。
    其中,任何标点符号都可以作为日期或时间的分隔符,例如 '2012-12-31 11:30:45', '2012^12^31 11+30+45', '2012/12/31 11*30*45' 和'2012@12@31 11^30^45' 的结果都是等价的。
    另外,日期和时间中间的分隔符可以是空格,也可以是 T ,如 '2012-12-31 11:30:45' 和 '2012-12-31T11:30:45' 的结果也是等价的。
  • 没有分隔符的字符串格式 'YYYYMMDDhhmmss' 或 'YYMMDDhhmmss'。
    例如 '20070523091528' 和 '070523091528' 都会被解释为 '2007-05-23 09:15:28' ,但 '071122129015' 会被解释为 '0000-00-00 00:00:00' ,因为 '90' 不是一个有效的小时数。
  • 数值类型的 YYYYMMDDhhmmss 或 YYMMDDhhmmss。

如果格式中年为两位数,则 MySQL 的解析规则如下:

  1. 70-99 之间的年会被解释为 1970-1999
  2. 00-69 之间的年会被解释为 2000-2069
    对于字符串格式,如果月份、日期、小时、分钟、秒的值小于 10,则可以不加前导 0,如 '2015-6-9 1:2:3' 会被解释为 2015-06-09 01:02:03 。

对于数值格式,长度应该为 6、8、12 或 14 位。如果长度为 8 或 14,则年的长度为 4 位,否则年的长度为 2 位。

⓶ 小数秒

  MySQL 中 DATETIME 和 TIMESTAMP 类型支持小数秒,精度最长为 6 位(微秒)。定义小数秒的语法为 type_name(fsp) 。

1
2
3
4
sql复制代码CREATE TABLE t1 (
t TIMESTAMP(3),
dt DATETIME(6)
);

  当向数据表中写入带有小数秒的记录时,如果字段定义的精度小于写入的值的精度,则默认会将小数秒四舍五入到字段定义的精度。如果开启了 TIME_TRUNCATE_FRACTIONAL 模式,则将小数秒截取到字段定义的精度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
sql复制代码CREATE TABLE fractest( 
c1 TIME(2),
c2 DATETIME(2),
c3 TIMESTAMP(2)
);
// 四舍五入
INSERT INTO fractest VALUES('17:51:04.777', '2018-09-08 17:51:04.777', '2018-09-08 17:51:04.777');

mysql> SELECT * FROM fractest;
+-------------+------------------------+------------------------+
| c1 | c2 | c3 |
+-------------+------------------------+------------------------+
| 17:51:04.78 | 2018-09-08 17:51:04.78 | 2018-09-08 17:51:04.78 |
+-------------+------------------------+------------------------+
1 row in set (0.00 sec)

// 截取
SET @@sql_mode = sys.list_add(@@sql_mode, 'TIME_TRUNCATE_FRACTIONAL');
INSERT INTO fractest VALUES('17:51:04.777', '2018-09-08 17:51:04.777', '2018-09-08 17:51:04.777');

mysql> SELECT * FROM fractest;
+-------------+------------------------+------------------------+
| c1 | c2 | c3 |
+-------------+------------------------+------------------------+
| 17:51:04.78 | 2018-09-08 17:51:04.78 | 2018-09-08 17:51:04.78 |
| 17:51:04.77 | 2018-09-08 17:51:04.77 | 2018-09-08 17:51:04.77 |
+-------------+------------------------+------------------------+
2 rows in set (0.00 sec)

小数秒和 DATETIME 或 TIMESTAMP 之间的分隔符只能是 .

⒉ 范围

  在 MySQL 中,无论 DATETIME 类型的数据以哪种格式写入,最终 MySQL 解析和展示都是采用 YYYY-MM-DD hh:mm:ss 的格式,其支持的范围为 1000-01-01 00:00:00 到 9999-12-31 23:59:59 。

对于 DATETIME 类型,虽然支持的范围从 1000-01-01 00:00:00 开始,但早于这个时间的日期时间也可以写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
> sql复制代码CREATE TABLE `time_format_test` (
> `id` int unsigned NOT NULL AUTO_INCREMENT,
> `ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
> `dt` datetime NOT NULL,
> PRIMARY KEY (`id`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='时间日期格式测试'
>
> mysql> insert into time_format_test (ts, dt) values (now(), '0800-01-01 00:00:00');
> Query OK, 1 row affected (0.00 sec)
>
> mysql> select * from time_format_test;
> +----+---------------------+---------------------+
> | id | ts | dt |
> +----+---------------------+---------------------+
> | 1 | 2021-11-25 08:53:29 | 0800-01-01 00:00:00 |
> +----+---------------------+---------------------+
> 1 row in set (0.00 sec)
>
>

  MySQL 中 TIMESTAMP 所支持的范围要远小于 DATETIME ,其范围只有 UTC 1970-01-01 00:00:01 到 UTC 2038-01-19 03:14:07 。

DATETIME 和 TIMESTAMP 类型都可以包含小数秒,在往这两种数据类型的列中写入包含小数秒的值时,这些小数秒也会被存储。
包含小数秒之后,DATETIME 的范围变为 1000-01-01 00:00:00.000000 到 9999-12-31 23:59:59.999999 。相应的,TIMESTAMP 的范围变为 UTC 1970-01-01 00:00:01.000000 到 UTC 2038-01-19 03:14:07.999999 。

⒊ 时区

  MySQL 中,TIMESTAMP 类型的值在保存时会由当前时区转换成 UTC ,在读取时则会从 UTC 转换成指定的时区。而 DATETIME 类型的值在保存和读取时与时区无关,在底层会以 bigint 类型的值存储。默认情况下,MySQL 连接的时区采用的是 MySQL 服务的时区,而 MySQL 服务的时区默认情况下与所在服务器的时区保持一致。如果 MySQL 连接中指定了时区,则当前时区为 MySQL 连接中指定的时区.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
sql复制代码CREATE TABLE `time_format_test` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`dt` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='时间日期格式测试'
// 时区默认与服务器时区一致
mysql> select @@time_zone;
+-------------+
| @@time_zone |
+-------------+
| SYSTEM |
+-------------+
1 row in set (0.00 sec)

mysql> insert into time_format_test (ts, dt) values (now(), now());
Query OK, 1 row affected (0.01 sec)

mysql> select * from time_format_test;
+----+---------------------+---------------------+
| id | ts | dt |
+----+---------------------+---------------------+
| 1 | 2021-11-25 10:05:46 | 2021-11-25 10:05:46 |
+----+---------------------+---------------------+
1 row in set (0.00 sec)
// 修改时区设置,TIMESTAMP 类型的值会随时区变化,DATETIME 类型的值不变
mysql> set time_zone = '+0:00';
Query OK, 0 rows affected (0.00 sec)

mysql> select @@time_zone;
+-------------+
| @@time_zone |
+-------------+
| +00:00 |
+-------------+
1 row in set (0.00 sec)

mysql> select * from time_format_test;
+----+---------------------+---------------------+
| id | ts | dt |
+----+---------------------+---------------------+
| 1 | 2021-11-25 02:05:46 | 2021-11-25 10:05:46 |
+----+---------------------+---------------------+
1 row in set (0.00 sec)

mysql> set time_zone = '+8:00';
Query OK, 0 rows affected (0.00 sec)

mysql> select @@time_zone;
+-------------+
| @@time_zone |
+-------------+
| +08:00 |
+-------------+
1 row in set (0.00 sec)

mysql> select * from time_format_test;
+----+---------------------+---------------------+
| id | ts | dt |
+----+---------------------+---------------------+
| 1 | 2021-11-25 10:05:46 | 2021-11-25 10:05:46 |
+----+---------------------+---------------------+
1 row in set (0.00 sec)

  TIMESTAMP 类型的值会随着时区变化,与之相关的函数 FROM_UNIXTIME() 的值也会随着时区变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
sql复制代码mysql> select unix_timestamp('2021-11-25 10:05:46');
+---------------------------------------+
| unix_timestamp('2021-11-25 10:05:46') |
+---------------------------------------+
| 1637805946 |
+---------------------------------------+
1 row in set (0.00 sec)

mysql> set time_zone = '+0:00';
Query OK, 0 rows affected (0.00 sec)

mysql> select from_unixtime(1637805946);
+---------------------------+
| from_unixtime(1637805946) |
+---------------------------+
| 2021-11-25 02:05:46 |
+---------------------------+
1 row in set (0.01 sec)

mysql> set time_zone = '+8:00';
Query OK, 0 rows affected (0.00 sec)

mysql> select from_unixtime(1637805946);
+---------------------------+
| from_unixtime(1637805946) |
+---------------------------+
| 2021-11-25 10:05:46 |
+---------------------------+
1 row in set (0.00 sec)

  从 MySQL 8.0.19 开始,在写入 DATETIME 或 TIMESTAMP 类型的值时,可以指定时区。指定了时区的日期/时间在写入后会转换成数据库当前设置的时区。此后如果再更改时区设置,TIMESTAMP 类型会随时区变化,但 DATETIME 类型不会随时区变化。

在指定时区时,如果时区小于 10 ,则一定要加前导 0,否则会导致写入的值无效而变为 0 值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sql复制代码// 设置时区时,小于 10 的时区一定要加前导 0
mysql> insert into time_format_test (ts, dt) values ('2021-11-25 10:35:18+09:00', '2021-11-25 10:35:18+01:00');
Query OK, 1 row affected (0.00 sec)
// 设置了时区的值会转换成当前时区的值
mysql> select * from time_format_test;
+----+---------------------+---------------------+
| id | ts | dt |
+----+---------------------+---------------------+
| 1 | 2021-11-25 09:35:18 | 2021-11-25 17:35:18 |
+----+---------------------+---------------------+
1 row in set (0.00 sec)

mysql> set time_zone = '+0:00';
Query OK, 0 rows affected (0.00 sec)

mysql> select * from time_format_test;
+----+---------------------+---------------------+
| id | ts | dt |
+----+---------------------+---------------------+
| 1 | 2021-11-25 01:35:18 | 2021-11-25 17:35:18 |
+----+---------------------+---------------------+
1 row in set (0.00 sec)

⒋ 自动初始化以及自动更新

  对于 DATETIME 和 TIMESTAMP 类型的列,可以通过设置 DEFAULT CURRENT_TIMESTAMP 和 ON UPDATE CURRENT_TIMESTAMP 来对列值进行自动初始化以及自动更新。

  对于设置了自动更新的列,如果对同一行中其他的列进行了更新操作,但值没有发生变化,那么自动更细此时并不会起作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sql复制代码mysql> update time_format_test set dt = '2021-11-25 17:35:18' where id = 1;
Query OK, 0 rows affected (0.01 sec)
Rows matched: 1 Changed: 0 Warnings: 0

mysql> select * from time_format_test;
+----+---------------------+---------------------+
| id | ts | dt |
+----+---------------------+---------------------+
| 1 | 2021-11-25 09:35:18 | 2021-11-25 17:35:18 |
+----+---------------------+---------------------+
1 row in set (0.00 sec)

mysql> update time_format_test set dt = now() where id = 1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0

mysql> select * from time_format_test;
+----+---------------------+---------------------+
| id | ts | dt |
+----+---------------------+---------------------+
| 1 | 2021-11-25 11:19:40 | 2021-11-25 11:19:40 |
+----+---------------------+---------------------+
1 row in set (0.00 sec)

  对于 TIMESTAMP 类型,如果系统变量 explicit_defaults_for_timestamp 的值为 0,并且列值不允许为 NULL,则在给相应的列赋 NULL 值时,列值会自动变成当前的日期时间值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
sql复制代码CREATE TABLE `time_format_test` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`ts` timestamp,
`dt` datetime,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='时间日期格式测试';
Query OK, 0 rows affected (0.05 sec)

mysql> select @@explicit_defaults_for_timestamp;
+-----------------------------------+
| @@explicit_defaults_for_timestamp |
+-----------------------------------+
| 0 |
+-----------------------------------+
1 row in set (0.00 sec)

mysql> insert into time_format_test (ts, dt) values (null, null);
Query OK, 1 row affected (0.00 sec)

mysql> select * from time_format_test;
+----+---------------------+------+
| id | ts | dt |
+----+---------------------+------+
| 1 | 2021-11-25 11:27:58 | NULL |
+----+---------------------+------+
1 row in set (0.00 sec)

  对于 TIMESTAMP 类型,如果系统变量 explicit_defaults_for_timestamp 的值为 0,并且列值不允许为 NULL ,那么第一个类型为 TIMESTAMP 的列会自动添加 DEFAULT CURRENT_TIMESTAMP 和 ON UPDATE CURRENT_TIMESTAMP 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sql复制代码CREATE TABLE `time_format_test` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`ts1` timestamp,
`ts2` timestamp,
`dt` datetime,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='时间日期格式测试';
Query OK, 0 rows affected (0.04 sec)

mysql> show create table time_format_test;
+------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| time_format_test | CREATE TABLE `time_format_test` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`ts1` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`ts2` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
`dt` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='时间日期格式测试' |
+------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

  如果要避免上述这种情况,可以在定义数据表时为 TIMESTAMP 类型的列手动设置默认值或允许其值为 NULL 。除此之外,还可以将系统变量 explicit_defaults_for_timestamp 的值设为 1,这意味着 TIMESTAMP 类型的列的默认值以及自动更新需要明确指定。

  如果 TIMESTAMP 或 DATETIME 类型的列在定义时指定了小数秒的精度,那么自始至终,同一个列的小数秒的精度应该相同。

1
2
3
4
5
6
7
sql复制代码CREATE TABLE t1 (
ts TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6)
);
// 以下定义不允许出现
CREATE TABLE t1 (
ts TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(3)
);

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

我的docker随笔35:jenkins服务部署

发表于 2021-11-26

这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战

本文涉及一种在容器中部署 jenkins 服务的方法,后续将其与 gitlab 配合以实现 CICD 功能。其目的是为了在实际工作中使用代码托管及自动化操作。

一、引言

因工作需要,需部署 jenkins 服务器进行测试。与网上几分钟即可部署的文章相比,本文更注重个人的实践记录。本文与前面的 gitlab 有一定的关联。

二、技术小结

  • gitlab 有官方的 Docker 镜像。本文直接使用之。
  • 使用 docker-compose 部署,指定端口。注意挂载 docker.socket文件。
  • 首次登录会提示输入随机文件,一旦输入并设置成功后,不再提示。
  • 各种配置慢慢补充。

三、部署gitlab服务

3.1 部署脚本

本文使用 docker-compose 部署,在测试阶段,使用了多个版本。此处是比较简单的版本,用于初次使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
yaml复制代码version: '2'
services:
  jenkins:
      image: jenkins/jenkins:centos7
      container_name: lljenkins
      restart: always
      volumes:
           # 使用主机的docker服务(因为要在jenkins容器中运行docker)
          - /var/run/docker.sock:/var/run/docker.sock
           # 使用数据盘的目录作为jenkins的工作目录
          - $PWD/jenkins_home:/var/jenkins_home
      ports:
           # 端口映射
          - "9999:8080"
          - "50000:50000"
      networks:
          - llgitlab-net
​
networks:
  llgitlab-net:
      driver: bridge

注意几点:

  • 脚本中指定了docker.sock的路径,后续可以在jenkins用docker构建。
  • jenkins 数据目录在本目录下的jenkins_home目录,其权限修改为1000,命令为sudo chown -R 1000 jenkins_home。
  • 根据实际环境,选择了 centos7 版本的 jenkins。

3.2 运行

运行命令如下:

1
复制代码docker-compose up -d

注:jenkins 的启动比 gitlab 快很多。

停止命令如下:

1
复制代码docker-compose down

运行过程中会出现初始密码:

1
2
3
4
5
6
7
8
markdown复制代码Jenkins initial setup is required. An admin user has been created and a password generated.
Please use the following password to proceed to installation:
​
c4365f900e554ff5b8885d8c2975929b
​
This may also be found at: /var/jenkins_home/secrets/initialAdminPassword
​
*************************************************************

四、初次配置

服务启动后,需要进行必要的配置,因此在本文一并给出。

4.1 登录

初次登录时,会提示输入管理员密码,如图1所示:

1.png
在 docker 启动时出现有密码,也可以直接在页面所示文件中找到。

4.2 安装插件:

2.png
选右侧自定义安装,注:默认也勾选了部分插件。

安装过程图示:

3.png

注:笔者在实践中,遇到不可抗拒因素,导致网络不稳定,经常安装不成功,如果尝试多次失败,可以跳过不安装。

五、一些配置

5.1 git 仓库地址问题

配置了项目,构建,提示:

1
css复制代码stderr: error: RPC failed; result=22, HTTP code = 404

原因及解决:

1
go复制代码在设置项目git地址时,必须添加`.git`后缀,否则地址不认。

5.2 Publish over SSH

在插件管理界面中搜索Publish Over SSH,并安装之。

在配置界面中进行配置。如下:

4.png

注:需测试是否能正常连接。

在具体项目构建后操作添加Send build artifacts over SSH。源文件如果带路径的话,默认情况目标目录也带有。比如源文件为taget/a.out,目标目录为/tmp,则目标文件路径为/tmp/target/a.out`。

5.3 邮件通知

本节使用腾讯企业邮件测试(感谢腾讯为个人域名提供免费的邮箱服务)。此为前提。

使用插件Email Extension Plugin,在插件管理页面搜索,确认是否安装:

image-20210916003759787.png

系统管理->系统配置。

设置系统管理员邮件地址:

image-20210916085303466.png

第三方邮件通知插件

image-20210916090427313.png

触发条件:

image-20210916091126192.png

再设置邮件模板。

修改默认标题(Default Subject),示例:

1
2
3
swift复制代码$PROJECT_NAME - Build # $BUILD_NUMBER - $BUILD_STATUS!
$PROJECT_NAME - Build # $BUILD_NUMBER - $BUILD_STATUS:
$PROJECT_NAME 项目第 # $BUILD_NUMBER 次构建 - $BUILD_STATUS

修改默认内容(Default Content),根据网上资料修改的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
xml复制代码$PROJECT_NAME 项目第 # $BUILD_NUMBER 次构建 - $BUILD_STATUS
​
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>PROJECT_NAME 第 # $BUILD_NUMBER 次构建 - $BUILD_STATUS</title>
</head>
​
<body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4"
  offset="0">
  <table width="95%" cellpadding="0" cellspacing="0" style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">
      <tr>
          本邮件由系统自动发出,无需回复!<br/>
          ${PROJECT_NAME } 项目构建信息如下</br>
          <td><font color="#CC0000">构建结果 - ${BUILD_STATUS}</font></td>
      </tr>
​
      <tr>
          <td><br />
          <b><font color="#0B610B">构建信息</font></b>
          <hr size="2" width="100%" align="center" /></td>
      </tr>
      <tr>
          <td>
              <ul>
                  <li>项目名称 : ${PROJECT_NAME}</li>
                  <li>构建编号 : 第${BUILD_NUMBER}次构建</li>
                  <li>触发原因: ${CAUSE}</li>
                  <li>构建状态: ${BUILD_STATUS}</li>
                  <li>构建日志: <a href="${BUILD_URL}console">${BUILD_URL}console</a></li>
                  <li>构建 Url : <a href="${BUILD_URL}">${BUILD_URL}</a></li>
                  <li>工作目录 : <a href="${PROJECT_URL}ws">${PROJECT_URL}ws</a></li>
                  <li>项目 Url : <a href="${PROJECT_URL}">${PROJECT_URL}</a></li>
              </ul>
​
              <font color="#0B610B">最近提交(#$SVN_REVISION)</font>
              <hr size="2" width="100%" />
              <ul>
              ${CHANGES_SINCE_LAST_SUCCESS, reverse=true, format="%c", changesFormat="<li>%d [%a] %m</li>"}
              </ul>
              详细提交: <a href="${PROJECT_URL}changes">${PROJECT_URL}changes</a><br/>
​
          </td>
      </tr>
  </table>
</body>
</html>

构建日志:${BUILD_LOG, maxLines=100}

自带邮件通知插件

在“邮件通知”处添加 SMTP 服务器,此处为smtp.exmail.qq.com。点击高级,设置邮件账号和密码,勾选”SSL协议“、”使用 TLS“,端口设置为465。

image-20210916085725950.png

最后进行邮件发送的测试,输入接收者邮箱,点击测试即可。提示Email was successfully sent表示发送成功。

image-20210916085826846.png

邮件接收:

image-20210916090144129.png

注:#10表示是第10次发送,表示点击测试的次数——包括失败的。

发送测试邮件出错及解决方法:

1
2
3
4
5
6
7
8
javascript复制代码提示:
com.sun.mail.smtp.SMTPSenderFailedException: 501 mail from address must be same as authorization user
解决:
Jenkins管理员邮箱与此处设置的发送者邮箱保持一致。
​
提示:
javax.mail.MessagingException: Got bad greeting from SMTP host: smtp.exmail.qq.com, port: 465, response: [EOF]
原因:勾选`使用SSL协议`。

某次发送提示:

1
2
3
vbnet复制代码Sending email for trigger: Always
Not sending mail to unregistered user li@latelee.org because your SCM claimed this was associated with a user ID ?li' which your security realm does not recognize; you may need changes in your SCM plugin
Sending email to: li@latelee.org

结论:看信息是发送不成功,但能接收邮件,暂时不管。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

深入浅出Redis持久化 前言 RDB AOF 混合持久化

发表于 2021-11-26

前言

redis作为内存数据库,数据都在内存里,如果突然宕机,则数据都会丢失(这里假设不使用非易失性内存),redis提供了持久化机制来防止这种情况发生

如果将redis仅作为缓存使用,且不需要宕机后快速生成缓存数据,可以不使用持久化机制,还能提升性能

redis提供了以下两种持久化方式:

  • RDB(redis database):定时将某一时刻内存中的数据保存到磁盘上
  • AOF(append only file):通过持续增量记录redis的写命令来持久化数据

下面介绍其原理和实现

RDB

快照原理

redis通过定时将内存中某一时刻的快照持久化到磁盘上,来实现数据保存

redis怎么“捕获”某一时刻的快照?

一个简单的想法是,在持久化的过程中,不处理任何请求,也就是执行save命令

但这样会阻塞线上业务,更好的做法是:边持久化边响应客户请求,也就是bgsave命令

以快照的方式持久化,需要保证获取的是“一致性”的快照

什么意思呢?在持久化的过程中,如果先持久化了内存的前半部分,再持久化后半部分,可能在持久化后半部分时,又有请求同时写了前,后半部分的数据。而此时前半部分已经持久化完毕,不会修改了。这样前后两部分的数据就是“不一致”的

举个例子:
image.png

  1. 内存中一开始A=1,B=2
  2. 将A=1序列化到磁盘
  3. 某请求在内存中将A,b修改为2,此时磁盘上A=1
  4. 继续序列化,将B=2写入磁盘
  5. 出现问题,此时磁盘中是不合法的数据,因为内存中从来没有某一时刻是A=1,B=2,也就是说A的老版本和B的新版本混在一起了

那怎么获取一致性的快照呢?

  • Mysql Innodb的可重复读隔离级别采用MVCC的方式,使得每次的读操作获取的都是一致性的数据,同时不阻塞其他读写请求
  • redis采用多进程写时复制(copy on write)技术来实现

当redis服务端收到bgsave命令时,调用fork函数产生一个子进程,由该子进程创建RDB文件,主进程继续响应客户请求

刚创建子进程时,其和父进程共享数据段,这是操作系统节约内存的机制

只有在某进程需要对数据进行修改时,才会将其复制一份,单独给自己使用,才这个复制出来的页面进行修改。也就是说,子进程在生成快照的过程中需要遍历的页面,是不会被修改的,永远是刚fork出来的样子。

刚fork出来的页面肯定是那一时刻的一个一致性快照,因此将其序列化到磁盘没有任何问题

保存时机

redis在什么时机会触发一次fork子进程生成快照文件的操作呢?

若在配置文件中进行如下save配置:

1
2
3
4
5
yaml复制代码save 900 1

save 300 10

save 60 3600

以上配置表示redis会在以下情况满足时,自动执行bgsave命令:

  • 900秒内至少1个key发送变化(新增,修改,删除)
  • 300秒内至少10个key发送变化
  • 60秒内至少3600个key发送变化

为什么需要配置多个规则?

试想如果只有中间的规则(300秒内至少10个key发送变化)

那如果redis中只有9个key发生变化,则不管经过多久,这9个key都不会被持久化,因为永远不满足300秒内至少10个key发送变化的条件

因此在save配置项中最好有一条 save XXX 1 的配置兜底,确保redis中所有的变动在一定时间内都能被持久化

如果短时间内变化的次数较多,但根据唯一的那条配置,需间隔300秒后才会进行一次持久化。如果宕机,则会丢失从上一次持久化到宕机这段时间内的修改。也就是说最多会丢失300秒的数据

因此在save配置项中最好有一条 save XXX(小于300秒) XXX(大于10个)的配置,这样在短时间内变更较多时,能提早,更频繁地持久化。如果宕机,最多只会丢失更短时间间隔的数据

怎么实现的?其实非常简单

以下用go代码示例,不过没有特殊的语法,不影响理解

redis维护了两个变量:

  • dirty:在上次bgsave后,执行了多少次修改操作
  • lastsave:上次bgsave的时间

以及配置的规则saveConfig:

1
2
3
4
5
6
7
go复制代码type saveConfig struct {

Change int // 多少时间内 save 60 3600 中的 3600

time int // 多少个key发生变化 save 60 3600 中的 60

}

在每次事件循环中尝试每个配置,如果符合某个配置的条件,执行bgsave

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
go复制代码

func serverCron(){

/**

执行其他操作

*/



interval := time.Now().Sub(lastsave)

// 尝试每个配置

for saveConfig := range saveConfigs {

// 如果符合某个配置的条件,执行bgsave

if dirty >= saveConfig.Change && interval >= saveConfig.Time {

bgsave()

break

}

}

}

AOF

与RDB通过快照的方式保存redis中的数据不同,AOF通过保存redis执行的写命令来记录数据库的状态。如果AOF文件记录了有史以来的所有命令,则将这些命令在一个空的redis重播一遍,就可以恢复数据

同步策略

一条写命令从产生到写入AOF文件需经过以下三步:

命令追加,文件写入,文件同步

一条写命令在被执行完毕后,会被追加到内存缓冲区

接下来就到了事件循环的末尾,一次事件循环的伪代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
scss复制代码func eventLoop() {

for {

// 处理文件事件,即客户的请求读写,也就是在这里面完成命令追加

processFileEvents()



// 处理时间事件,例如定期删除过期键

processTimeEvents()



// 根据配置,执行文件写入或文件同步

flushAppendOnlyFile()

}

}

在事件循环的开头,会执行文件事件,即客户的请求读写,也就是在这里面完成命令追加

在一次事件循环结束前,redis会根据配置appendfsync的值来来决定怎么处理之前加入到内存缓冲区的aof命令:

  • always:将aof_buf缓冲区中的内容全部写入并同步aof文件
+ 由于每次事件循环都会同步数据到磁盘,总所周知,磁盘的速度比内存慢很多,因此该配置`效率最低`,但`安全性也最高`,因为若宕机最多只会丢失一个事件循环的数据
  • everysec:将aof_buf缓冲区中的内容全部写入到aof文件,若当前距离上次同步超过1秒,则执行同步
+ 同步的频率从每次时间循环变为每隔一秒,`效率提升不少`,同时若宕机最多丢失1秒的数据
  • no:将aof_buf缓冲区中的内容全部写入到aof文件,但不执行同步,何时同步由操作系统决定
+ 从效率上来说是`最快`的,因此每次都不用等待数据同步到磁盘,但是何时同步数据不可控,有`丢失较长时间范围的数据的风险`

文件写入和同步:
现代操作系统为了提升效率,用户将一些数据写入磁盘时(文件写入),操作系统通常会将数据暂存于内存缓冲区,等数据填满或超过一定时限后再真正刷入磁盘。同时操作系统也提供了文件同步的函数

值得一提的是,当配置为always时,并不是每写一条命令就同步一次磁盘,而是一次事件循环后同步一次。因为一次事件循环中可能会执行多条命令

生产环境中通常将appendfsync配置为everysec,在保存高性能的同时尽量减少数据丢失

AOF重写

随着程序的运行,aof文件会越来越大,若不加以处理,数据库重启或宕机时使用aof文件来还原的耗时就会越来越长,甚至超出磁盘容量限制。因此redis会定时为aof文件瘦身,使其只保存必要的数据

那什么是不必要的数据呢?

举个例子,假设历史上对list执行了以下6条命令

1
2
3
4
5
6
7
8
9
10
11
css复制代码rpush list "A" // ["A"]

rpush list "B" // ["A","B"]

rpush list "C" // ["A","B","C"]

lpop list // ["B","C"]

lpop list // ["C"]

rpush list "D" "E" // ["C","D","E"]

但这6条命令其实可以用1条命令来替代:

1
arduino复制代码rpush list "C" "D" "E"

这样一来,占用空间和恢复时间都答复减少

进行重写有以下两种方式:

  1. 分析现有aof文件中有关于list的内容,进行重写
  2. 读取内存中list的值,用rpush XXX 这一条命令替换掉aof文件中和list有关的命令
1. 就像上面的`rpush list "C" "D" "E"`

很明显,第二种方式实现简单,效率也高,不像第一种方式需要设计复杂的算法来比对,处理aof文件

redis作为单线程应用,如果将aof重写放到主线程中执行,会导致重写期间redis无法处理客户响应

为了避免这种情况,redis将aof文件重写的工作放到子进程中。这么做有以下优点:

  • 主线程能继续对外提供服务,不受aof文件重写的影响
  • 子进程基于fork那一时刻的数据,不受主线程后续操作的影响,这也是fork子进程方式的通用优点

子进程根据内存快照,生成一份新的aof文件

主线程在子进程重写期间,还在源源不断地接收并执行新的写命令,可能在子进程完成重写后,数据库实际的状态,和重写后的aof文件不一致。因此需要将这期间新的写命令追加到重写后的aof文件中,再将重写后的aof文件替换到原来的aof文件,这样aof重写才算完成

为了解决数据不一致问题,redis设置了aof重写缓冲区,在子进程重写期间,新的写命令除了会被写入原aof文件中,还会被写入aof重写缓冲区,这样在子进程重写完毕后,能知道哪些是新命令,将这些新命令追加到重写好的aof文件即可大功告成

image.png

为啥新命令还要写到原aof文件中?保证原来的aof持久化逻辑正常运行,当重写失败时不会对原来产生影响

将新命令追加到重写好的aof文件中不需要在子进程中执行,在主线程中执行即可,原因为

  • 这些新的写命令不会很多,不会对主线程造成太大影响
  • 若再用子进程,还需要考虑怎么合并追加期间的新命令,最终还是需要一个同步操作去合并

混合持久化

单独使用RDB,可能会丢失很多数据,但若单独使用AOF,在恢复数据时相比RDB会慢很多。于是redis 4.0推出了混合持久化,将RDB文件和增量的AOF日志文件放在一起,这里的AOF日志是RDB持久化结束到当前时刻的增量更新日志,通常比较小

image.png
这样的混合持久化方式既有了RDB恢复快的优点,也有AOF不会丢失大量数据的优点

总结

  • RDB通过写时复制技术抓取快照进行持久化,配置保存实际时需考虑到各种情况
  • 根据业务需要配置AOF同步策略,为了避免文件过大,需要进行AOF文件重写
  • 混合持久化方式集成了两者的优点

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java学习16 泛型 泛型中通配符 案例 泛型接口 泛型方

发表于 2021-11-26

「这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战」。

泛型

1.泛型的作用与定义

类型的参数化,就是可以把类型像方法的参数那样传递。

泛型使编译器可以在编译期间对类型进行检查以提高类型安全,减少运行时由于对象类型不匹配引发的异常。

一说到泛型,大伙肯定不会陌生,我们代码里面有很多类似这样的语句:

1
ini复制代码List list=new ArrayList<>();

ArrayList就是个泛型类,我们通过设定不同的类型,可以往集合里面存储不同类型的数据类型(而且只能存储设定的数据类型,这是泛型的优势之一)。“泛型”简单的意思就是泛指的类型(参数化类型)。想象下这样的场景:如果我们现在要写一个容器类(支持数据增删查询的),我们写了支持String类型的,后面还需要写支持Integer类型的。然后呢?Doubel、Float、各种自定义类型?这样重复代码太多了,而且这些容器的算法都是一致的。我们可以通过泛指一种类型T,来代替我们之前需要的所有类型,把我们需要的类型作为参数传递到容器里面,这样我们算法只需要写一套就可以适应所有的类型。最典型的的例子就是ArrayList了,这个集合我们无论传递什么数据类型,它都能很好的工作。

  1. Java泛型介绍

下面我们来介绍Java泛型的相关内容,下面会介绍以下几个方面:

  • Java泛型类
  • Java泛型方法
  • Java泛型接口

Java泛型类

类结构是面向对象中最基本的元素,如果我们的类需要有很好的扩展性,那么我们可以将其设置成泛型的。假设我们需要一个数据的包装类,通过传入不同类型的数据,可以存储相应类型的数据。

Java泛型方法

前面我们介绍的泛型是作用于整个类的,现在我们来介绍泛型方法。泛型方法既可以存在于泛型类中,也可以存在于普通的类中。如果使用泛型方法可以解决问题,那么应该尽量使用泛型方法。下面我们通过例子来看一下泛型方法的使用:

1
2
3
4
5
6
7
8
9
10
11
csharp复制代码class DataHolder<T>{
   T item;    public void setData(T t) {        this.item=t;
  }    public T getData() {        return this.item;
  }    /**
    * 泛型方法
    * @param e
    */
   public void PrinterInfo(E e) {
       System.out.println(e);
  }
}

我们来看运行结果:

1
复制代码1AAAAA8.88

Java泛型接口

Java泛型接口的定义和Java泛型类基本相同,下面是一个例子:

1
2
vbnet复制代码//定义一个泛型接口public interface Generator<T> {    public T next();
}

此处有两点需要注意:

  • 泛型接口未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中。例子如下:
1
2
3
4
5
6
typescript复制代码/* 即:class DataHolder implements Generator{
* 如果不声明泛型,如:class DataHolder implements Generator,编译器会报错:"Unknown class"
*/class FruitGenerator<T> implements Generator<T>{    @Override
   public T next() {        return null;
  }
}
  • 如果泛型接口传入类型参数时,实现该泛型接口的实现类,则所有使用泛型的地方都要替换成传入的实参类型。例子如下:
1
2
3
4
typescript复制代码class DataHolder implements Generator<String>{    @Override
   public String next() {        return null;
  }
}

从这个例子我们看到,实现类里面的所有T的地方都需要实现为String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
typescript复制代码/**Test01案例
* Test02泛型接口
* Test03泛型方法
*Day01通配符
* 泛型类型只允许设置类和接口,而不能使用基本数据类型(不能int要Integer)\
* 通配符?表示能接收一切但不能修改
*/
class Message<T> {//设置泛型上限<T extends Number>
   private T note ;
​
   public T getNote() {
       return note;
  }
​
   public void setNote(T note) {
       this.note = note;
  }
}
public class Day01 {
   public static void main(String[] args) {
           Message<String >msg = new Message<>();
           msg.setNote("99");
           fun(msg);
  }
   public static void fun(Message<? super String>temp){//设置泛型下限
       System.out.println(temp.getNote());
  }
}

泛型中通配符

我们在定义泛型类,泛型方法,泛型接口的时候经常会碰见很多不同的通配符,比如 T,E,K,V 等等,这些通配符又都是什么意思呢?

常用的 T,E,K,V,?

本质上这些个都是通配符,没啥区别,只不过是编码时的一种约定俗成的东西。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个 字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。通常情况下,T,E,K,V,?是这样约定的:

  • ?表示不确定的 java 类型
  • T (type) 表示具体的一个java类型
  • K V (key value) 分别代表java键值中的Key Value
  • E (element) 代表Element

上界通配符 < ? extends E>

上届:用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。

在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:

  • 如果传入的类型不是 E 或者 E 的子类,编译不成功
  • 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用
1
2
3
4
5
6
java复制代码private <K extends A, E extends B> E test(K arg1, E arg2){
   E result = arg2;
   arg2.compareTo(arg1);
   //.....
   return result;
}

下界通配符 < ? super E>

下界: 用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object

在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。

dst 类型 “大于等于” src 的类型,这里的“大于等于”是指 dst 表示的范围比 src 要大,因此装得下 dst 的容器也就能装 src 。

上界通配符主要用于读数据,下界通配符主要用于写数据。

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
typescript复制代码//在Point类定义的时候完全不知道x和y的属性是什么类型,而由使用者来决定
class Point<T>{//T表示参数
private T x;
private T y;
public T getX(){
return x;
}
public void setX(T x){
this.x = x;
}
public T getY(){
return y;
}
public void setY(T y){
this.y = y;
}
}
public class Test01 {
public static void main(String[] args) {
//第一步:设置数据
Point<String> p = new Point<String>();//相当于所有的T都变成String
p.setX("东经10度"); //设置坐标
p.setY("北纬10度");
//第二步:取出数据
String x = p.getX(); //避免了向下转型
String y = p.getY();
System.out.println("x = "+ x +"、y = "+y);
}

}

泛型接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typescript复制代码//泛型接口的两种实现形式
interface IMessage<T>{//在接口上定义了泛型
public void print(T t);
}
class MessageImpl implements IMessage<String>{
public void print(String t){
System.out.println(t);
}
}
public class Test02 {
public static void main(String[] args) {
IMessage<String> msg = new MessageImpl();
msg.print("hello hh");

}
}

泛型方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码//在之前定义的类或接口上发现都可以在里面的方法在泛型类或接口里面,也可以单独定义
//泛型方法(不建议使用)
public class Test03 {
public static void main(String[] args) {
Integer data[] = fun(1,2,3,4);
for (int temp : data) {//迭代和自动拆箱
System.out.println(temp);
}
}
//<T>描述的是泛型标记的声明
public static <T> T[] fun(T ... args){
return args;
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Solo 博客搭建指南

发表于 2021-11-26

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

相关文档

开发指南:hacpai.com/article/149…

hexo 文章导入:hacpai.com/article/149…

源码

源码获取

1
shell复制代码git clone --recurse-submodules https://github.com/b3log/solo.git

建库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sql复制代码CREATE DATABASE  solo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 新增用户密码,%代表所有主机,也可以具体到你的主机ip地址
CREATE USER 'solouser'@'%' IDENTIFIED WITH mysql_native_password BY 'solouser';
CREATE USER 'solouser'@'localhost' IDENTIFIED WITH mysql_native_password BY 'solouser';

-- 查询用户
select user, host, plugin, authentication_string from user;

-- 删除用户
Delete FROM USER Where user='solouser' ;
-- 删除账户及权限:
drop user solouser@'%';

-- 授权
GRANT ALL ON solo.* TO 'solouser'@'%';
GRANT ALL ON solo.* TO 'solouser'@'localhost';


-- 刷新权限,消除缓存的影响
FLUSH PRIVILEGES;

环境准备

JDK,MySQLTomcat,Solo。

Solo war 包下载地址:github.com/b3log/solo/…

Tomcat 下载地址:tomcat.apache.org/download-90…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
shell复制代码
# apt 获取的方式
sudo apt-get update
sudo apt-get install openjdk-8-jdk-headless

java -version

sudo apt-get install mysql-server

sudo apt-get install mysql-client

sudo netstat -tap | grep mysql

mysql -u root -p

CREATE DATABASE `solo` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE USER 'solouser'@'%' identified BY 'solo@pwd';
GRANT ALL ON solo.* TO 'solouser'@'%';
FLUSH PRIVILEGES;



wget https://github.com/b3log/solo/releases/download/v3.6.0/solo-v3.6.0.war
wget https://archive.apache.org/dist/tomcat/tomcat-8/v8.0.23/bin/apache-tomcat-8.0.23.tar.gz

环境搭建

安装 Java

1
2
3
shell复制代码
# 下载包的方式
tar -zxvf jdk-10.0.1_linux-x64_bin.tar.gz --directory=/mnt/blog

安装 Tomcat

1
shell复制代码tar -zxvf apache-tomcat-8.0.23.tar.gz  --directory=/mnt/apache

配置环境变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
shell复制代码# 查询 Java 目录
which java
# 结果为:/usr/bin/java

# 查软链接
ls -l /usr/bin/java
# 结果为 lrwxrwxrwx 1 root root 22 Apr 20 19:57 /usr/bin/java -> /etc/alternatives/java

# 查 JDK 目录
ls -l /etc/alternatives/java
# 结果为 lrwxrwxrwx 1 root root 46 Apr 20 19:57 /etc/alternatives/java -> /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java
# 则 JDK 目录为:/usr/lib/jvm/java-8-openjdk-amd64

echo "export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64" >> ~/.bashrc
echo "export JRE_HOME=/usr/lib/jvm/java-8-openjdk-amd64" >> ~/.bashrc


echo "export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64" >> ~/.bashrc
echo "export JRE_HOME=/usr/lib/jvm/java-8-openjdk-amd64" >> ~/.bashrc

端口号修改

1
2
3
4
5
6
7
8
shell复制代码vi ./conf/server.xml
# 修改后需要重启
./bin/shutdown.sh
./bin/startup.sh


# 查看 Tomcat 运行情况 加载db配置失败,会报错在这里
/mnt/apache/apache-tomcat-8.0.23/bin/catalina.sh run

安装 solo 博客

1
2
3
4
5
6
7
8
shell复制代码# 关闭 Tomcat
./bin/shutdown.sh

# 复制 Solo war 包到 Tomcat 的 webapps 目录下
cp solo-v3.6.0.war /mnt/apache/apache-tomcat-8.0.23/webapps/solo.war

# 启动 Tomcat
./bin/startup.sh

参考:hacpai.com/article/151…

hacpai.com/article/151…

docker 安装

1
2
3
4
5
6
sql复制代码CREATE USER 'solo'@'%' identified BY 'solo';


GRANT ALL ON solo.* TO 'solo'@'%';

FLUSH PRIVILEGES;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
shell复制代码# 拉取最新镜像
docker pull b3log/solo

# 安装 solo
docker run --detach --name solo --network=host \
--env RUNTIME_DB="MYSQL" \
--env JDBC_USERNAME="solo" \
--env JDBC_PASSWORD="solo" \
--env JDBC_DRIVER="com.mysql.cj.jdbc.Driver" \
--env JDBC_URL="jdbc:mysql://127.0.0.1:3306/solo?useUnicode=yes&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC" \
b3log/solo --listen_port=80 --server_scheme=http --server_host=yonglun.shop

启动参数说明:

--listen_port:进程监听端口
--server_scheme:最终访问协议,如果反代服务启用了 HTTPS 这里也需要改为 https
--server_host:最终访问域名或公网 IP,不要带端口号

参考:github.com/b3log/solo

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Python的从0到1(第二十天)-Python字典的基本概

发表于 2021-11-26

这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战

字典的基本概念

什么是字典

字典是Python中一种常见的数据类型,用于存储具有映射关系的数据。

在我们的日常生活中,很多数据都”组队”出现:如图书编号和图书、商品条码和名称、学生和科目成绩…..

每本图书都有自己的编号,每件商品会有专属的条码,每位学生都有对应的科目成绩…….这些成对的数据间的对应关系就属于映射关系。

简单地说,你可以把映射关系理解为两个数据间的一种对应关系。

使用字典来存储这些具有映射关系的数据会更简洁,处理起来也更方便。

接下来,我就教你创建一个字典,你先看看字典的语法:

我们用一个花括号{}把这些”成对的”数据括起来,如: {‘小白’: 90, ‘小黑’: 85},就可以定义一个字典。

在字典中,这些”成对”的数据称为键值对,如:”小白”:90就是一个键值对。

字典中的数据以键值对的形式存储,不同键值对之间用英文逗号,隔开。

键值对又由键(key)和值(value)组成,键和值用英文冒号:连接,键在冒号之前,值在冒号之后。

在字典{‘小白’: 90, ‘小黑’: 85}中,如姓名:’小白’,’小黑’就属于键,对应的成绩:90,85就属于值。

1
2
3
4
5
6
7
python复制代码# 根据要求创建一个字典,并赋值给变量 info

info = {'姓名': '张三', '年龄': 22, '身高': 175.6}

# 打印info

print(info)

接下来,我们来观察下字典info,看看字典中键和值的数据类型有哪些?

1
2
3
4
5
6
7
8
9
arduino复制代码info = {

'个人简介': ('小白', '男'),

'兴趣爱好': ['游戏', '电影'],

'联系方式': {'电话': 99996666, '邮箱': 'xiaobai@qijia.com'}

}

这个字典”有点长”,你能找出字典中的值,并说出它们的数据类型吗?

为了让你更清楚地观察这个字典,我把它分成三行来写,:

当然,不管是写成一行还是三行,字典本身并没有改变。

你可以清晰的看出字典info中3个键值对。

第一个键值对: ‘个人简介’: (‘小白’, ‘男’)

键是’个人简介’,所对应的值是(‘小白’,’男’),对于(‘小白’,’男’)这种数据类型,你还有印象吗?

这是我们刚才复习的:元组。

第二个键值对: ‘兴趣爱好’: [‘游戏’, ‘电影’]

键是’兴趣爱好’,所对应的值是[‘游戏’,’电影’],那么[‘游戏’,’电影’]又属于什么数据类型?

没错,这是一个列表。

第三个键值对: ‘联系方式’: {‘电话’: 99996666, ‘邮箱’: ‘xiaobai@qijia.com‘}

键是’联系方式’,所对应的值是 ****{‘电话’: 99996666, ‘邮箱’: ‘xiaobai@qijia.com‘},它是一个字典。

元组,列表,字典也可以作为字典中的值。


我们把刚刚探究的字典中键和值的数据类型进行一个总结:

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

☆打卡算法☆LeetCode 58、最后一个单词的长度 算法

发表于 2021-11-26

这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战

推荐阅读

  • CSDN主页
  • GitHub开源地址
  • Unity3D插件分享
  • 简书地址
  • 我的个人博客
  • QQ群:1040082875

大家好,我是小魔龙,Unity3D软件工程师,VR、AR,虚拟仿真方向,不定时更新软件开发技巧,生活感悟,觉得有用记得一键三连哦。

一、题目

1、算法题目

“给定一个由若干单词组成的字符串,返回最后一个单词的长度。”

题目链接:

来源:力扣(LeetCode)

链接:58. 最后一个单词的长度 - 力扣(LeetCode) (leetcode-cn.com)

2、题目描述

给你一个字符串 s,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中最后一个单词的长度。

单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。

1
2
3
ini复制代码示例 1:
输入: s = "Hello World"
输出: 5
1
2
3
ini复制代码示例 2:
输入: s = " fly me to the moon "
输出: 4

二、解题

1、思路分析

飘了,看见这种题都觉得简单了。。。

题目要返回最后一个单词的长度,可以直接使用反向遍历的方法,找到最后一个单词,然后返回长度即可。

从后往前找到第一个字母之后,遇到第一个空格或者到达字符串的起始位置,遍历到的字母数量就是最后一个单词的长度。

2、代码实现

代码参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
csharp复制代码public class Solution {
public int LengthOfLastWord(string s) {
int index = s.Length - 1;
while (s[index] == ' ') {
index--;
}
int wordLength = 0;
while (index >= 0 && s[index] != ' ') {
wordLength++;
index--;
}
return wordLength;
}
}

image.png

3、时间复杂度

时间复杂度 : O(n)

其中n是数组的长度,只需要遍历一遍数组即可求得答案。

空间复杂度: O(1)

只需要常数级别的空间存放变量。

三、总结

话说python这么妖孽的嘛,一行代码就能解决:

1
scss复制代码return len(s.split()[-1])

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

链表基础题

发表于 2021-11-26

这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战

2.删除所有值为x的节点(没要求递归)

思路:创建一个p和pre指针,一个指向当前节点,一个指向前一个节点

如果遇到相同,那么就”跨越”这个节点

图片.png

3.反向输出链表

思路:一直递归到最后,从而输出从里层往外输出

图片.png

4.删除链表中最小元素

##思路:维护最小值指针和他的前驱指针,遍历记录最小值,最后删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c复制代码void DeleMin(LinkList &L){

LNode *pre=L,*p=pre->next;
LNode *minpre=pre,*minp=p; //记录最小值得节点以及他的前驱

while(p != NULL){
if(p->data < minpre->data){ //后一个比前一个小
min = p; //维护min处于最小节点位置
minpre = pre; //维护minpre处于最小节点的前驱
}
p = p->next; //大步往前走
pre = pre->next; //大步往前走
}
minpre->next = min->next; //删除最小值
free(minp);
return L;
}

5.链表就地反转

思路:维护两个指针,cur和pre,让pre永远在cur的前方

1
2
3
4
5
6
7
8
9
10
c复制代码ListNode reverseList(ListNode head) {
ListNode *pre = NULL, *cur = head;
while (cur != NULL) {
ListNode *t = cur->next; //再存cur的下一个指针
cur->next = pre; //cur指向pre
pre = cur; //cur往前走一步
cur = t; //pre往前走一步
}
return pre;
}

6.重排链表,使其变成递增序列

思路:使用插入排序每次遍历到的节点,插入到前面的有序表中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
c复制代码void Sort(LinkList &L){
LNode *p = L->next;
LNode *r = p->next; //保存p的后继,防止断链
LNode *pre;
p->next = NULL; //构造第一个有序表
p = r;

while(p != NULL){
r = p->next;
pre = L;
while(pre->next != NULL && pre->next->data < p->data){
pre = pre->next; //pre往前走,寻找插入p的前驱结点
}
p->next = pre->next; //将*p插入到*pre之后
pre->next = p;
p = r;
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…180181182…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%