Java基础知识大纲


Java基础知识大纲

一.java基础问题

1.Arraylist 原理以及特性

ArrayList 是 Java 中的一个动态数组实现,它属于 Java Collections Framework 的一部分。以下是关于 ArrayList 的一些原理和特性:

1.1 底层实现原理:
  • ArrayList 的底层是基于数组实现的,它通过一个动态数组来存储元素。
  • 当数组容量不足以容纳新的元素时,ArrayList 会自动增加容量,通常是当前容量的 1.5 倍。
1.2 动态增长:
  • ArrayList 具有动态增长的能力,因此可以根据需要自动调整大小。
  • 这使得 ArrayList 在元素数量不断增加的情况下表现较好。
1.3 随机访问:
  • ArrayList 允许通过索引进行快速的随机访问,因为底层是数组,可以直接通过索引定位元素。
1.4 实现了List接口:
  • ArrayList 实现了 List 接口,因此支持有序的元素集合,并且允许包含重复元素。
1.5 可变大小:
  • 与传统数组不同,ArrayList 的大小是可变的,可以根据需要进行动态调整。
1.6 支持泛型:
  • ArrayList 支持泛型,因此可以在编译时提供类型安全性。
1.7 迭代器:
  • ArrayList 提供了迭代器,可以通过迭代器遍历集合中的元素。
1.8 不是线程安全的:
  • ArrayList 不是线程安全的,如果有多个线程同时访问一个 ArrayList 实例,并且至少有一个线程修改了该 ArrayList,那么它必须保持外部同步。
1.9 性能特性:
  • 由于 ArrayList 支持随机访问,因此适合读取操作。但是,在中间插入或删除元素时可能会比较耗时,因为需要移动元素。
1.10 自动装箱:
  • 由于 ArrayList 存储的是对象,如果存储基本数据类型,会自动进行装箱和拆箱操作。

总体而言,ArrayList 是一个灵活、方便的集合类,适用于大多数场景,但在某些特殊情况下,可能需要考虑其他数据结构的使用,比如 LinkedListHashSet,具体取决于使用场景和需求。

2. Hashmap 原理

HashMap 是 Java 中常用的数据结构之一,它实现了 Map 接口,用于存储键值对。以下是关于 HashMap 的一些原理:

2.1 底层数据结构:
  • HashMap 使用数组和链表(或红黑树)的组合来实现。
  • 数组用于存储桶(buckets),每个桶是一个链表(或红黑树)的头部。
2.2 哈希函数:
  • 当我们将键值对放入 HashMap 时,首先会根据键的哈希码计算出一个哈希值。
  • 这个哈希值用于确定该键值对在数组中的存储位置(桶)。
2.3 解决哈希冲突:
  • 不同的键可能有相同的哈希值,这就是哈希冲突。HashMap 使用链表(或红黑树)来解决冲突。
  • 如果两个键具有相同的哈希值,它们会被放入同一个桶中,形成一个链表。
  • 当链表的长度达到一定阈值时,链表会转换成红黑树,以提高查找效率。
2.4 扩容:
  • HashMap 中的元素数量超过容量乘以加载因子时,HashMap 会进行扩容。
  • 扩容会重新计算哈希值,并重新分配到新的更大的桶数组中。
2.5 加载因子:
  • 加载因子是一个用于衡量容器满的程度的参数,它的默认值为 0.75。
  • 达到加载因子时,HashMap 会自动扩容,以保持其性能。
2.6 迭代顺序:
  • HashMap 的迭代顺序不是严格按照插入顺序或者哈希值的顺序。
  • 在 Java 8 之后,HashMap 在实现中引入了红黑树,导致元素的遍历顺序可能不同于插入顺序。
2.7 线程不安全:
  • HashMap 不是线程安全的,如果有多个线程同时访问一个 HashMap 实例,并且至少有一个线程修改了该 HashMap,那么它必须保持外部同步。
2.8 性能优势:
  • 在良好的散列函数和适当的容量大小下,HashMap 具有常数时间的查找复杂度,因此在大多数情况下,它提供了高效的存储和检索性能。

HashMap 是一个常见的数据结构,用于快速检索和存储键值对。然而,在多线程环境中,需要考虑线程安全性,可以考虑使用 ConcurrentHashMap 或者在外部进行同步控制。

3. set数据结构

3.1 HashSet:
  • 原理: HashSet 基于哈希表实现,使用哈希函数将元素映射到数组索引上。它通过一个数组和链表(在Java 8之后,链表可能会转换成红黑树)的组合来存储元素。哈希表的插入、删除和查找操作具有常数级别的时间复杂度。
  • 应用场景:
    • 当你需要一个无序的、不允许重复元素的集合。
    • 当你对性能要求较高,因为 HashSet 提供了快速的插入、删除和查找操作。
3.2 LinkedHashSet:
  • 原理: LinkedHashSetHashSet 的基础上使用链表维护元素的插入顺序。它继承自 HashSet,所以也是基于哈希表实现的,但额外维护了一个链表来保持插入的顺序。
  • 应用场景:
    • 当你需要一个有序的、不允许重复元素的集合,且需要快速地按照插入顺序进行迭代。
    • 当你的应用场景需要综合考虑插入顺序和快速查找时。
3.3 TreeSet:
  • 原理: TreeSet 基于红黑树实现,保持了元素的有序性。红黑树是一种自平衡的二叉查找树,确保元素按照升序或降序排列。
  • 应用场景:
    • 当你需要一个有序的、不允许重复元素的集合,且需要按照自定义顺序进行排序。
    • 当你对元素的自然排序或者提供的比较器的性能要求较高时。
3.4 应用场景小结:
  • 通用场景: 如果只需要一个无序集合,而不关心元素的顺序,通常选择 HashSet,因为它提供了更好的性能。
  • 按插入顺序: 如果需要保持元素的插入顺序,可以选择 LinkedHashSet
  • 按自然顺序或自定义排序: 如果需要一个有序集合,并且按照元素的自然顺序或者提供的比较器进行排序,可以选择 TreeSet

选择适当的 Set 实现取决于具体的业务需求和性能要求。

二.Jpa基础知识

Java Persistence API(JPA)是一种Java EE和Java SE的规范,用于通过对象-关系映射(ORM)将Java对象映射到关系数据库。以下是关于JPA的一些基础知识:

1. 实体(Entity):

  • 在JPA中,实体是指一个简单的Java对象,它映射到数据库中的表。实体类使用 @Entity 注解进行标识。
@Entity
public class Employee {
    // 实体类的属性和对应数据库表的字段
}

2. 主键(Primary Key):

  • 实体类需要指定一个主键字段,可以使用 @Id 注解标识。主键可以是简单类型,也可以是嵌入式对象。
@Entity
public class Employee {
    @Id
    private Long id;
    // 其他属性和方法
}

3. 映射关系(Mapping Relationships):

  • JPA支持在实体之间建立关系,包括一对一、一对多、多对一和多对多关系。可以使用 @OneToOne@OneToMany@ManyToOne@ManyToMany 等注解来指定关系。
@Entity
public class Department {
    // 一对多关系
    @OneToMany(mappedBy = "department")
    private List<Employee> employees;
}

@Entity
public class Employee {
    // 多对一关系
    @ManyToOne
    private Department department;
}

4. 持久性上下文(Persistence Context):

  • 持久性上下文是一个在应用程序执行期间,用于管理实体的上下文。它负责实体的状态变更和与数据库的同步。

5. EntityManager:

  • EntityManager 是JPA中的核心接口之一,用于执行实体管理操作,如保存、更新、删除和查询实体。
@PersistenceContext
private EntityManager entityManager;

6. 查询语言(JPQL):

  • JPQL(Java Persistence Query Language)是一种基于对象模型的查询语言,用于查询实体。它类似于SQL,但是操作的是实体对象而不是数据库表。
TypedQuery<Employee> query = entityManager.createQuery(
    "SELECT e FROM Employee e WHERE e.department.name = :deptName", Employee.class);
query.setParameter("deptName", "IT");
List<Employee> resultList = query.getResultList();

7. 事务管理:

  • JPA通常与事务结合使用,以确保数据库操作的一致性。可以使用 @Transactional 注解或者在编程中通过 EntityTransaction 来管理事务。
@Transactional
public void saveEmployee(Employee employee) {
    entityManager.persist(employee);
}

8. 配置文件(persistence.xml):

  • JPA配置文件是 persistence.xml,其中定义了数据源、实体类、JPA提供商等信息。
<persistence-unit name="myPersistenceUnit" transaction-type="RESOURCE_LOCAL">
    <class>com.example.Employee</class>
    <!-- 其他配置 -->
</persistence-unit>

JPA提供了一种方便的方式来进行对象-关系映射,简化了数据库操作的过程。上述介绍是JPA的一些基础概念,实际使用中还涉及到更多高级特性和配置。

9. jpa整合应用springboot:

在Spring Boot中使用JPA通常涉及以下步骤:

9.1. 添加依赖:

确保在pom.xml(如果使用Maven)或build.gradle(如果使用Gradle)中添加Spring Boot和JPA的依赖。

<!-- Maven 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- 如果使用Hibernate作为JPA提供商 -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
</dependency>
9.2. 配置数据源:

application.propertiesapplication.yml中配置数据库连接信息。

spring.datasource.url=jdbc:mysql://localhost:3306/your_database
spring.datasource.username=your_username
spring.datasource.password=your_password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
9.3. 定义实体类:

创建JPA实体类,并使用@Entity@Id等注解标识实体和主键。

@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String department;
    // getters and setters
}
9.4. 创建Repository接口:

创建一个继承自JpaRepository的接口,它会提供一些基本的CRUD操作,你也可以自定义查询方法。

public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    List<Employee> findByDepartment(String department);
}
9.5. 使用Repository:

在你的服务类或控制器中注入EmployeeRepository,然后使用它进行数据库操作。

@Service
public class EmployeeService {
    @Autowired
    private EmployeeRepository employeeRepository;

    public List<Employee> getEmployeesByDepartment(String department) {
        return employeeRepository.findByDepartment(department);
    }

    public Employee saveEmployee(Employee employee) {
        return employeeRepository.save(employee);
    }
}
9.6. 启用JPA:

确保在主应用程序类上添加@SpringBootApplication注解,并在需要启用JPA的地方添加@EnableJpaRepositories注解。

@SpringBootApplication
@EnableJpaRepositories(basePackages = "com.example.repository")
public class YourApplication {
    public static void main(String[] args) {
        SpringApplication.run(YourApplication.class, args);
    }
}
9.7. 其他配置(可选):

根据需要,你还可以进行其他JPA和Hibernate的配置,例如配置文件persistence.xml(如果需要自定义JPA配置)或者定义Hibernate方言等。

这些步骤基本上覆盖了在Spring Boot中使用JPA的基本配置和使用方法。通过这些步骤,你可以轻松地进行数据库操作而无需写过多的SQL语句。

三.Mybatis-Plus基础知识

MyBatis-Plus是基于MyBatis的增强工具包,提供了一些增强功能和简化开发的特性。以下是MyBatis-Plus的一些基础知识:

1. 添加依赖:

pom.xml(如果使用Maven)或build.gradle(如果使用Gradle)中添加MyBatis-Plus的依赖。

<!-- Maven 依赖 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>最新版本</version>
</dependency>

2. 配置数据源:

application.propertiesapplication.yml中配置数据库连接信息,与Spring Boot的配置方式一致。

spring.datasource.url=jdbc:mysql://localhost:3306/your_database
spring.datasource.username=your_username
spring.datasource.password=your_password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

3. 定义实体类:

创建与数据库表对应的实体类,与MyBatis一样,但MyBatis-Plus提供了一些注解简化操作。

import com.baomidou.mybatisplus.annotation.TableName;

@TableName("employee")
public class Employee {
    private Long id;
    private String name;
    private String department;
    // getters and setters
}

4. 继承BaseMapper:

在DAO接口中继承BaseMapper接口,MyBatis-Plus会自动实现通用的CRUD操作。

public interface EmployeeMapper extends BaseMapper<Employee> {
    // 可以添加一些自定义的查询方法
}

5. 使用Service:

在Service中注入EmployeeMapper,并使用其提供的方法进行数据库操作。

@Service
public class EmployeeService {
    @Autowired
    private EmployeeMapper employeeMapper;

    public List<Employee> getAllEmployees() {
        return employeeMapper.selectList(null);
    }

    public Employee getEmployeeById(Long id) {
        return employeeMapper.selectById(id);
    }

    public void saveEmployee(Employee employee) {
        employeeMapper.insert(employee);
    }

    public void updateEmployee(Employee employee) {
        employeeMapper.updateById(employee);
    }

    public void deleteEmployee(Long id) {
        employeeMapper.deleteById(id);
    }
}

6. 其他特性:

MyBatis-Plus还提供了一些其他特性,如条件构造器、分页查询、逻辑删除、自动填充等,可以根据实际需求选择使用。

// 示例:条件构造器
QueryWrapper<Employee> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("department", "IT").like("name", "John");

List<Employee> employees = employeeMapper.selectList(queryWrapper);

7. 配置(可选):

根据需要,你可以在application.propertiesapplication.yml中添加一些MyBatis-Plus的配置,例如分页插件的配置。

# 分页插件配置
mybatis-plus.configuration.mapper-locations=classpath:mapper/**/*.xml
mybatis-plus.pagehelper.helper-dialect=mysql

通过上述配置和基本使用,你可以轻松地利用MyBatis-Plus进行数据库操作,而无需手写过多的SQL语句。 MyBatis-Plus的文档提供了更详细的信息,可以根据实际需求进行更高级的配置和使用。

8. Mybatis-Plus xml配置文件标签用法详情

MyBatis-Plus中的XML文件主要用于配置映射关系、SQL语句等信息,它扩展了MyBatis的XML配置文件,提供了一些用于简化和增强配置的标签。以下是一些常用的XML标签及其用法:

8.1. <resultMap>

用于映射查询结果到实体类的字段。

<resultMap id="userResultMap" type="User">
    <id property="id" column="user_id" />
    <result property="username" column="user_name" />
    <result property="password" column="user_password" />
</resultMap>
8.2. <select>

配置查询语句。

<select id="selectUserById" resultMap="userResultMap">
    SELECT * FROM user WHERE user_id = #{id}
</select>
8.3. <insert>

配置插入语句。

<insert id="insertUser" parameterType="User">
    INSERT INTO user (user_id, user_name, user_password)
    VALUES (#{id}, #{username}, #{password})
</insert>
8.4. <update>

配置更新语句。

<update id="updateUser" parameterType="User">
    UPDATE user SET user_name = #{username}, user_password = #{password}
    WHERE user_id = #{id}
</update>
8.5. <delete>

配置删除语句。

<delete id="deleteUser" parameterType="int">
    DELETE FROM user WHERE user_id = #{id}
</delete>
8.6. <if>

用于条件判断,根据条件决定是否拼接SQL片段。

<select id="selectUsers" resultMap="userResultMap">
    SELECT * FROM user
    <where>
        <if test="username != null">
            AND user_name = #{username}
        </if>
        <if test="password != null">
            AND user_password = #{password}
        </if>
    </where>
</select>
8.7. <foreach>

用于处理集合类型的参数,循环拼接SQL片段。

<select id="selectUsersByIdList" resultMap="userResultMap">
    SELECT * FROM user
    WHERE user_id IN
    <foreach collection="idList" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>
8.8. <sql>

用于定义可重用的SQL片段。

<sql id="columns">
    user_id, user_name, user_password
</sql>

<select id="selectAllUsers" resultMap="userResultMap">
    SELECT <include refid="columns" /> FROM user
</select>
8.9. <choose><when><otherwise>

用于处理多个条件的选择语句。

<select id="selectUsersByCondition" resultMap="userResultMap">
    SELECT * FROM user
    <where>
        <choose>
            <when test="username != null">
                AND user_name = #{username}
            </when>
            <when test="password != null">
                AND user_password = #{password}
            </when>
            <otherwise>
                AND 1 = 1
            </otherwise>
        </choose>
    </where>
</select>

这些XML标签提供了灵活的配置选项,允许开发者根据具体的需求进行配置。通过合理使用这些标签,可以更好地管理SQL语句和映射关系,提高代码的可维护性。

四. 线程

6.线程池有几个参数,如何配置

线程池是一种用于管理和重用线程的机制,Java中通过Executor框架提供了线程池的实现。线程池的配置通常涉及到一些重要的参数。在Java中,常见的线程池参数有以下几个:

6.1. corePoolSize(核心线程数):
  • 核心线程数是线程池中一直保持活动的线程数量,即使它们处于空闲状态。当有新任务提交时,如果当前线程池中的线程数小于核心线程数,会创建新的线程来执行任务。
  • 核心线程通常会一直存在,除非设置了allowCoreThreadTimeOut
6.2. maximumPoolSize(最大线程数):
  • 最大线程数是线程池中允许的最大线程数量。当线程池中的线程数达到核心线程数时,新任务会放入工作队列(如果队列未满),或者创建新的非核心线程(如果线程数未达到最大线程数)。
6.3. keepAliveTime(线程空闲时间):
  • 线程空闲时间是非核心线程在空闲状态下被回收前等待新任务的最长时间。如果设置了allowCoreThreadTimeOut,那么核心线程也会受到这个参数的影响。
6.4. unit(时间单位):
  • 用于指定keepAliveTime的时间单位,通常是TimeUnit.SECONDSTimeUnit.MILLISECONDS等。
6.5. workQueue(工作队列):
  • 工作队列是用于保存等待执行任务的队列。当线程池中的线程数达到核心线程数时,新任务会放入工作队列。工作队列可以是不同类型的队列,如LinkedBlockingQueueArrayBlockingQueue等。
6.6. threadFactory(线程工厂):
  • 用于创建新线程的工厂。可以通过自定义线程工厂来配置线程的一些特性,如线程的命名、优先级等。
6.7. handler(拒绝策略):
  • 当工作队列和线程池都满了,且线程池中的线程数达到最大线程数时,采取的拒绝策略。常见的拒绝策略有AbortPolicy(默认,直接抛出异常)、CallerRunsPolicy(使用调用者的线程执行任务)、DiscardPolicy(直接丢弃任务)等。
如何配置:

可以通过ThreadPoolExecutor的构造函数或者通过Executors.newXXXThreadPool等静态工厂方法来创建线程池,并传入相应的参数进行配置。例如:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize,          // 核心线程数
    maximumPoolSize,       // 最大线程数
    keepAliveTime,         // 线程空闲时间
    TimeUnit.SECONDS,       // 时间单位
    workQueue,             // 工作队列
    threadFactory,         // 线程工厂
    handler                // 拒绝策略
);

另外,Java 5及以上版本提供了Executors.newFixedThreadPoolExecutors.newCachedThreadPool等静态工厂方法,这些方法使用了一些默认的参数值,适合一些简单的使用场景。例如:

ExecutorService executor = Executors.newFixedThreadPool(10); // 创建固定大小的线程池

在配置线程池时,需要根据具体的应用场景和需求来调整参数,以确保线程池的性能和稳定性。

7.线程与进程的区别

线程(Thread)和进程(Process)是操作系统中管理执行程序的两个基本概念,它们之间有一些关键的区别:

7.1. 定义:
  • 线程: 线程是一个进程内的独立执行单元,是程序执行流的最小单元。一个进程可以包含多个线程,它们共享同一进程的资源,如内存空间、文件句柄等。
  • 进程: 进程是一个独立的程序执行单元,拥有独立的内存空间和系统资源。进程是系统分配资源的基本单位。
7.2. 资源共享:
  • 线程: 线程属于进程,共享进程的资源。多个线程可以同时访问相同的内存空间和文件,因此线程之间的通信更加方便。
  • 进程: 进程之间的资源是相互隔离的,每个进程拥有独立的内存空间和系统资源。进程之间通信相对复杂,需要通过进程间通信(IPC)的机制。
7.3. 开销:
  • 线程: 线程的创建和销毁比进程轻量级,线程切换的开销相对较小。
  • 进程: 进程的创建和销毁较为重量级,进程切换的开销相对较大。
7.4. 并发性:
  • 线程: 多线程的并发性较高,多个线程可以并行执行,共同完成任务。
  • 进程: 进程的并发性较低,进程之间相对独立,通常需要通过进程间通信来协调工作。
7.5. 健壮性:
  • 线程: 由于线程共享同一进程的资源,一个线程的错误可能影响到整个进程,导致程序崩溃。
  • 进程: 进程之间相对独立,一个进程的错误不会直接影响其他进程,因此进程的健壮性相对较高。
7.6. 切换代价:
  • 线程: 线程切换的代价相对较小,因为线程共享同一地址空间,切换时只需保存寄存器等少量状态。
  • 进程: 进程切换的代价较大,需要保存和恢复更多的状态信息,包括内存映射、文件描述符等。
7.7. 通信:
  • 线程: 线程之间的通信较为简便,可以通过共享内存等方式进行直接通信。
  • 进程: 进程之间通信相对复杂,需要使用IPC机制,如消息队列、管道、信号等。

总体来说,线程和进程都是并发编程的基本单位,各自有优势和适用场景。线程通常用于轻量级的并发任务,而进程通常用于更加独立和复杂的任务。在实际开发中,通常会根据任务的性质和要求来选择使用线程或进程。

8.线程池是怎么用的

线程池是一种用于管理和重用线程的机制,它通过维护一定数量的线程,根据需要分配任务给这些线程,可以提高线程的利用率、降低线程创建和销毁的开销。在Java中,线程池通常使用Executor框架来实现。以下是使用线程池的基本步骤:

8.1. 创建线程池:

通过Executors类提供的静态工厂方法创建不同类型的线程池,常用的线程池有:

  • newFixedThreadPool(int n):创建一个固定大小的线程池,有固定数量的线程处理任务。
  • newCachedThreadPool():创建一个可缓存的线程池,线程数根据任务的数量动态调整。
  • newSingleThreadExecutor():创建一个单线程的线程池,任务按顺序执行。
  • newScheduledThreadPool(int corePoolSize):创建一个固定大小的线程池,可以执行定时任务和周期性任务。
ExecutorService executor = Executors.newFixedThreadPool(10);
8.2. 提交任务:

使用submit方法提交任务给线程池。任务可以是Runnable接口的实现类,也可以是Callable接口的实现类。

executor.submit(new Runnable() {
    @Override
    public void run() {
        // 执行任务的代码
    }
});

或者使用Lambda表达式:

executor.submit(() -> {
    // 执行任务的代码
});
8.3. 关闭线程池:

当不再需要线程池时,应该调用shutdown()方法来关闭线程池。关闭线程池后,将不再接受新的任务,但会等待已经提交的任务执行完成。

executor.shutdown();

如果要立即关闭线程池,并尝试停止所有正在执行的任务,可以使用shutdownNow()方法:

executor.shutdownNow();
8.4. 使用Future获取任务结果(可选):

如果提交的任务实现了Callable接口,可以通过submit方法的返回值Future来获取任务的执行结果。

Future<String> future = executor.submit(new Callable<String>() {
    @Override
    public String call() throws Exception {
        // 执行任务的代码,返回结果
        return "Task Result";
    }
});

try {
    String result = future.get(); // 阻塞等待任务执行完成并获取结果
    System.out.println(result);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}
8.5. 注意事项:
  • 应根据实际需求选择合适的线程池类型。
  • 使用线程池时应根据任务的性质和规模来配置合适的线程数。
  • 线程池的生命周期应该根据应用程序的生命周期来管理,及时关闭不再需要的线程池。
  • 如果任务的执行时间较短,可以考虑使用newCachedThreadPool,而对于需要长时间运行的任务,可以使用newFixedThreadPool
  • 在Java 8 及以上版本,推荐使用java.util.concurrent.Executors.newWorkStealingPool()来创建工作窃取线程池,它结合了线程池和工作窃取算法,适用于并行计算场景。

9.线程安全性,怎么解决线程安全性

线程安全性是指在多线程环境下,程序能够正确、可靠地执行,而不会产生不确定的结果或导致数据不一致。在并发编程中,解决线程安全性问题是一个重要的任务。以下是一些常见的手段来解决线程安全性问题:

9.1. 互斥锁(Synchronization):
  • 使用关键字 synchronized 来保护共享资源,确保在同一时刻只有一个线程访问该资源。这可以通过方法级别的同步或使用同步代码块来实现。
public synchronized void synchronizedMethod() {
    // 线程安全的操作
}
public void synchronizedBlock() {
    synchronized (lockObject) {
        // 线程安全的操作
    }
}
9.2. 使用线程安全的数据结构:
  • Java提供了一些线程安全的数据结构,如ConcurrentHashMapCopyOnWriteArrayList等。这些数据结构内部使用了一些机制来保证多线程访问时的安全性。
Map<String, String> concurrentMap = new ConcurrentHashMap<>();
List<String> copyOnWriteList = new CopyOnWriteArrayList<>();
9.3. 原子类(Atomic Classes):
  • 使用java.util.concurrent.atomic包提供的原子类,如AtomicIntegerAtomicLong等,可以保证特定操作的原子性。
AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet(); // 原子操作
9.4. 使用锁机制:
  • 显式地使用ReentrantLock等锁机制,以便更细粒度地控制对共享资源的访问。
private final ReentrantLock lock = new ReentrantLock();

public void performTask() {
    lock.lock();
    try {
        // 线程安全的操作
    } finally {
        lock.unlock();
    }
}
9.5. 不可变对象:
  • 创建不可变对象,即对象一旦被创建就不能被修改。不可变对象天然是线程安全的。
public final class ImmutableObject {
    private final int value;

    public ImmutableObject(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}
9.6. 线程局部变量:
  • 使用ThreadLocal来保证每个线程都有自己的变量副本,从而避免共享变量的线程安全问题。
private static ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
9.7. 避免竞态条件(Race Condition):
  • 竞态条件是指在多线程环境下,由于执行顺序不确定而导致程序产生不正确的结果。避免竞态条件可以通过同步、加锁、使用原子操作等手段来确保一组操作的原子性。
9.8.注意事项:
  • 在解决线程安全性问题时,需要考虑性能、可维护性以及是否引入死锁等问题。
  • 避免过度的同步,因为过度同步可能导致性能下降。
  • 在设计并发程序时,考虑尽量减少共享状态,以降低并发编程的复杂度。

10 怎么创建线程,方式有几种

在Java中,创建线程的方式主要有两种:通过继承Thread类和通过实现Runnable接口。此外,在Java 5及以上版本,还引入了更为灵活的Executor框架,以及CallableFuture接口。下面分别详细介绍这几种方式:

10.1. 继承 Thread 类:

通过创建一个继承自Thread类的子类,重写run()方法来定义线程的执行逻辑。

class MyThread extends Thread {
    public void run() {
        // 线程执行的逻辑
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getId() + " Value " + i);
        }
    }
}

public class ThreadExample {
    public static void main(String args[]) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();

        // 启动线程
        thread1.start();
        thread2.start();
    }
}
10.2. 实现 Runnable 接口:

通过创建一个实现了Runnable接口的类,实现run()方法,然后创建Thread对象并将实现了Runnable接口的对象传递给Thread构造函数。

class MyRunnable implements Runnable {
    public void run() {
        // 线程执行的逻辑
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getId() + " Value " + i);
        }
    }
}

public class RunnableExample {
    public static void main(String args[]) {
        Thread thread1 = new Thread(new MyRunnable());
        Thread thread2 = new Thread(new MyRunnable());

        // 启动线程
        thread1.start();
        thread2.start();
    }
}
10.3. 使用 CallableFuture(Java 5及以上):

通过创建一个实现Callable接口的类,使用ExecutorServicesubmit方法来提交任务,并获得Future对象。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

class MyCallable implements Callable<String> {
    public String call() {
        // 线程执行的逻辑
        return "Task completed";
    }
}

public class CallableExample {
    public static void main(String args[]) throws Exception {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        Future<String> future = executorService.submit(new MyCallable());

        // 获取线程执行结果
        String result = future.get();
        System.out.println(result);

        // 关闭线程池
        executorService.shutdown();
    }
}
10.4. 使用 Executor 框架(Java 5及以上):

Executor框架提供了更高级的线程管理和控制机制,通过使用ExecutorService来提交和管理任务。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class MyRunnable implements Runnable {
    public void run() {
        // 线程执行的逻辑
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getId() + " Value " + i);
        }
    }
}

public class ExecutorExample {
    public static void main(String args[]) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        // 提交任务给线程池
        executorService.submit(new MyRunnable());
        executorService.submit(new MyRunnable());

        // 关闭线程池
        executorService.shutdown();
    }
}

在选择创建线程的方式时,一般推荐使用实现Runnable接口的方式,因为它更灵活,一个类可以同时实现多个接口。此外,使用Executor框架能够更好地控制线程的生命周期、并发度等。

五.springboot知识

1.springboot 常用注解的用法

Spring Boot提供了许多注解,用于简化应用程序的开发和配置。以下是一些常用的Spring Boot注解及其用法:

1.1. @SpringBootApplication

用于标注主类,通常位于Spring Boot应用程序的入口类上。它包含了@Configuration@EnableAutoConfiguration@ComponentScan注解,用于开启自动配置和组件扫描。

@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}
1.2. @Controller@RestController

用于定义控制器类。@RestController注解相当于@Controller + @ResponseBody,用于返回JSON格式的数据。

@RestController
public class MyController {
    @GetMapping("/hello")
    public String hello() {
        return "Hello, Spring Boot!";
    }
}
1.3. @Service

用于标注服务类。通常用于注入到其他组件中。

@Service
public class MyService {
    public String doSomething() {
        return "Service is doing something.";
    }
}
1.4. @Repository

用于标注数据访问组件,通常用于注入到服务层中。

@Repository
public class MyRepository {
    public String getData() {
        return "Data from repository.";
    }
}
1.5. @Autowired

用于自动装配Bean,可以标注在字段、构造方法、Setter方法上。

@Service
public class MyService {
    private final MyRepository myRepository;

    @Autowired
    public MyService(MyRepository myRepository) {
        this.myRepository = myRepository;
    }

    // other methods...
}
1.6. @Value

用于注入配置文件中的属性值。

@Service
public class MyService {
    @Value("${my.property}")
    private String myProperty;

    // other methods...
}
1.7. @RequestMapping@GetMapping@PostMapping@PutMapping@DeleteMapping

用于定义请求映射。@RequestMapping可以用于类和方法上,而其他注解一般用于方法上。

@RestController
@RequestMapping("/api")
public class MyController {
    @GetMapping("/hello")
    public String hello() {
        return "Hello, Spring Boot!";
    }
}
1.8. @PathVariable@RequestParam

用于获取请求路径中的变量或请求参数。

@RestController
@RequestMapping("/api")
public class MyController {
    @GetMapping("/user/{id}")
    public String getUserById(@PathVariable Long id) {
        return "User ID: " + id;
    }

    @GetMapping("/greet")
    public String greet(@RequestParam String name) {
        return "Hello, " + name + "!";
    }
}
1.9. @Configuration

用于定义配置类,可以在配置类中定义Bean。

@Configuration
public class MyConfiguration {
    @Bean
    public MyBean myBean() {
        return new MyBean();
    }
}

这些注解只是Spring Boot中的一部分,实际上还有很多其他有用的注解。使用这些注解可以有效简化代码,提高开发效率。

六.spring知识

Spring是一个广泛用于构建企业级Java应用程序的开源框架。它提供了全面的基础设施支持,使得开发者能够专注于业务逻辑的开发而不用过多关注底层的复杂性。以下是一些关键的Spring知识点:

1. 核心概念:
  • IoC(Inversion of Control): IoC是Spring的核心理念,它通过将对象的创建和管理交给Spring容器来实现,降低了组件之间的耦合度。
  • DI(Dependency Injection): DI是IoC的一种具体实现,通过注入依赖关系来实现对象之间的解耦。Spring支持构造器注入、属性注入和方法注入。
2. Spring容器:
  • BeanFactory: 是Spring的最基本容器,负责管理Bean的生命周期和依赖关系。
  • ApplicationContext: 是BeanFactory的扩展,提供更多的功能,如事件传播、国际化、资源加载等。
3. Spring配置:
  • XML配置: 通过XML文件配置Spring Bean和相关的依赖关系。
  • 注解配置: 使用注解(如@Component@Configuration@Autowired等)替代XML配置,提高开发效率。
4. AOP(Aspect-Oriented Programming):
  • AOP是一种编程范式,通过在不同的模块中定义横切关注点(cross-cutting concerns)并将其与主业务逻辑解耦。Spring提供了强大的AOP支持,可以通过注解或XML配置进行切面编程。
5. 数据访问与事务管理:
  • Spring JDBC: 简化了JDBC代码的编写,提供了异常处理和资源管理等功能。
  • ORM(Object-Relational Mapping): Spring支持多种ORM框架,如Hibernate、MyBatis等,简化了数据库操作。
  • 事务管理: Spring提供声明式事务管理,可以通过注解或XML配置来实现事务。
6. Spring MVC:
  • Spring MVC是一个基于MVC模式的Web框架,用于构建Web应用程序。它提供了模型(Model)、视图(View)、控制器(Controller)的支持,并通过注解简化了URL映射、数据绑定等操作。
7. Spring Boot:
  • Spring Boot是Spring家族中的一个子项目,用于简化Spring应用程序的开发和部署。它提供了一套默认的配置,支持快速搭建、运行和测试Spring应用程序。
8. Spring Security:
  • Spring Security是用于处理身份验证和授权的框架,提供了一系列的过滤器和扩展点,用于保护应用程序的安全性。
9. Spring Cloud:
  • Spring Cloud是用于构建分布式系统的一组工具,提供了服务发现、配置管理、负载均衡等功能,支持构建微服务架构。
10. 测试:
  • Spring提供了一系列的测试支持,包括单元测试、集成测试、模拟测试等。可以使用JUnit或TestNG等测试框架进行测试。

这些是Spring框架的一些核心知识点,Spring的强大之处在于它提供了一整套解决方案,涵盖了应用程序开发的各个方面。在使用Spring时,开发者可以根据实际需求选择合适的模块和组件,以构建高效、可维护的企业级应用程序。

11. Spring注解用法

Spring框架提供了丰富的注解,用于简化配置和开发,以下是一些常用的Spring注解及其用法:

11.1. @Component@Service@Repository@Controller

这些注解用于声明一个类为Spring的组件,分别用于标识普通组件、服务层组件、数据访问组件和控制器组件。

@Component
public class MyComponent {
    // ...
}

@Service
public class MyService {
    // ...
}

@Repository
public class MyRepository {
    // ...
}

@Controller
public class MyController {
    // ...
}
11.2. @Autowired

用于自动装配(注入)Bean,可以标注在字段、构造方法、Setter方法上。

@Service
public class MyService {
    private final MyRepository myRepository;

    @Autowired
    public MyService(MyRepository myRepository) {
        this.myRepository = myRepository;
    }

    // other methods...
}
11.3. @Value

用于注入配置文件中的属性值。

@Service
public class MyService {
    @Value("${my.property}")
    private String myProperty;

    // other methods...
}
11.4. @Configuration

用于定义配置类,通常与@Bean注解一起使用,将方法返回的对象注册为Spring的Bean。

@Configuration
public class MyConfiguration {
    @Bean
    public MyBean myBean() {
        return new MyBean();
    }
}
11.5. @Primary@Qualifier

用于解决自动装配时的歧义性。@Primary用于标识首选的Bean,而@Qualifier用于指定具体要注入的Bean。

@Component
@Primary
public class MyPrimaryBean implements MyInterface {
    // ...
}

@Component
public class MySecondaryBean implements MyInterface {
    // ...
}

@Service
public class MyService {
    @Autowired
    private MyInterface myInterface;

    // ...
}
11.6. @Scope

用于指定Bean的作用域,包括单例(Singleton)、原型(Prototype)等。

@Component
@Scope("prototype")
public class MyPrototypeBean {
    // ...
}
11.7. @RequestMapping@GetMapping@PostMapping@PutMapping@DeleteMapping

用于定义请求映射。

@Controller
@RequestMapping("/api")
public class MyController {
    @GetMapping("/hello")
    public String hello() {
        return "Hello, Spring!";
    }
}
11.8. @ResponseBody@RequestBody

用于处理HTTP请求和响应中的数据。@ResponseBody用于将方法返回的对象转换为HTTP响应体,而@RequestBody用于将HTTP请求体转换为方法参数。

@RestController
public class MyRestController {
    @PostMapping("/submit")
    @ResponseBody
    public String submit(@RequestBody MyObject myObject) {
        // ...
    }
}
11.9. @Transactional

用于声明事务管理,通常标注在方法或类上。

@Service
@Transactional
public class MyTransactionalService {
    // ...
}
11.10. @Async

用于声明异步方法,通常标注在方法上。

@Service
public class MyAsyncService {
    @Async
    public void asyncMethod() {
        // ...
    }
}

这些注解是Spring框架中常用的一些注解,它们可以帮助开发者简化配置、提高开发效率,并提供了更灵活的方式来处理依赖注入、事务管理、请求处理等方面的问题。在实际应用中,根据具体的场景和需求选择合适的注解是非常重要的。

12. Java bean注入方式

在Java中,Bean的注入方式通常有三种:构造器注入、Setter方法注入和字段注入。这些注入方式可以通过依赖注入(DI)的方式来实现,Spring框架是其中一个典型的使用依赖注入的例子。

12.1. 构造器注入:

通过构造器注入,依赖关系通过构造器的参数传递进来。

public class MyService {
    private final MyRepository myRepository;

    // 构造器注入
    public MyService(MyRepository myRepository) {
        this.myRepository = myRepository;
    }

    // other methods...
}
12.2. Setter方法注入:

通过Setter方法注入,依赖关系通过Setter方法设置。

public class MyService {
    private MyRepository myRepository;

    // Setter方法注入
    public void setMyRepository(MyRepository myRepository) {
        this.myRepository = myRepository;
    }

    // other methods...
}
12.3. 字段注入:

通过字段注入,依赖关系通过直接设置字段的方式实现。

public class MyService {
    // 字段注入
    private MyRepository myRepository;

    // other methods...
}
12.4. 使用@Autowired注解进行自动注入:

Spring框架提供了@Autowired注解,用于标注在构造器、Setter方法、字段上,以实现自动装配。通过@Autowired注解,Spring容器会自动寻找匹配的Bean进行注入。

12.4.1. 使用构造器注入:
public class MyService {
    private final MyRepository myRepository;

    // 构造器注入
    @Autowired
    public MyService(MyRepository myRepository) {
        this.myRepository = myRepository;
    }

    // other methods...
}
12.4.2. 使用Setter方法注入:
public class MyService {
    private MyRepository myRepository;

    // Setter方法注入
    @Autowired
    public void setMyRepository(MyRepository myRepository) {
        this.myRepository = myRepository;
    }

    // other methods...
}
12.4.3. 使用字段注入:
public class MyService {
    // 字段注入
    @Autowired
    private MyRepository myRepository;

    // other methods...
}

选择使用哪种注入方式通常取决于个人或团队的偏好,以及具体的业务场景。构造器注入通常被认为是最佳实践,因为它确保在对象创建时,所有必需的依赖关系都已经被满足。 Setter方法注入和字段注入也是常见的,根据实际需要选择适合的方式。

13. Spring中ioc概念及应用

IoC(Inversion of Control) 是 Spring 框架的核心概念之一。它是一种设计思想,通过将控制权反转给框架,由框架负责控制对象的创建和管理,而不是由应用程序代码直接控制。

在传统的编程模型中,应用程序代码通常会负责创建和管理对象。而在 IoC 容器中,对象的创建和管理被交由容器负责,应用程序代码只需关注对象的使用。IoC 容器负责将各个组件(对象)连接起来,形成一个完整的应用程序。

Spring IoC 的核心容器是 BeanFactoryApplicationContext

13.1. BeanFactory:

BeanFactory 是 Spring IoC 容器的最基本形式,它负责管理 Spring 应用中的所有对象(Bean)。在配置文件中定义的 Bean 会在应用运行时由 BeanFactory 进行创建和管理。

XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource("spring-config.xml"));
MyService myService = (MyService) factory.getBean("myService");
13.2. ApplicationContext:

ApplicationContextBeanFactory 的扩展,提供了更多的功能。除了管理 Bean 的生命周期外,它还支持国际化、事件传播、资源加载等。

ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
MyService myService = context.getBean(MyService.class);
13.3. Bean的定义:

在 Spring IoC 容器中,Bean 的定义包括 Bean 的标识符(ID)、类型(Class)以及配置元数据(Properties)。Bean 的定义通常通过 XML 配置文件、Java 注解或 Java 代码进行。

<!-- XML 配置文件中的 Bean 定义 -->
<bean id="myService" class="com.example.MyService" />

<!-- 使用 Java 注解进行 Bean 定义 -->
@Component
public class MyService {
    // ...
}

<!-- 使用 Java 代码进行 Bean 定义 -->
@Configuration
public class AppConfig {
    @Bean
    public MyService myService() {
        return new MyService();
    }
}
13.4. 依赖注入(Dependency Injection):

IoC 的另一个关键概念是依赖注入,它是 IoC 的具体实现。在 Spring 中,依赖注入可以通过构造器注入、Setter 方法注入、字段注入等方式实现。Spring IoC 容器负责将 Bean 之间的依赖关系建立起来。

public class MyService {
    private MyRepository myRepository;

    // 通过构造器注入
    public MyService(MyRepository myRepository) {
        this.myRepository = myRepository;
    }

    // 通过 Setter 方法注入
    public void setMyRepository(MyRepository myRepository) {
        this.myRepository = myRepository;
    }

    // 通过字段注入
    @Autowired
    private MyRepository myRepository;
}
13.5. IoC 的优势:
  • 松耦合(Loose Coupling): IoC 通过将对象之间的依赖关系交由容器管理,降低了组件之间的耦合度,使得代码更加灵活和可维护。
  • 可测试性(Testability): 由于对象的创建和管理被容器接管,可以更轻松地进行单元测试,更容易模拟和替换对象。
  • 可维护性(Maintainability): IoC 通过集中管理对象的创建和配置,使得系统更易于维护和扩展。
  • 灵活性(Flexibility): 可以通过配置文件或注解来改变应用程序的行为,而不需要修改代码。

总体而言,IoC 是 Spring 框架的核心理念,通过它,我们可以更好地组织和管理应用程序的组件,提高代码的可读性、可维护性和可测试性。

14. AOP含义用法

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

通知(Advice)

通知描述了切面要完成的工作以及何时执行。比如我们的日志切面需要记录每个接口调用时长,就需要在接口调用前后分别记录当前时间,再取差值。

  • 前置通知(Before):在目标方法调用前调用通知功能;

  • 后置通知(After):在目标方法调用之后调用通知功能,不关心方法的返回结果;

  • 返回通知(AfterReturning):在目标方法成功执行之后调用通知功能;

  • 异常通知(AfterThrowing):在目标方法抛出异常后调用通知功能;

  • 环绕通知(Around):通知包裹了目标方法,在目标方法调用之前和之后执行自定义的行为。

连接点(JoinPoint)
  • 通知功能被应用的时机。比如接口方法被调用的时候就是日志切面的连接点。
切点(Pointcut)
  • 切点定义了通知功能被应用的范围。比如日志切面的应用范围就是所有接口,即所有controller层的接口方法。
切面(Aspect)
  • 切面是通知和切点的结合,定义了何时、何地应用通知功能。
引入(Introduction)
  • 在无需修改现有类的情况下,向现有的类添加新方法或属性。
织入(Weaving)
  • 把切面应用到目标对象并创建新的代理对象的过程。
AOP相关注解
  • @Aspect:用于定义切面
  • @Before:通知方法会在目标方法调用之前执行
  • @After:通知方法会在目标方法返回或抛出异常后执行
  • @AfterReturning:通知方法会在目标方法返回后执行
  • @AfterThrowing:通知方法会在目标方法抛出异常后执行
  • @Around:通知方法会将目标方法封装起来
  • @Pointcut:定义切点表达式
切点表达式

指定了通知被应用的范围,表达式格式:

execution(方法修饰符 返回类型 方法所属的包.类名.方法名称(方法参数))
//com.macro.mall.controller包中所有类的public方法都应用切面里的通知
execution(public * com.macro.mall.controller.*.*(..))
//com.macro.mall包及包含controller的子包下所有类中的所有方法都应用切面里的通知
execution(public * com.macro.mall.*.controller.*.*(..))
//com.macro.mall.controller.PmsBrandController类中的所有方法都应用切面里的通知
execution(* com.macro.mall.controller.PmsBrandController.*(..))

七.Redis+AOP优化权限管理功能

原由:由于每次访问接口进行权限校验时都会从数据库中去查询用户信息,这样做会带来性能问题。

解决方案:使用Redis作为缓存,把用户信息和用户资源信息存入到Redis中去,避免频繁查询数据库。使用的是RedisTemple的操作方式。当查询数据时,先去Redis缓存中查询,如果Redis中没有,再从数据库查询,查询到以后在把数据存储到Redis中去。

/**
 * UmsAdminService实现类
 * Created by macro on 2018/4/26.
 */
@Service
public class UmsAdminServiceImpl implements UmsAdminService {

    @Override
    public UmsAdmin getAdminByUsername(String username) {
        //先从缓存中获取数据
        UmsAdmin admin = getCacheService().getAdmin(username);
        if(admin!=null) return  admin;
        //缓存中没有从数据库中获取
        UmsAdminExample example = new UmsAdminExample();
        example.createCriteria().andUsernameEqualTo(username);
        List<UmsAdmin> adminList = adminMapper.selectByExample(example);
        if (adminList != null && adminList.size() > 0) {
            admin = adminList.get(0);
            //将数据库中的数据存入缓存中
            getCacheService().setAdmin(admin);
            return admin;
        }
        return null;
    }

    @Override
    public List<UmsResource> getResourceList(Long adminId) {
        //先从缓存中获取数据
        List<UmsResource> resourceList = getCacheService().getResourceList(adminId);
        if(CollUtil.isNotEmpty(resourceList)){
            return  resourceList;
        }
        //缓存中没有从数据库中获取
        resourceList = adminRoleRelationDao.getResourceList(adminId);
        if(CollUtil.isNotEmpty(resourceList)){
            //将数据库中的数据存入缓存中
            getCacheService().setResourceList(adminId,resourceList);
        }
        return resourceList;
    }
    
    @Override
    public UmsAdminCacheService getCacheService() {
        return SpringUtil.getBean(UmsAdminCacheService.class);
    }
}

上面这种查询操作其实用Spring Cache来操作更简单,直接使用@Cacheable即可实现,为什么还要使用RedisTemplate来直接操作呢?因为作为缓存,我们所希望的是,如果Redis宕机了,我们的业务逻辑不会有影响,而使用Spring Cache来实现的话,当Redis宕机以后,用户的登录等种种操作就会都无法进行了。

当Redis宕机以后,我们直接就无法登录了,下面我们使用AOP来解决这个问题。

使用AOP处理缓存操作异常

为什么要用AOP来解决这个问题呢?因为我们的缓存业务类UmsAdminCacheService已经写好了,要保证缓存业务类中的方法执行不影响正常的业务逻辑,就需要在所有方法中添加try catch逻辑。使用AOP,我们可以在一个地方写上try catch逻辑,然后应用到所有方法上去。试想下,我们如果又多了几个缓存业务类,只要配置下切面即可,这波操作多方便!

首先我们先定义一个切面,在相关缓存业务类上面应用,在它的环绕通知中直接处理掉异常,保障后续操作能执行。

/**
 * Redis缓存切面,防止Redis宕机影响正常业务逻辑
 * Created by macro on 2020/3/17.
 */
@Aspect
@Component
@Order(2)
public class RedisCacheAspect {
    private static Logger LOGGER = LoggerFactory.getLogger(RedisCacheAspect.class);

    @Pointcut("execution(public * com.macro.mall.portal.service.*CacheService.*(..)) || execution(public * com.macro.mall.service.*CacheService.*(..))")
    public void cacheAspect() {
    }

    @Around("cacheAspect()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = null;
        try {
            result = joinPoint.proceed();
        } catch (Throwable throwable) {
            LOGGER.error(throwable.getMessage());
        }
        return result;
    }

}

这样处理之后,就算我们的Redis宕机了,我们的业务逻辑也能正常执行。

不过并不是所有的方法都需要处理异常的,比如我们的验证码存储,如果我们的Redis宕机了,我们的验证码存储接口需要的是报错,而不是返回执行成功。

对于上面这种需求我们可以通过自定义注解来完成,首先我们自定义一个CacheException注解,如果方法上面有这个注解,发生异常则直接抛出。

/**
 * 自定义注解,有该注解的缓存方法会抛出异常
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheException {
}

之后需要改造下我们的切面类,对于有@CacheException注解的方法,如果发生异常直接抛出。

/**
 * Redis缓存切面,防止Redis宕机影响正常业务逻辑
 * Created by macro on 2020/3/17.
 */
@Aspect
@Component
@Order(2)
public class RedisCacheAspect {
    private static Logger LOGGER = LoggerFactory.getLogger(RedisCacheAspect.class);

    @Pointcut("execution(public * com.macro.mall.portal.service.*CacheService.*(..)) || execution(public * com.macro.mall.service.*CacheService.*(..))")
    public void cacheAspect() {
    }

    @Around("cacheAspect()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        Object result = null;
        try {
            result = joinPoint.proceed();
        } catch (Throwable throwable) {
            //有CacheException注解的方法需要抛出异常
            if (method.isAnnotationPresent(CacheException.class)) {
                throw throwable;
            } else {
                LOGGER.error(throwable.getMessage());
            }
        }
        return result;
    }

}

接下来我们需要把@CacheException注解应用到存储和获取验证码的方法上去,这里需要注意的是要应用在实现类上而不是接口上,因为isAnnotationPresent方法只能获取到当前方法上的注解,而不能获取到它实现接口方法上的注解。

/**
 * UmsMemberCacheService实现类
 * Created by macro on 2020/3/14.
 */
@Service
public class UmsMemberCacheServiceImpl implements UmsMemberCacheService {
    @Autowired
    private RedisService redisService;
    
    @CacheException
    @Override
    public void setAuthCode(String telephone, String authCode) {
        String key = REDIS_DATABASE + ":" + REDIS_KEY_AUTH_CODE + ":" + telephone;
        redisService.set(key,authCode,REDIS_EXPIRE_AUTH_CODE);
    }

    @CacheException
    @Override
    public String getAuthCode(String telephone) {
        String key = REDIS_DATABASE + ":" + REDIS_KEY_AUTH_CODE + ":" + telephone;
        return (String) redisService.get(key);
    }
}

总结:对于影响性能的,频繁查询数据库的操作,我们可以通过Redis作为缓存来优化。缓存操作不该影响正常业务逻辑,我们可以使用AOP来统一处理缓存操作中的异常。

八.springMVC知识

SpringMVC的含义以及请求流程

SpringMVC(Model-View-Controller)是 Spring 框架中用于开发基于Java的Web应用程序的一部分。它提供了一种用于构建灵活且可扩展的Web应用程序的模型-视图-控制器架构。下面是关于SpringMVC的含义和请求流程的简要说明:

含义:
  1. Model(模型): 代表应用程序的数据模型,通常表示业务对象或数据访问层。模型负责管理数据、业务规则和逻辑。

  2. View(视图): 负责显示用户界面,接受用户输入。通常是JSP页面、HTML、或者Thymeleaf等模板引擎,用于展示数据给用户。

  3. Controller(控制器): 接收用户输入并处理业务逻辑,负责将用户请求映射到相应的模型和视图。控制器充当模型和视图之间的协调者。

请求流程:
  1. 客户端发送请求: 客户端(浏览器)发送HTTP请求到SpringMVC应用程序。

  2. DispatcherServlet(前端控制器)处理请求: DispatcherServlet 是SpringMVC的前端控制器,它负责接收所有的请求,并将请求分发给合适的控制器。

  3. HandlerMapping选择控制器: HandlerMapping 根据请求的URL映射到相应的控制器(Controller)。Controller是一个用于处理请求的Java类。

  4. Controller处理请求: Controller执行业务逻辑,可能调用业务服务、访问数据库等,然后根据请求返回一个ModelAndView对象。

  5. ModelAndView返回视图和模型数据: ModelAndView 包含了视图名和要传递给视图的模型数据。视图名将被解析为实际的视图(JSP、Thymeleaf等),模型数据将用于在视图中渲染内容。

  6. ViewResolver解析视图: ViewResolver 将视图名解析为实际的视图,决定使用哪个视图引擎(JSP、Thymeleaf等)来渲染页面。

  7. 视图渲染: 视图引擎将使用模型数据来渲染页面,生成HTML响应。

  8. DispatcherServlet返回响应: DispatcherServlet 将渲染好的HTML响应发送回客户端。

这是一个简化的SpringMVC请求流程,实际应用中可能包含更多的细节和配置。SpringMVC提供了丰富的特性和配置选项,使得开发者能够轻松构建灵活且可维护的Web应用程序。

九.Springboot怎么整合Spring Security

在 Spring Boot 中整合 Spring Security 相对简单,Spring Boot 提供了许多自动配置,你只需要添加依赖并进行一些基本配置即可。以下是整合 Spring Security 的基本步骤:

1. 添加 Spring Security 依赖:

在项目的 Maven 或 Gradle 配置文件中添加 Spring Security 的依赖。

Maven:

<dependencies>
    <!-- Spring Security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
</dependencies>

Gradle:

dependencies {
    // Spring Security
    implementation 'org.springframework.boot:spring-boot-starter-security'
}

2. 配置 Spring Security:

在 Spring Boot 项目中,你可以在 application.propertiesapplication.yml 文件中进行基本配置。以下是一个简单的配置示例:

# application.yml

spring:
  security:
    user:
      name: user
      password: password
    basic:
      enabled: true

这个配置启用了基本的 HTTP 身份验证,并定义了一个默认用户(用户名:user,密码:password)。

3. 自定义配置:

如果你需要进行更复杂的配置,例如定义哪些路径需要身份验证,如何处理登录和注销等,可以创建一个配置类并继承 WebSecurityConfigurerAdapter

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .logoutSuccessUrl("/")
                .permitAll();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

上述配置示例定义了:

  • /public/** 路径下的所有请求不需要身份验证。
  • 其他所有请求都需要身份验证。
  • 配置了自定义登录页和登出操作。

4. 添加用户和角色:

你可以通过实现 UserDetailsService 接口或使用注解的方式来定义用户和角色信息。

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 实现自定义的用户加载逻辑,返回一个 UserDetails 对象
        return User.withUsername(username)
            .password("{noop}password") // 密码需要以明文形式存储,这里使用 {noop} 前缀表示明文
            .roles("USER")
            .build();
    }
}

这只是一个简单的入门示例,实际项目中可能需要更复杂的配置和自定义逻辑。Spring Security 提供了强大的功能,可以满足复杂的安全性需求。

十.Spring Security 与RBAC整合

Spring Security 可以很容易与 RBAC(基于角色的访问控制)模型进行整合。在整合过程中,你可以使用 Spring Security 提供的注解和配置来定义角色和权限,并控制访问。以下是整合 Spring Security 和 RBAC 的一些建议:

1. 添加 Spring Security 依赖:

在项目的 Maven 或 Gradle 配置文件中添加 Spring Security 的依赖。

Maven:
<dependencies>
    <!-- Spring Security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
</dependencies>
Gradle:
dependencies {
    // Spring Security
    implementation 'org.springframework.boot:spring-boot-starter-security'
}

2. 配置 Spring Security:

在项目中创建一个配置类,继承 WebSecurityConfigurerAdapter,通过配置类定义角色和权限,以及访问规则。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/public/**").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasRole("USER")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .logoutSuccessUrl("/")
                .permitAll();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

上述配置示例中:

  • /public/** 路径下的所有请求不需要身份验证。
  • /admin/** 路径下的请求需要拥有 “ADMIN” 角色才能访问。
  • /user/** 路径下的请求需要拥有 “USER” 角色才能访问。
  • 其他所有请求需要身份验证。

3. 用户角色和权限管理:

可以通过实现 UserDetailsService 接口或使用注解的方式来定义用户和角色信息。

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 实现自定义的用户加载逻辑,返回一个 UserDetails 对象
        return User.withUsername(username)
            .password("{bcrypt}$2a$10$eVR0uFjMlG0EukF3h9WPH.FI5iY0wha6w4tY0FcRQ8O6zh5dm5IEe") // 使用 BCryptPasswordEncoder 加密的密码
            .roles("USER")
            .build();
    }
}

4. 加密密码:

为了提高安全性,建议使用密码加密。上述示例中使用了 BCryptPasswordEncoder 加密密码。你可以使用其他加密方式,如 PasswordEncoderFactories.createDelegatingPasswordEncoder()

5. 自定义访问决策管理器(可选):

如果你有更复杂的访问控制需求,可以考虑实现自定义的访问决策管理器。

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;

import java.util.Collection;

public class CustomAccessDecisionManager implements AccessDecisionManager {

    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
        // 自定义访问决策逻辑
        // ...
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

通过实现 AccessDecisionManager 接口,你可以实现自己的访问决策逻辑。

这只是一个基本示例,实际项目中可能需要更复杂的配置和自定义逻辑。Spring Security 提供了强大的功能,可以满足复杂的安全性需求。

十一.Spring Security实现接口的动态权限控制

1. 实现方法

1.1. 原先权限控制是采用Spring Security的默认机制实现的,在需要权限的接口上使用@PreAuthorize注解定义好需要的权限,然后将该权限值存入到权限表中,当用户登录时,将其所拥有的权限查询出来
1.2. 之后Spring Security把用户拥有的权限值和接口上注解定义的权限值进行比对,如果包含则可以访问,反之就不可以访问;

2. 使用默认的权限控制产生的问题

2.1. 需要在每个接口上都定义好访问该接口的权限值,而且只能挨个控制接口的权限,无法批量控制。其实每个接口都可以由它的访问路径唯一确定,我们可以使用基于路径的动态权限控制来解决这些问题。

十二. Spring Security实现基于路径的动态权限

首先我们需要创建一个过滤器,用于实现动态权限控制,这里需要注意的是doFilter方法,对于OPTIONS请求直接放行,否则前端调用会出现跨域问题。对于配置在IgnoreUrlsConfig中的白名单路径我也需要直接放行,所有的鉴权操作都会在super.beforeInvocation(fi)中进行。

/**
 * 动态权限过滤器,用于实现基于路径的动态权限过滤
 * Created by macro on 2020/2/7.
 */
public class DynamicSecurityFilter extends AbstractSecurityInterceptor implements Filter {

    @Autowired
    private DynamicSecurityMetadataSource dynamicSecurityMetadataSource;
    @Autowired
    private IgnoreUrlsConfig ignoreUrlsConfig;

    @Autowired
    public void setMyAccessDecisionManager(DynamicAccessDecisionManager dynamicAccessDecisionManager) {
        super.setAccessDecisionManager(dynamicAccessDecisionManager);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
        //OPTIONS请求直接放行
        if(request.getMethod().equals(HttpMethod.OPTIONS.toString())){
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            return;
        }
        //白名单请求直接放行
        PathMatcher pathMatcher = new AntPathMatcher();
        for (String path : ignoreUrlsConfig.getUrls()) {
            if(pathMatcher.match(path,request.getRequestURI())){
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
                return;
            }
        }
        //此处会调用AccessDecisionManager中的decide方法进行鉴权操作
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.afterInvocation(token, null);
        }
    }

    @Override
    public void destroy() {
    }

    @Override
    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }

    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return dynamicSecurityMetadataSource;
    }

}

在DynamicSecurityFilter中调用super.beforeInvocation(fi)方法时会调用AccessDecisionManager中的decide方法用于鉴权操作,而decide方法中的configAttributes参数会通过SecurityMetadataSource中的getAttributes方法来获取,configAttributes其实就是配置好的访问当前接口所需要的权限,下面是简化版的beforeInvocation源码。

public abstract class AbstractSecurityInterceptor implements InitializingBean,
		ApplicationEventPublisherAware, MessageSourceAware {
    

protected InterceptorStatusToken beforeInvocation(Object object) {
        
        //获取元数据
		Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
				.getAttributes(object);

		Authentication authenticated = authenticateIfRequired();

		//进行鉴权操作
		try {
			this.accessDecisionManager.decide(authenticated, object, attributes);
		}
		catch (AccessDeniedException accessDeniedException) {
			publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
					accessDeniedException));

			throw accessDeniedException;
		}
	}
}

需要自己实现SecurityMetadataSource接口的getAttributes方法,用于获取当前访问路径所需资源。

/**
 * 动态权限数据源,用于获取动态权限规则
 * Created by macro on 2020/2/7.
 */
public class DynamicSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    private static Map<String, ConfigAttribute> configAttributeMap = null;
    @Autowired
    private DynamicSecurityService dynamicSecurityService;

    @PostConstruct
    public void loadDataSource() {
        configAttributeMap = dynamicSecurityService.loadDataSource();
    }

    public void clearDataSource() {
        configAttributeMap.clear();
        configAttributeMap = null;
    }

    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        if (configAttributeMap == null) this.loadDataSource();
        List<ConfigAttribute>  configAttributes = new ArrayList<>();
        //获取当前访问的路径
        String url = ((FilterInvocation) o).getRequestUrl();
        String path = URLUtil.getPath(url);
        PathMatcher pathMatcher = new AntPathMatcher();
        Iterator<String> iterator = configAttributeMap.keySet().iterator();
        //获取访问该路径所需资源
        while (iterator.hasNext()) {
            String pattern = iterator.next();
            if (pathMatcher.match(pattern, path)) {
                configAttributes.add(configAttributeMap.get(pattern));
            }
        }
        // 未设置操作请求权限,返回空集合
        return configAttributes;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }

}

由于后台资源规则被缓存在了一个Map对象之中,所以当后台资源发生变化时,我们需要清空缓存的数据,然后下次查询时就会被重新加载进来。这里我们需要修改UmsResourceController类,注入DynamicSecurityMetadataSource,当修改后台资源时,需要调用clearDataSource方法来清空缓存的数据。

/**
 * 后台资源管理Controller
 * Created by macro on 2020/2/4.
 */
@Controller
@Api(tags = "UmsResourceController", description = "后台资源管理")
@RequestMapping("/resource")
public class UmsResourceController {

    @Autowired
    private UmsResourceService resourceService;
    @Autowired
    private DynamicSecurityMetadataSource dynamicSecurityMetadataSource;

    @ApiOperation("添加后台资源")
    @RequestMapping(value = "/create", method = RequestMethod.POST)
    @ResponseBody
    public CommonResult create(@RequestBody UmsResource umsResource) {
        int count = resourceService.create(umsResource);
        dynamicSecurityMetadataSource.clearDataSource();
        if (count > 0) {
            return CommonResult.success(count);
        } else {
            return CommonResult.failed();
        }
    }
 }

我们需要实现AccessDecisionManager接口来实现权限校验,对于没有配置资源的接口我们直接允许访问,对于配置了资源的接口,我们把访问所需资源和用户拥有的资源进行比对,如果匹配则允许访问。

/**
 * 动态权限决策管理器,用于判断用户是否有访问权限
 * Created by macro on 2020/2/7.
 */
public class DynamicAccessDecisionManager implements AccessDecisionManager {

    @Override
    public void decide(Authentication authentication, Object object,
                       Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        // 当接口未被配置资源时直接放行
        if (CollUtil.isEmpty(configAttributes)) {
            return;
        }
        Iterator<ConfigAttribute> iterator = configAttributes.iterator();
        while (iterator.hasNext()) {
            ConfigAttribute configAttribute = iterator.next();
            //将访问所需资源或用户拥有资源进行比对
            String needAuthority = configAttribute.getAttribute();
            for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
                if (needAuthority.trim().equals(grantedAuthority.getAuthority())) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("抱歉,您没有访问权限");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }

}

之前在DynamicSecurityMetadataSource中注入了一个DynamicSecurityService对象,它是我自定义的一个动态权限业务接口,其主要用于加载所有的后台资源规则。

/**
 * 动态权限相关业务接口
 * Created by macro on 2020/2/7.
 */
public interface DynamicSecurityService {
    /**
     * 加载资源ANT通配符和资源对应MAP
     */
    Map<String, ConfigAttribute> loadDataSource();
}

需要修改Spring Security的配置类SecurityConfig,当有动态权限业务类时在FilterSecurityInterceptor过滤器前添加我们的动态权限过滤器。

/**
 * SpringSecurity相关配置,仅用于配置SecurityFilterChain
 * Created by macro on 2019/11/5.
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Autowired(required = false)
    private DynamicSecurityService dynamicSecurityService;
    @Autowired(required = false)
    private DynamicSecurityFilter dynamicSecurityFilter;

    @Bean
    SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
                .authorizeRequests();
        //省略若干代码...
        //有动态权限配置时添加动态权限校验过滤器
        if(dynamicSecurityService!=null){
            registry.and().addFilterBefore(dynamicSecurityFilter, FilterSecurityInterceptor.class);
        }
        return httpSecurity.build();
    }

}

在创建动态权限相关对象时,还使用了@ConditionalOnBean这个注解,当没有动态权限业务类时就不会创建动态权限相关对象,实现了有动态权限控制和没有这两种情况的兼容。

/**
 * SpringSecurity通用配置
 * 包括通用Bean、Security通用Bean及动态权限通用Bean
 * Created by macro on 2022/5/20.
 */
@Configuration
public class CommonSecurityConfig {

    @ConditionalOnBean(name = "dynamicSecurityService")
    @Bean
    public DynamicAccessDecisionManager dynamicAccessDecisionManager() {
        return new DynamicAccessDecisionManager();
    }

    @ConditionalOnBean(name = "dynamicSecurityService")
    @Bean
    public DynamicSecurityMetadataSource dynamicSecurityMetadataSource() {
        return new DynamicSecurityMetadataSource();
    }

    @ConditionalOnBean(name = "dynamicSecurityService")
    @Bean
    public DynamicSecurityFilter dynamicSecurityFilter(){
        return new DynamicSecurityFilter();
    }
}

当前端跨域访问没有权限的接口时,会出现跨域问题,只需要在没有权限访问的处理类RestfulAccessDeniedHandler中添加允许跨域访问的响应头即可。

/**
 * 自定义返回结果:没有权限访问时
 * Created by macro on 2018/4/26.
 */
public class RestfulAccessDeniedHandler implements AccessDeniedHandler{
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException e) throws IOException, ServletException {
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Cache-Control","no-cache");
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.parse(CommonResult.forbidden(e.getMessage())));
        response.getWriter().flush();
    }
}

十三. Mysql基础知识

Mysql索引有哪些以及适用场景

MySQL支持多种类型的索引,每种索引类型适用于不同的业务场景。选择合适的索引类型取决于数据库表的特点、查询模式和性能要求。以下是常见的MySQL索引类型及其适用场景:

1. 主键索引(Primary Key Index):
  • 适用场景: 主键索引适用于唯一标识表中每一行数据的情况。通常用于表的主键列,确保每条记录都有唯一的标识。
CREATE TABLE users (
    user_id INT PRIMARY KEY,
    username VARCHAR(50)
);
2. 唯一索引(Unique Index):
  • 适用场景: 唯一索引确保索引列的值是唯一的,但允许存在空值。适用于需要保证数据唯一性的列。
CREATE TABLE email_list (
    email_id INT,
    email_address VARCHAR(100) UNIQUE
);
3. 普通索引(Normal Index):
  • 适用场景: 普通索引是最基本的索引类型,用于加速查询。适用于经常被查询的列。
CREATE TABLE products (
    product_id INT,
    product_name VARCHAR(50),
    INDEX index_name (product_name)
);
4. 全文索引(Full-Text Index):
  • 适用场景: 全文索引用于全文搜索,适用于需要在大文本字段上进行搜索的场景,例如文章内容搜索。
CREATE TABLE articles (
    article_id INT,
    title VARCHAR(100),
    body TEXT,
    FULLTEXT (title, body)
);
5. 空间索引(Spatial Index):
  • 适用场景: 空间索引用于地理空间数据类型的查询,适用于包含地理坐标的表。
CREATE TABLE locations (
    location_id INT,
    name VARCHAR(50),
    location POINT,
    SPATIAL INDEX (location)
);
6. 复合索引(Composite Index):
  • 适用场景: 复合索引由多个列组成,适用于需要优化涉及这些列的查询。适用于多列联合查询。
CREATE TABLE orders (
    order_id INT,
    customer_id INT,
    product_id INT,
    INDEX index_name (customer_id, product_id)
);
7. 前缀索引(Prefix Index):
  • 适用场景: 前缀索引是对索引列的前缀进行索引,适用于需要减小索引大小的场景,提高查询性能。
CREATE TABLE example (
    id INT,
    name VARCHAR(100),
    INDEX index_name (name(30))
);
8. 组合索引:
  • 适用场景: 将多个单列索引合并成一个索引,适用于多个列的多条件查询。
CREATE TABLE employees (
    employee_id INT,
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    INDEX index_name (first_name, last_name)
);

选择合适的索引类型取决于具体的业务需求和查询模式。需要注意的是,过多或不合理使用索引可能导致性能下降,因此在设计数据库时应谨慎考虑。了解查询模式、表的大小和数据分布等因素,有助于更好地选择和优化索引。

2. Mysql的日志机制

MySQL的日志机制是其关键特性之一,它有助于确保数据库的可靠性、恢复性以及事务的一致性。MySQL主要包括以下几种日志:

2.1. 二进制日志(Binary Log):
  • 作用: 二进制日志记录了对数据库的修改操作,包括插入、更新和删除等。这个日志使得在数据库发生故障时,可以通过重新执行二进制日志中的操作来进行恢复。

  • 文件名: 通常以 mysql-bin.xxxxxx 形式存在,每个文件对应一段时间内的日志。

  • 配置: 可以通过配置文件中的 log_bin 来启用或禁用二进制日志。

# 启用二进制日志
log_bin = /path/to/mysql-bin
2.2. 错误日志(Error Log):
  • 作用: 错误日志记录了MySQL服务器运行时的错误和警告信息,有助于诊断问题。

  • 文件名: 通常以 hostname.err 形式存在。

  • 配置: 可以通过配置文件中的 log_error 来指定错误日志的位置。

# 指定错误日志文件位置
log_error = /path/to/error.log
2.3. 查询日志(Query Log):
  • 作用: 查询日志记录了MySQL服务器收到的所有SQL查询语句,可用于审计和分析查询性能。

  • 文件名: 通常以 hostname-slow.log 形式存在,记录慢查询。

  • 配置: 可以通过配置文件中的 slow_query_log 来启用或禁用慢查询日志。

# 启用慢查询日志
slow_query_log = 1
# 指定慢查询日志文件位置
slow_query_log_file = /path/to/slow-query.log
# 阈值,执行时间超过该值的查询会被记录
long_query_time = 1.0
2.4. 更新日志(Update Log):
  • 作用: 更新日志用于记录某些数据更改的信息,包括对InnoDB表中的聚簇索引进行的修改。

  • 文件名: 通常以 hostname.ibd 形式存在。

  • 配置: 可以通过配置文件中的 innodb_log_file_sizeinnodb_log_files_in_group 来配置InnoDB的更新日志。

# 更新日志文件大小
innodb_log_file_size = 50M
# 更新日志文件组数
innodb_log_files_in_group = 2
2.5. 慢查询日志(Slow Query Log):
  • 作用: 慢查询日志记录执行时间超过一定阈值的SQL查询语句,用于识别和优化性能较差的查询。

  • 文件名: 通常以 hostname-slow.log 形式存在。

  • 配置: 可以通过配置文件中的 slow_query_log 来启用或禁用慢查询日志。

# 启用慢查询日志
slow_query_log = 1
# 指定慢查询日志文件位置
slow_query_log_file = /path/to/slow-query.log
# 阈值,执行时间超过该值的查询会被记录
long_query_time = 1.0

这些日志一起构成了MySQL强大的日志机制,为数据库的运维、调优和故障恢复提供了有力的支持。根据具体的需求,可以配置这些日志,以满足特定的业务和管理要求。

3. Mysql事务机制与隔离级别

MySQL的事务机制和隔离级别是数据库管理系统中的重要组成部分,用于确保数据的一致性、隔离性、持久性和原子性。以下是MySQL的事务机制和隔离级别的基本概念:

1. 事务机制:
  • 事务(Transaction): 事务是一系列SQL语句的执行,它们被视为一个原子操作,要么全部执行成功,要么全部失败回滚。MySQL使用事务来确保数据库的完整性和一致性。

  • 事务控制语句:

    • START TRANSACTION;:开始一个新的事务。
    • COMMIT;:提交事务,使得事务中的所有操作生效。
    • ROLLBACK;:回滚事务,撤销事务中的所有操作。
  • 事务的ACID属性:

    • 原子性(Atomicity): 事务是一个原子操作,要么全部执行成功,要么全部失败回滚。
    • 一致性(Consistency): 事务执行后,数据库从一个一致性状态转移到另一个一致性状态。
    • 隔离性(Isolation): 事务的执行应该与其他事务隔离,互不干扰。
    • 持久性(Durability): 一旦事务提交,其结果应该是永久性的。
2. 隔离级别(Isolation Levels):

MySQL支持多个事务隔离级别,用于控制事务之间的可见性和并发性。隔离级别定义了一个事务内的操作对其他事务的影响程度。MySQL支持的隔离级别包括:

  • READ UNCOMMITTED(读未提交): 允许读取其他事务未提交的数据。可能导致脏读、不可重复读和幻读。

  • READ COMMITTED(读已提交): 一个事务只能读取已经提交的其他事务的数据。避免了脏读,但仍可能发生不可重复读和幻读。

  • REPEATABLE READ(可重复读): 保证在同一事务中多次读取相同记录时,结果是一致的。避免了脏读和不可重复读,但仍可能发生幻读。

  • SERIALIZABLE(串行化): 最高的隔离级别,通过强制事务串行执行来避免脏读、不可重复读和幻读。性能较差,一般情况下很少使用。

  • 默认隔离级别: MySQL的默认隔离级别是REPEATABLE READ。

  • 设置隔离级别: 可以通过SET TRANSACTION ISOLATION LEVEL语句设置事务的隔离级别。

-- 设置隔离级别为READ COMMITTED
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

选择适当的隔离级别取决于应用程序的需求和性能要求。较高的隔离级别通常会导致更多的锁和性能开销,因此需要在一致性和性能之间进行权衡。在高并发环境中,了解隔离级别对事务并发性和数据一致性的影响非常重要。

4. SQL的优化策略

SQL优化是数据库性能优化的重要组成部分,它旨在提高查询性能、减少资源消耗以及优化数据库结构。以下是一些常见的SQL优化策略:

1. 使用合适的索引:
  • 创建索引: 在经常用于检索和过滤的列上创建索引,可以显著提高查询性能。
  • 复合索引: 对多个列创建复合索引,以支持联合查询。
  • 避免过多索引: 过多的索引可能导致性能下降,因此需要权衡。
2. 优化查询语句:
  • *避免使用SELECT 只选择实际需要的列,减少数据传输和IO开销。
  • 使用LIMIT: 对于不需要检索全部结果的查询,使用LIMIT限制返回的行数。
  • 避免使用SELECT DISTINCT: 如果可能,尽量避免使用DISTINCT关键字。
3. 合理使用JOIN操作:
  • 选择合适的JOIN类型: 根据查询需求选择INNER JOIN、LEFT JOIN等。
  • 避免笛卡尔积: 确保关联条件的正确性,避免产生不必要的笛卡尔积。
4. 使用合适的数据类型:
  • 选择合适的数据类型: 使用尽可能小的数据类型,减少存储和IO开销。
  • 避免在WHERE子句中对列进行函数操作: 这可能导致索引失效。
5. 定期分析和优化表结构:
  • 规范化数据库: 将数据规范化以减少重复数据,提高数据一致性。
  • 使用合适的存储引擎: 不同的存储引擎适用于不同的使用场景。
6. 使用缓存:
  • 数据库缓存: 使用数据库自身的查询缓存或者外部缓存系统。
  • 应用程序缓存: 缓存查询结果,减少对数据库的访问。
7. 分析执行计划:
  • EXPLAIN语句: 使用EXPLAIN语句查看查询执行计划,了解索引使用情况和查询优化器的决策。
  • 优化执行计划: 根据执行计划进行调整,可能需要调整索引、重写查询等。
8. 定期维护统计信息:
  • 统计信息: 确保数据库收集和维护表的统计信息,以供查询优化器使用。
  • 定期分析性能: 定期监控数据库性能,识别慢查询,进行调整。
9. 分区表:
  • 表分区: 对大表进行分区,可以提高查询性能和维护效率。
10. 使用连接池:
  • 连接池: 使用连接池减少数据库连接的开销,提高性能。

这些优化策略通常需要综合考虑,因为优化不同类型的查询和操作可能需要不同的方法。在进行SQL优化时,重要的是监测和分析实际的数据库性能,并根据需求调整和优化。

5. Mysql的存储引擎

MySQL支持多种存储引擎(Storage Engine),它们是底层负责数据的存储和检索的模块。每个存储引擎都有其特定的优势和适用场景。以下是一些常见的MySQL存储引擎:

1. InnoDB:
  • 特点:

    • 事务支持: 支持ACID事务,适合要求数据完整性和一致性的应用。
    • 行级锁定: 提供行级锁定,减少了读写冲突。
    • 外键支持: 支持外键约束,确保数据的完整性。
    • 崩溃恢复: 支持崩溃恢复,数据库恢复到崩溃前的状态。
  • 适用场景: 适用于事务处理和具有高并发读写需求的应用。

2. MyISAM:
  • 特点:

    • 表级锁定: 使用表级锁定,可能导致并发写入性能较差。
    • 无事务支持: 不支持事务和回滚。
    • 全文索引: 支持全文索引,适用于搜索引擎等应用。
    • 压缩表: 支持表级别的压缩。
  • 适用场景: 适用于读密集型、插入和查询频繁的应用,如数据仓库和日志分析。

3. MEMORY (HEAP):
  • 特点:

    • 内存表: 数据存储在内存中,提供快速的读写访问。
    • 表级锁定: 使用表级锁定,适用于小型临时表。
    • 数据不持久: 数据在数据库关闭时丢失,适用于临时数据存储。
  • 适用场景: 适用于需要快速读写访问的临时表,但不要求数据持久性的场景。

4. ARCHIVE:
  • 特点:

    • 压缩表: 适用于高度压缩的归档数据。
    • 只支持INSERT和SELECT: 不支持更新和删除操作。
    • 表级锁定: 使用表级锁定。
  • 适用场景: 适用于存档数据,以便占用更少的磁盘空间。

5. CSV:
  • 特点:

    • CSV格式: 数据以CSV格式存储。
    • 支持读写: 支持读写操作。
    • 表级锁定: 使用表级锁定。
  • 适用场景: 适用于需要将MySQL数据导出为CSV文件或导入CSV文件的场景。

6. 其他存储引擎:

MySQL还支持其他存储引擎,如Blackhole、Federated、NDB Cluster等。这些存储引擎各自具有特定的优势和适用场景,可以根据实际需求选择合适的存储引擎。

在MySQL中,可以根据表的需求选择不同的存储引擎,甚至在同一数据库中的不同表使用不同的存储引擎。选择存储引擎时需要考虑事务支持、锁定级别、性能特性、数据完整性等因素。

十四. Elasticsearch基础知识

Elasticsearch是一个开源的分布式搜索和分析引擎,建立在Apache Lucene基础上。它提供了强大的全文搜索、分析和数据可视化功能,适用于大规模数据的实时搜索和分析。以下是Elasticsearch的一些基础知识:

1. 核心概念:

  • Document(文档): Elasticsearch存储的基本数据单元,通常用JSON格式表示。
  • Index(索引): 一组相关文档的集合,每个文档都属于一个索引。
  • Type(类型): 在过去的版本中,文档可以根据其类型进行组织,但在最新版本中逐渐被废弃,建议将所有文档放在一个类型中。
  • Node(节点): 单个运行的Elasticsearch实例。
  • Cluster(集群): 由一个或多个节点组成,共同保存整个数据集并提供跨所有节点的搜索和索引功能。

2. RESTful API:

Elasticsearch提供了RESTful API,通过HTTP与其进行交互。常见的API操作包括:

  • Index API: 创建、删除索引。
  • Document API: 增、删、改、查文档。
  • Search API: 执行全文搜索和复杂查询。
  • Aggregations API: 数据聚合和分析。

3. Mapping(映射):

  • Mapping定义了文档的字段和类型。 在创建索引时,可以显式定义映射,也可以使用动态映射,让Elasticsearch自动推断字段类型。

4. 分布式搜索和分析:

  • Elasticsearch是分布式的,能够水平扩展。数据被分成多个分片,每个分片可以在不同的节点上,从而提高搜索和分析的性能。

5. 倒排索引:

  • Elasticsearch基于Lucene的倒排索引(Inverted Index)。这种索引结构允许快速的全文搜索,通过跟踪每个词在哪些文档中出现来实现。

    倒排索引(Inverted Index)是Elasticsearch中一种核心的数据结构,用于支持全文搜索和快速的文本查询。与传统的顺序索引不同,倒排索引存储了词项(terms)到文档的映射关系,而不是文档到词项的关系。这种索引结构使得在大量文档中高效地执行搜索变得可能。

    以下是倒排索引的关键概念:

    5.1. 词项(Terms):
    • 词项是文档中的单词或标记,通常通过分析文本得到。在倒排索引中,每个词项都是索引的一个条目。
    5.2. 文档(Documents):
    • 文档是要被索引的基本单位。在倒排索引中,每个文档都与其包含的词项列表相关联。
    5.3. 倒排列表(Inverted List):
    • 对于每个词项,倒排索引维护一个称为倒排列表的数据结构。倒排列表存储了包含该词项的所有文档的引用或位置。
    5.4. 倒排索引的建立过程:
      1. **文本分析:** 对文档中的文本进行分析,将其拆分为词项。
      2. **建立倒排列表:** 对于每个词项,记录包含该词项的文档和它们的位置信息。
      3. **构建倒排索引:** 将所有的倒排列表组合在一起,形成完整的倒排索引。
    
    5.5. 查询过程:
    • 当执行搜索时,查询的词项会被映射到倒排索引中,检索相关文档的引用,然后通过这些引用找到实际的文档数据。这使得搜索非常高效,因为不需要扫描整个文档集合。
    5.6. 权重和评分:
    • 倒排索引不仅记录了文档的存在,还可以存储关于文档的其他信息,如词频、位置信息等。这些信息用于计算文档的相关性,从而为搜索结果进行排序。

    倒排索引是Elasticsearch实现强大全文搜索的关键之一。它的设计使得搜索和检索过程变得高效,特别适用于需要在大量文档中进行复杂查询的场景,比如全文搜索引擎、日志分析等。

6. 实时性:

  • Elasticsearch提供了实时(near real-time)搜索和分析,文档的索引和搜索几乎是实时的,适用于需要快速响应的场景。

7. 插件和生态系统:

  • Elasticsearch具有丰富的插件生态系统,可以通过插件扩展其功能,例如Kibana用于数据可视化,Logstash用于日志处理。

8. 安全性:

  • Elasticsearch提供安全功能,包括身份验证、授权和传输层加密,以保护数据的安全性。

9. 监控和集群管理:

  • Elasticsearch提供了监控和集群管理工具,例如X-Pack插件,可用于实时监控和管理集群。

Elasticsearch被广泛应用于各种场景,包括日志和事件数据分析、全文搜索、指标分析等。由于其灵活性和强大的搜索能力,它成为处理大规模数据集的理想选择。

10. **es实现分词搜索: **

在Elasticsearch中,分词搜索是通过全文搜索实现的。全文搜索涉及将文本分析为单独的词项,然后在倒排索引中搜索这些词项。以下是使用Elasticsearch实现分词搜索的基本步骤:

10.1. 创建索引:

在Elasticsearch中,首先需要创建一个索引,用于存储文档和构建倒排索引。索引定义了文档的映射和分析器等信息。

PUT /my_index
{
  "mappings": {
    "properties": {
      "content": {
        "type": "text"
      }
    }
  }
}

上述示例中,创建了一个名为my_index的索引,并定义了一个字段content,类型为text,表示这个字段将被用于全文搜索。

10.2. 添加文档:

向索引中添加包含需要搜索的文本的文档。

POST /my_index/_doc/1
{
  "content": "Elasticsearch is a powerful search engine."
}

POST /my_index/_doc/2
{
  "content": "Full-text search is essential for modern applications."
}
10.3. 执行分词搜索:

使用全文搜索查询来执行分词搜索。

GET /my_index/_search
{
  "query": {
    "match": {
      "content": "powerful search"
    }
  }
}

上述查询使用了match查询,它会对搜索词进行分词,并与文档中的词项匹配。这样可以找到包含任意搜索词的文档。

10.4. 自定义分析器:

Elasticsearch提供了各种内置分析器,但也可以根据需要创建自定义分析器。自定义分析器允许您指定分词器、过滤器等详细设置。

PUT /my_index
{
  "settings": {
    "analysis": {
      "analyzer": {
        "custom_analyzer": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": ["lowercase", "my_custom_filter"]
        }
      },
      "filter": {
        "my_custom_filter": {
          "type": "stop",
          "stopwords": ["is", "a", "for"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "content": {
        "type": "text",
        "analyzer": "custom_analyzer"
      }
    }
  }
}

上述示例中,定义了一个名为custom_analyzer的自定义分析器,其中包括标准分词器和一个停用词过滤器。然后将custom_analyzer应用于content字段。

通过这些步骤,您可以使用Elasticsearch实现分词搜索,并根据需要自定义分析器以满足特定的搜索需求。

十五. Java并发编程

Java并发编程是指在多个线程同时执行的情况下,有效地管理和协调线程的执行。以下是Java并发编程的一些基本知识:

1. 线程和进程:
  • 线程(Thread): 线程是程序执行的最小单元,一个进程可以包含多个线程。
  • 进程(Process): 进程是一个独立的执行环境,包含独立的内存空间和系统资源。
2. 创建和运行线程:
  • 继承Thread类:

    class MyThread extends Thread {
        public void run() {
            // 线程执行的代码
        }
    }
  • 实现Runnable接口:

    class MyRunnable implements Runnable {
        public void run() {
            // 线程执行的代码
        }
    }
  • 使用Executor框架:

    ExecutorService executor = Executors.newFixedThreadPool(5);
    executor.execute(new MyRunnable());
3. 线程状态:
  • 新建(New): 线程已经被创建,但尚未启动。
  • 就绪(Runnable): 线程已经启动,等待被调度执行。
  • 运行(Running): 线程正在执行。
  • 阻塞(Blocked): 线程因为某些原因暂时停止执行。
  • 终止(Terminated): 线程执行完成或因异常退出。
4. 同步和锁:
  • 同步方法: 使用synchronized关键字修饰的方法,确保同一时刻只有一个线程可以执行该方法。
  • 同步块: 使用synchronized关键字包裹的代码块,用于实现更灵活的同步。
  • Lock接口: ReentrantLock等实现了Lock接口的类提供更灵活的锁机制。
5. 线程安全性:
  • 原子性: 单个操作是原子的,不会被中断。
  • 可见性: 一个线程对共享变量的修改能立即被其他线程看到。
  • 有序性: 程序执行的顺序按照代码的先后顺序。
6. 并发工具类:
  • CountDownLatch: 用于等待一个或多个线程执行完成。
  • CyclicBarrier: 用于让一组线程相互等待,直到到达某个公共屏障点。
  • Semaphore: 用于控制同时访问的线程个数。
7. 线程池:
  • Executors类: 提供了创建和管理线程池的工厂方法。
  • ThreadPoolExecutor: 真正的线程池实现,可以灵活地配置线程池的参数。
8. 并发集合:
  • ConcurrentHashMap: 线程安全的HashMap实现。
  • CopyOnWriteArrayList: 线程安全的ArrayList实现,适合读多写少的场景。
9. 原子操作类:
  • AtomicInteger、AtomicLong、AtomicReference: 提供了原子性操作,避免了使用synchronized关键字。
10. volatile关键字:
  • volatile修饰变量: 保证变量的可见性,但不保证原子性,适用于标记状态等场景。
11. 线程间通信:
  • wait()、notify()、notifyAll(): 用于线程之间的协调和通信。
  • Lock和Condition: ReentrantLockCondition接口提供了更灵活的线程间通信方式。

以上是Java并发编程的一些基础知识,实现并发程序时需要注意线程安全性、避免死锁和饥饿等问题。了解这些基础知识有助于有效地设计和调试并发程序。

十六. Redis基础知识

Redis(Remote Dictionary Server)是一个开源的、内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。以下是Redis的一些基础知识:

1. 数据结构:

Redis支持多种数据结构,包括:

  • 字符串(String): 最基本的数据类型,可以包含任何数据,比如文本、JSON等。
  • 哈希表(Hash): 类似于Java中的Map,适合存储对象。
  • 列表(List): 类似于Java中的List,支持左右插入和删除操作。
  • 集合(Set): 不重复的元素集合,支持交集、并集、差集等操作。
  • 有序集合(Sorted Set): 类似于集合,但每个元素都关联一个分数,可以按照分数排序。

2. 持久化:

Redis支持两种持久化方式:

  • 快照(RDB): 定期保存数据的快照,将数据写入磁盘。
  • 追加文件(AOF): 记录每个写操作,以保证数据的持久性。

3. 内存模型:

Redis是基于内存的数据库,但也可以通过配置使用磁盘存储数据。内存模型允许快速读写操作,使其在缓存和实时数据处理等场景中表现优异。

4. 单线程模型:

Redis采用单线程模型,即一个请求在同一时间内只能被处理一次。这样可以避免复杂的并发控制,使得Redis的操作变得简单且高效。

5. 发布/订阅(Pub/Sub):

Redis支持发布/订阅模式,允许多个客户端订阅相同的频道,从而在某一客户端发送消息时,所有订阅该频道的客户端都能接收到消息。

6. 事务:

Redis的事务机制类似于数据库事务,支持多个命令的原子性执行。事务可以通过MULTI、EXEC、DISCARD和WATCH等命令来进行控制。

7. 分布式锁:

Redis通过SETNX(SET if Not eXists)命令实现分布式锁,通过给一个不存在的键设置值来实现锁定。

分布式锁是在分布式系统中用于控制对共享资源或临界区域的访问的一种同步机制。在Redis中,常用的实现分布式锁的方式包括使用SETNX(SET if Not eXists)命令和Redlock算法。

7.1. 使用SETNX命令实现分布式锁:

通过SETNX命令,可以尝试将一个值设置到指定的键,只有当该键不存在时才会设置成功。这一特性可以用来实现分布式锁。

加锁(获取锁):
SET lock_key "unique_value" NX PX 30000
  • lock_key是锁的键名,可以是一个唯一的标识。
  • "unique_value"是一个唯一的值,用于标识持有锁的客户端。
  • NX表示只有在键不存在时才设置成功。
  • PX 30000表示设置一个过期时间为30秒的锁,以防止锁永远不会被释放。
解锁(释放锁):
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

上述Lua脚本用于释放锁,通过比较锁的值是否与预期值相同,如果相同则执行删除操作,释放锁。

7.2. 使用Redlock算法实现分布式锁:

Redlock算法是一个由Redis作者提出的分布式锁算法,通过在多个Redis节点上加锁,提高了系统的可靠性。

加锁(获取锁):
# Python示例代码
import redis
from redlock import Redlock

dl = Redlock([{"host": "localhost", "port": 6379, "db": 0}], retry_count=3)

lock = dl.lock("resource_name", 1000)

上述代码使用了redlock-py库,通过多个Redis节点协同工作,确保在网络分区等故障情况下仍能提供可靠的分布式锁。

解锁(释放锁):
# Python示例代码
dl.unlock(lock)

Redlock算法的实现相对复杂,需要确保时钟同步、多节点可用性等条件,因此在使用之前需要仔细了解其适用场景和风险。

7.3. Java实现Redis分布式锁:

在Java中实现分布式锁,可以使用Redis作为分布式锁的后端存储,结合Java的JedisLettuce等Redis客户端库。以下是一个简单的Java实现分布式锁的示例代码:

使用Jedis库:
import redis.clients.jedis.Jedis;

public class DistributedLock {

    private final Jedis jedis;
    private final String lockKey;
    private final String lockValue;
    private final int lockTimeout;

    public DistributedLock(Jedis jedis, String lockKey, String lockValue, int lockTimeout) {
        this.jedis = jedis;
        this.lockKey = lockKey;
        this.lockValue = lockValue;
        this.lockTimeout = lockTimeout;
    }

    public boolean acquireLock() {
        // 使用SET命令尝试获取锁
        String result = jedis.set(lockKey, lockValue, "NX", "PX", lockTimeout);
        return "OK".equals(result);
    }

    public void releaseLock() {
        // 使用Lua脚本释放锁
        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        jedis.eval(luaScript, 1, lockKey, lockValue);
    }

    public static void main(String[] args) {
        // 示例用法
        try (Jedis jedis = new Jedis("localhost", 6379)) {
            DistributedLock distributedLock = new DistributedLock(jedis, "my_lock", "unique_value", 30000);

            if (distributedLock.acquireLock()) {
                try {
                    // 获取锁成功,执行业务逻辑
                    System.out.println("Lock acquired, performing business logic...");
                } finally {
                    // 释放锁
                    distributedLock.releaseLock();
                }
            } else {
                System.out.println("Failed to acquire lock");
            }
        }
    }
}

上述示例中,通过SET命令尝试获取锁,使用NX参数表示只在键不存在时设置成功,使用PX参数设置锁的过期时间。在释放锁时,使用Lua脚本确保解锁操作的原子性。

使用Lettuce库:
import io.lettuce.core.RedisClient;
import io.lettuce.core.api.StatefulRedisConnection;

public class DistributedLock {

    private final RedisClient redisClient;
    private final String lockKey;
    private final String lockValue;
    private final int lockTimeout;

    public DistributedLock(RedisClient redisClient, String lockKey, String lockValue, int lockTimeout) {
        this.redisClient = redisClient;
        this.lockKey = lockKey;
        this.lockValue = lockValue;
        this.lockTimeout = lockTimeout;
    }

    public boolean acquireLock() {
        try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
            // 使用SET命令尝试获取锁
            String result = connection.sync().set(lockKey, lockValue, "NX", "PX", lockTimeout);
            return "OK".equals(result);
        }
    }

    public void releaseLock() {
        try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
            // 使用Lua脚本释放锁
            String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            connection.sync().eval(luaScript, List.of(lockKey), List.of(lockValue));
        }
    }

    public static void main(String[] args) {
        // 示例用法
        RedisClient redisClient = RedisClient.create("redis://localhost:6379");
        DistributedLock distributedLock = new DistributedLock(redisClient, "my_lock", "unique_value", 30000);

        if (distributedLock.acquireLock()) {
            try {
                // 获取锁成功,执行业务逻辑
                System.out.println("Lock acquired, performing business logic...");
            } finally {
                // 释放锁
                distributedLock.releaseLock();
            }
        } else {
            System.out.println("Failed to acquire lock");
        }
    }
}

上述示例使用了Lettuce库,它是一个高性能的Redis客户端。与Jedis类似,通过set命令获取锁,通过Lua脚本释放锁。

请注意,以上示例代码仅为演示目的,实际应用中还需要考虑异常处理、锁

7.4. 注意事项:
  1. 加锁的超时时间: 在使用SETNX命令时,应注意设置合适的超时时间,防止因为某个客户端崩溃而导致锁一直被占用。
  2. 解锁的原子性: 使用Lua脚本可以保证解锁操作的原子性,避免误解锁。
  3. Redlock算法的复杂性: 在选择使用Redlock算法时,需要仔细评估其在特定环境下的可用性和性能开销。

分布式锁的实现需要考虑网络分区、节点故障等复杂情况,因此在选择和使用分布式锁时需要谨慎,根据实际需求选择合适的方案。

8. 管道(Pipeline):

通过使用管道,可以在一个请求中发送多个命令,从而减少网络通信的开销,提高性能。

9. Lua脚本:

Redis支持使用Lua脚本执行一系列命令,这使得可以在服务器端执行一组原子操作,减少了多个命令之间的网络通信开销。

10. 高可用性:

Redis提供了一些高可用性的解决方案,如主从复制、哨兵模式和集群模式,以保证系统的稳定性和可用性。

11. 客户端库和工具:

Redis有丰富的客户端库,支持多种编程语言,如Java、Python、Node.js等。此外,有一些图形化管理工具,如Redis Desktop Manager,可用于方便地管理Redis服务器。

12. Redis分布式缓存概念:

Redis分布式缓存是通过在多个节点上分布存储和管理数据,以提高缓存的容量、性能和可用性。在分布式缓存中,不同的节点可以共同存储和提供对相同缓存数据的访问,从而实现更好的横向扩展性。以下是一些与Redis分布式缓存相关的概念:

1. 分片(Sharding):

分片是指将数据分散存储在多个节点上的过程。在Redis分布式缓存中,可以通过哈希函数或其他分片算法将数据分布到不同的节点上。每个节点负责存储部分数据,从而实现数据的分布式存储。

2. 主从复制:

主从复制是指一个节点(主节点)的数据被复制到其他节点(从节点)上。在Redis中,主节点负责写入操作,从节点负责复制主节点的数据。主从复制可以提高数据的可用性和读取性能。

3. 哨兵模式(Sentinel):

哨兵模式是一种用于监控和管理Redis节点的机制。哨兵负责监控节点的健康状态,当主节点宕机时,会自动选举一个从节点作为新的主节点,确保系统的高可用性。

4. 集群模式(Cluster):

集群模式是一种用于水平扩展的分布式部署方式。在Redis集群模式中,数据被分散存储在多个节点上,每个节点负责一部分数据。集群模式提供了更好的横向扩展性和负载均衡。

5. 数据分片策略:

数据分片策略是指将数据分配到不同节点的规则。常见的策略包括:

  • 哈希分片: 使用哈希函数将数据的键映射到不同的节点。
  • 范围分片: 将数据按照范围划分到不同的节点,比如按照字母顺序划分。
  • 一致性哈希: 通过一致性哈希算法将数据均匀地分布到多个节点上。
6. 缓存击穿、雪崩和穿透:
  • 缓存击穿: 指的是对于某个热点数据,在缓存失效的瞬间,大量请求同时涌入,直接请求数据库,导致数据库负载过高。
  • 缓存雪崩: 指的是在某个时间点,大量缓存失效,导致大量请求直接落到数据库上。
  • 缓存穿透: 指的是恶意或异常请求查询一个不存在的数据,导致每次都要查询数据库,增加数据库负载。
7. 缓存更新策略:
  • 写穿透防护: 当请求查询的数据不存在时,可以设置一个空值或占位符,并设置较短的过期时间,防止频繁查询数据库。
  • 定期刷新: 定期刷新缓存,保持数据的新鲜性。
  • 异步刷新: 在缓存失效的瞬间,异步更新缓存,避免请求直接落到数据库。

这些概念涵盖了在构建和管理Redis分布式缓存时需要考虑的关键方面,以确保系统的性能、可用性和稳定性。

8. Java实现Redis分布式缓存:

在Java中实现Redis分布式缓存,一般可以使用Redis的Java客户端库,如Jedis或Lettuce。以下是一个简单的示例,演示如何使用Jedis实现Redis分布式缓存。

首先,你需要在项目中添加Jedis的依赖,如果使用Maven,可以在pom.xml中添加以下依赖:

<dependencies>
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>3.7.0</version>
    </dependency>
</dependencies>

然后,可以使用以下代码示例来实现Redis分布式缓存:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.exceptions.JedisDataException;
import redis.clients.jedis.exceptions.JedisException;

public class RedisDistributedCache {

    private final Jedis jedis;

    public RedisDistributedCache(String host, int port) {
        this.jedis = new Jedis(host, port);
    }

    public void put(String key, String value, int expirationInSeconds) {
        try {
            jedis.setex(key, expirationInSeconds, value);
        } catch (JedisException e) {
            handleJedisException(e);
        }
    }

    public String get(String key) {
        try {
            return jedis.get(key);
        } catch (JedisException e) {
            handleJedisException(e);
            return null;
        }
    }

    public void delete(String key) {
        try {
            jedis.del(key);
        } catch (JedisException e) {
            handleJedisException(e);
        }
    }

    private void handleJedisException(JedisException e) {
        // 处理Jedis异常,可以根据实际情况进行处理,例如记录日志或抛出自定义异常
        if (e instanceof JedisDataException) {
            System.out.println("JedisDataException: " + e.getMessage());
        } else {
            System.out.println("JedisException: " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        // 示例用法
        RedisDistributedCache cache = new RedisDistributedCache("localhost", 6379);

        // 存入缓存
        cache.put("key1", "value1", 60);

        // 获取缓存
        String result = cache.get("key1");
        if (result != null) {
            System.out.println("Value: " + result);
        } else {
            System.out.println("Key not found");
        }

        // 删除缓存
        cache.delete("key1");
    }
}

上述代码示例创建了一个简单的RedisDistributedCache类,其中包含了常用的缓存操作方法,包括putgetdelete。在实际项目中,你可能需要根据业务需求扩展这些方法,并考虑缓存的失效策略、异常处理等方面。此外,为了提高性能,可以考虑使用连接池管理与Redis的连接。

请注意,上述代码中的异常处理部分是简单示例,实际项目中可能需要更复杂的异常处理逻辑。

这些是Redis的一些基础知识,它是一个强大而灵活的工具,适用于多种应用场景,包括缓存、实时统计、消息队列等。

十七. 计算机网络基础知识

计算机网络是指将多台计算机连接在一起,以便它们可以相互通信和共享资源的系统。以下是一些计算机网络的基础知识:

1. 网络基础概念:

  • 网络: 多台计算机通过通信链路相连接,以便共享资源和信息。
  • 通信: 在网络中,设备之间通过传输数据的方式进行信息交流。

2. 网络的分类:

  • 局域网(LAN): 小范围内的网络,通常在一个建筑或者一个校园内。
  • 城域网(MAN): 比局域网范围更广,通常覆盖一个城市。
  • 广域网(WAN): 跨越大范围的网络,通常覆盖多个城市或者国家。

3. 协议:

  • 协议: 计算机网络中,协议是一种规则或约定,确保通信设备之间的正常通信。常见的协议有TCP/IP、HTTP、FTP等。

4. OSI模型:

  • OSI模型(Open Systems Interconnection): 将网络通信划分为七个层次,每个层次都有特定的功能。包括物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。

5. TCP/IP协议族:

  • TCP/IP: 是互联网所使用的通信协议族,包括传输控制协议(TCP)和因特网协议(IP)等。它是一个四层的模型,包括链路层、网络层、传输层和应用层。

    TCP/IP(Transmission Control Protocol/Internet Protocol)是一组用于在计算机网络上进行通信的协议。

    TCP(传输控制协议):

    想象一下,你正在通过互联网下载一个文件。TCP就像是一个可靠的快递服务。当你发送一个文件请求时,TCP会确保文件的每个部分都被正确地分割、传输、接收,并且没有任何遗漏。如果在传输过程中发生了错误,TCP会重新发送出错的那一部分,直到整个文件都被准确地传送到你的电脑上。

    IP(互联网协议):

    现在,想象一下你给你的朋友寄一封信。IP就像是邮递员,它帮助信件找到正确的地址,确保信件可以正确地从寄信人传递到收信人。在计算机网络中,IP协议负责确定数据的源地址和目标地址,就像邮寄信件时需要写上寄信人和收信人的地址一样。

    TCP/IP的协同工作:

    TCP和IP一起工作,就像是互联网通信的黄金搭档。TCP确保数据在传输过程中的可靠性,而IP负责数据的路由和传递。它们一同构成了一个可靠、高效的通信系统,使得数据可以在世界各地的计算机之间流动,就像你寄信给朋友一样简单。

    综合来说,TCP/IP就像是互联网通信的两位关键角色:一个负责确保数据的完整性和有序传输,另一个负责确定数据的源和目标,帮助数据找到正确的路径。这个协议家族是互联网通信的基石,使得我们能够畅通无阻地在网络上传递信息。

6. IP地址和子网:

  • IP地址: 是网络上的设备的标识符,分为IPv4和IPv6两种版本。
  • 子网: 将一个大的IP地址空间划分成若干个小的子网,有助于更有效地管理IP地址。

7. 域名和DNS:

  • 域名: 是用于标识互联网上计算机和网络服务的人类可读的名称。
  • DNS(Domain Name System): 是将域名映射到IP地址的分布式数据库系统。

8. HTTP和HTTPS:

  • HTTP(Hypertext Transfer Protocol): 是用于在Web浏览器和Web服务器之间传输超文本的协议。
  • HTTPS: 是HTTP的安全版本,通过SSL/TLS协议对数据进行加密传输。

9. 网络安全:

  • 防火墙: 用于控制网络流量,保护内部网络免受未经授权的访问。
  • VPN(Virtual Private Network): 通过加密和隧道技术,在公共网络上建立私人网络。

10. 网络设备:

  • 路由器: 用于在不同网络之间传递数据包。
  • 交换机: 在局域网中连接多个设备,并基于MAC地址进行数据转发。
  • 网关: 连接不同网络体系结构的设备,实现协议转换。

11. 无线网络:

  • Wi-Fi: 一种无线局域网技术,允许设备通过无线信号进行通信。
  • 蓝牙: 一种短距离通信技术,用于连接设备,如手机和耳机。

12. TCP/IP 与HTTP的区别:

TCP/IP(Transmission Control Protocol/Internet Protocol)和HTTP(Hypertext Transfer Protocol)是两个不同的概念,分别表示网络通信的协议族和应用层协议。以下是它们的主要区别:

1. 层级不同:
  • TCP/IP: 是一个包含多个层级的网络通信协议族,包括物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
  • HTTP: 是应用层协议,位于TCP/IP协议栈的最上层,负责在网络中传输超文本(Hypertext)。
2. 功能不同:
  • TCP/IP: 提供了一系列规范,包括数据传输、路由、寻址和网络通信的基本规则。它并没有规定应用层的具体协议,而是提供了一种通用的网络通信基础。
  • HTTP: 是一个应用层协议,用于在Web浏览器和Web服务器之间传输超文本。它规定了客户端和服务器之间的通信格式,定义了请求和响应的格式以及如何进行数据传输。
3. 使用场景不同:
  • TCP/IP: 作为基础的网络协议族,支持多种应用层协议,包括HTTP、FTP、SMTP等。它是网络通信的基础,而不仅仅局限于Web通信。
  • HTTP: 主要用于Web浏览器和Web服务器之间的通信,用于获取和传输超文本内容。它是Web应用的基础协议。
4. 连接性不同:
  • TCP/IP: 是一种连接性协议,它建立可靠的、面向连接的通信。
  • HTTP: 在TCP/IP之上运行,使用TCP协议进行通信,但本身是无连接的,即每个HTTP请求都是独立的,不保留连接状态。
5. 状态保持不同:
  • TCP/IP: 通过TCP协议提供连接的状态保持,确保数据的可靠性。
  • HTTP: 是一种无状态协议,每个请求和响应之间没有关联,服务器不会保留关于客户端的信息。
6. 总结:

TCP/IP是一组通用的网络通信协议,负责提供网络通信的基础规范。HTTP是基于TCP/IP协议的应用层协议,用于实现Web浏览器和Web服务器之间的超文本传输。在Web应用中,TCP/IP提供了底层的连接性和可靠性,而HTTP定义了高层次的数据传输规则。在实际应用中,它们通常是相互配合工作的。

这些基础知识涵盖了计算机网络的关键概念,理解这些概念对于设计、配置和维护计算机网络是至关重要的。网络技术的不断发展也带来了更多的网络协议和应用,因此保持学习和更新对于从事网络相关工作的专业人士是必要的。

十八. RabbitMQ基础知识

RabbitMQ 是一个消息中间件,用于在分布式系统中传递消息。以下是 RabbitMQ 的基础知识:

1. 消息中间件:

消息中间件是一种通过消息传递在应用程序之间进行通信的机制。它允许不同的应用程序在分布式系统中异步地发送和接收消息。

2. RabbitMQ 的角色:

  • Producer(生产者): 负责发送消息的应用程序。
  • Queue(队列): 用于存储消息的地方,生产者将消息发送到队列。
  • Exchange(交换机): 用于将消息路由到一个或多个队列。生产者发送消息到交换机,交换机根据规则将消息路由到队列。
  • Consumer(消费者): 负责接收和处理消息的应用程序。

3. 消息的传递模型:

RabbitMQ 使用 AMQP(Advanced Message Queuing Protocol)作为消息传递的协议。消息从生产者发送到交换机,然后由交换机路由到队列,最后由消费者从队列中接收和处理消息。

4. Exchange 的类型:

RabbitMQ 提供了不同类型的交换机,包括:

  • Direct Exchange(直连交换机): 将消息直接路由到与消息中的路由键相匹配的队列。
  • Fanout Exchange(扇出交换机): 将消息广播到所有绑定的队列。
  • Topic Exchange(主题交换机): 使用通配符匹配路由键,将消息路由到一个或多个队列。
  • Headers Exchange(头交换机): 根据消息的头部属性来进行匹配。

5. 持久化:

RabbitMQ 支持消息的持久化,确保消息在服务器重启后不会丢失。生产者和队列都可以声明为持久的。

6. 消息确认机制:

RabbitMQ 提供了消息确认机制,确保消息成功发送到队列或消费者成功接收和处理。生产者可以通过确认机制了解消息是否成功发送到 RabbitMQ,消费者可以通过确认机制告诉 RabbitMQ 已经成功处理了消息。

7. 消息的优先级:

RabbitMQ 支持为消息设置优先级,优先级高的消息将被优先处理。

8. 死信队列:

死信队列用于存储不能被消费的消息,通常是由于消息过期或者被拒绝。这样可以更灵活地处理无法正常处理的消息。

9. 集群和高可用性:

RabbitMQ 支持集群部署,以提高系统的可靠性和性能。

10. 管理界面:

RabbitMQ 提供了一个用户友好的管理界面,可以通过浏览器访问,用于监控和管理 RabbitMQ 服务器。

RabbitMQ 在分布式系统中起到了重要的角色,提供了可靠的消息传递机制,适用于各种场景,包括微服务架构、任务队列等。


文章作者: jochenxiao
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 jochenxiao !
扫码关注抖音号
  目录