2024年7月

书接上回,最近离职在家了实在无聊,除了看看考研的书,打打dnf手游,也就只能写写代码,结果昨晚挂在某平台的一个技术出售有人下单了,大概业务是需要帮忙辅导一些面试需要用到的SQL。
回想了下,在该平台接单SQL也超过3w元了,考察的也就是那几大类,我准备开一个新的专题,把我遇到的题目做一些示例和总结,欢迎大家指正。

今日主题:连续数据的查询
场景

场景一:需要查询最大连续签到的时长,一般用于toc的场景中
场景二:查询连续人流量超过2w的日期,某游乐场的业务管理系统

思路

这种题目我一般常用的思路就是动转静,连续转不变。
比如场景二,需要找连续的日期,那么就要再找一个连续的变量,让两个变量进行相减或者其他操作,得到不变的数据,然后通过分组或者分区查询即可。

例题

体育馆人流量表

列名 类型
id int
visit_date date
people int

visit_date 是该表中具有唯一值的列。
每日人流量信息被记录在这三列信息中:序号 (id)、日期 (visit_date)、 人流量 (people)
每天只有一行记录,id与日期递增,日期不一定是连续递增。
编写SQL找出每天的人数大于或等于 100 且 日期 连续的三行或更多行记录。返回按 visit_date 升序排列 的结果表
Scheme:

Create table If Not Exists Stadium (id int, visit_date DATE NULL, people int)
Truncate table Stadium
insert into Stadium (id, visit_date, people) values ('1', '2017-01-01', 10)
insert into Stadium (id, visit_date, people) values ('2', '2017-01-02', 109)
insert into Stadium (id, visit_date, people) values ('3', '2017-01-03', 150)
insert into Stadium (id, visit_date, people) values ('4', '2017-01-04', 99)
insert into Stadium (id, visit_date, people) values ('5', '2017-01-05', 145)
insert into Stadium (id, visit_date, people) values ('6', '2017-01-06', 1455)
insert into Stadium (id, visit_date, people) values ('7', '2017-01-07', 199)
insert into Stadium (id, visit_date, people) values ('8', '2017-01-09', 188)
例题解释

体育馆的人流表,要求找出最少三天连续人流量超过100的数据,并且按照
visit_date升序排序。

实战

做题目前先回顾下SqlServer中的一个函数

DATEADD(DAY,2,'2020-03-27');

该函数可以在日期上进行加减,并且可以执行加减的日期部分,DAY表示天数增加。

按照思路,日期连续,那么可以生成一列行号,用日期与行号作差,如果结果相等则表示连续。

解题 生成行号

需要自己创造动态列,比如自己添加行号列
至于为什么不用ID,id也是递增的,我们做完再讲。

SELECT t1.id,t1.visit_date,t1.people FROM (
SELECT t.*, COUNT(1) OVER(partition by DATEADD(DAY, t.rowId * -1,t.visit_date)) AS daysCount FROM
(SELECT *,ROW_NUMBER() Over (ORDER BY id) as rowId FROM Stadium where people >= 100) t
) t1 WHERE t1.daysCount > =3

步骤
1.筛选人流量大于等于100的数据,并且通过分区函数增加行号
2.用日期减去行号得到一个日期,相等则表示日期连续,再次通过分区函数基于得到的日期获取分区数量
3.筛选分区数量大于等于3的就是连续三天或者三天以上人流量大于等于100的数据
4.作为子查询结果处理得到结果。

结果
image

为什么不用id还是需要自己创造行号。因为筛选掉人流量不够的数据后,id与时间的等差结果还是不会变,就会得到错误的数据,本身不满足的数据仍旧会被查询出来。

背景

接过一个外包的项目,该项目使用JPA作为ORM。

项目中有多个entity带有@version字段

当并发高的时候经常报乐观锁错误OptimisticLocingFailureException

原理知识

JPA的@version是通过在SQL语句上做手脚来实现乐观锁的

UPDATE table_name SET updated_column = new_value, version = new_version WHERE id = entity_id AND version = old_version

这个"Compare And Set"操作必须放到数据库层,数据库层能够保证"Compare And Set"的原子性(update语句的原子性)

如果这个"Compare And Set"操作放在应用层,则无法保证原子性,即可能version比较成功了,但等到实际更新的时候,数据库的version已被修改。

这时候就会出现错误修改的情况

需求

解决此类报错,让事务能够正常完成

处理——重试

既然是乐观锁报错,那就是修改冲突了,那就自动重试就好了

案例代码

修改前

@Servicepublic classProductService {

@Autowired
privateProductRepository productRepository;

@Transactional
public voidupdateProductPrice(Long productId, Double newPrice) {
Product product
= productRepository.findById(productId).orElseThrow(()->new RuntimeException("Product not found")
product.setPrice(newPrice);
productRepository.save(product);
}
}

修改后

增加一个withRetry的方法,对于需要保证修改成功的地方(比如用户在UI页面上的操作),可以调用此方法。

@Servicepublic classProductService {

@Autowired
privateProductRepository productRepository;public voidupdateProductPriceWithRetry(Long productId, Double newPrice) {boolean updated = false;//一直重试直到成功 while(!updated) {try{
updateProductPrice(productId, newPrice);
updated
= true;
}
catch(OpitimisticLockingFailureException e) {
           System.out.println(
"updateProductPrice lock error, retrying...")
}
}
   }

@Transactional
public voidupdateProductPrice(Long productId, Double newPrice) {
Product product
= productRepository.findById(productId).orElseThrow(()->new RuntimeException("Product not found")
product.setPrice(newPrice);
productRepository.save(product);
}
}

依赖乐观锁带来的问题——高并发带来高冲突

上面的重试能够解决乐观锁报错,并让业务操作能够正常完成。但是却加重了数据库的负担。

另外乐观锁也有自己的问题:

业务层将事务修改直接提交给数据库,让乐观锁机制保障数据一致性

这时候并发越高,修改的冲突就更多,就有更多的无效提交,数据库压力就越大

高冲突的应对方式——引入悲观锁

解决高冲突的方式,就是在业务层引入悲观锁。

在业务操作之前,先获得锁。

一方面减少提交到数据库的并发事务量,另一方面也能减少业务层的CPU开销(获得锁后才执行业务代码)

@Servicepublic classProductService {

@Autowired
privateProductRepository productRepository;public voidsomeComplicateOperationWithLock(Object params) {//该业务涉及到的几个对象修改,需要获得该对象的锁//key=类前缀+对象id List<String> keys =Arrays.asList(....);//RedisLockUtil为分布式锁,可自行封装(可基于redisson实现)//获得锁之后才开始执行任务代码,然后在任务执行结束释放锁 RedisLockUtil.runWithLock(keys, retryTime, retryLockTimeout, ()->someComplicateOperation(params)}):

}


@Transactional
public voidsomeComplicateOperation(Object params) {
.....
}
}

遇到的坑

正常在获得锁之后,需要重新加载最新的数据,这样修改的时候才不会冲突。(前一个锁获得者可能修改了数据)

但是,JPA有持久化上下文,有一层缓存。如果在获得锁之前就将对象捞了出来,等获得锁之后重新捞还会得到缓存内的数据,而非数据库最新数据。

这样的话,即使用了悲观锁,事务提交的时候还是会出现冲突。

案例:

@Servicepublic classProductService {

@Autowired
privateProductRepository productRepository;public voidsomeComplicateOperationWithLock(Object params) {
//获得锁之前先查询了一次,此次查询数据将缓存在持久化上下文中
String productId
=xxxx;
Product product
= productRepository.findById(productId).orElseThrow(()->throw new RuntimeException("Product not found"));//该业务涉及到的几个对象修改,需要获得该对象的锁//key=类前缀+对象id List<String> keys =Arrays.asList(....);//RedisLockUtil为分布式锁,可自行封装//获得锁之后才开始执行任务代码,然后在任务执行结束释放锁 RedisLockUtil.runWithLock(keys, retryTime, retryLockTimeout, ()->someComplicateOperation(params)}):

}


@Transactional
public voidsomeComplicateOperation(Object params) {
.....
//取到缓存内的旧数据 Product product = productRepository.findById(productId).orElseThrow(()->throw new RuntimeException("Product not found"));
....
}
}

应对方式——refresh

在悲观锁范围内,首次加载entity数据的时候,使用refresh方法,强制从DB捞取最新数据。

@Servicepublic classProductService {

@Autowired
privateProductRepository productRepository;public voidsomeComplicateOperationWithLock(Object params) {//获得锁之前先查询了一次,此次查询数据将缓存在持久化上下文中 String productId =xxxx;
Product product
= productRepository.findById(productId).orElseThrow(()->throw new RuntimeException("Product not found"));//该业务涉及到的几个对象修改,需要获得该对象的锁//key=类前缀+对象id List<String> keys =Arrays.asList(....);//RedisLockUtil为分布式锁,可自行封装//获得锁之后才开始执行任务代码,然后在任务执行结束释放锁 RedisLockUtil.runWithLock(keys, retryTime, retryLockTimeout, ()->someComplicateOperation(params)}):

}


@Transactional
public voidsomeComplicateOperation(Object params) {
.....
//取到缓存内的旧数据 Product product = productRepository.findById(productId).orElseThrow(()->throw new RuntimeException("Product not found"));//使用refresh方法,强制从数据库捞取最新数据,并更新到持久化上下文中 EntityManager entityManager = SpringUtil.getBean(EntityManager.class)
product
=entityManager.refresh(product);
....
}
}

总结

此项目采用乐观锁+悲观锁混合方式,用悲观锁限制并发修改,用乐观锁做最基本的一致性保护。

关于一致性保护

对于一些简单的应用,写并发不高,事务+乐观锁就足够了

  • entity里面加一个@version字段
  • 业务方法加上@Transactional

这样代码最简单。

只有当写并发高的时候,或根据业务推断可能出现高并发写操作的时候,才需考虑引入悲观锁机制。

(代码越复杂越容易出问题,越难维护)

项目场景:

这两天不是一直在搞简化配置、使用公共配置、我的服务可以通过网关访问这几个任务嘛,也是不断地踩坑补知识才总算把这几个任务都搞好了,下面就是记录过程中遇到的问题。


使用公共配置

因为发现项目使用的配置文件过多,有application、application-test.yml、bootstrap.yml、远程nacos配置,我想不能搞得简单些就尽量把所有配置都放到线上,本地只是做区分么,然后自己恶补了下application和bootstrap区别

application和bootstrap区别和优先级

顺序:
bootstrap.properties > bootstrap.yml > application.properties > application.yml
优先级:
properties>yml
文件位置优先级:
src里的config文件夹>>根目录>>resource里的config文件夹>>resource下的
小结:
同样的yml,bootstrap比application优先级高,是由父上下文加载的,而application是由子加载
bootstrap一般配置的是引导配置,连接的是spring cloud配置中心,默认本地不能覆盖远程配置,远程配置一些加密的信息


使用公共配置common.yml

直接上配置代码,注意



参数里面

chixxxxx:
  nacos:
    server-addr: 123.123.000.000:8848
spring:
  profiles:
    active: test
  application:
    name: chixxxxx-gateway
    group: chixxxxx_GROUP
  cloud:
    nacos:
      discovery:
        group: ${spring.application.group}
        namespace: ${spring.profiles.active}
        server-addr: ${chixxxxx.nacos.server-addr}
      config:
        group: ${spring.application.group}
        namespace: ${spring.profiles.active}
        server-addr: ${chixxxxx.nacos.server-addr}
        prefix: ${spring.application.name}
        shared-configs:
          - data-id: common.yml
            group: ${spring.application.group}
        extension-configs:
          - data-id: chixxxxx-gateway-test.yml
            group: ${spring.application.group}


配置网关服务

我之前上家公司是配过网关的,其实就是指定服务+断言路径就行了,但是这边还是有点区别,首先是nginx.conf那边,先做了个匹配路径转发到网关的一个操作,如下

location /api/backend/ {
                proxy_pass http://localhost:8001;
            }

提前转发到网关,然后网关配置

- id: platform后台接口
        uri: lb://chixxxx-platform
        order: 1000
        predicates:
          - Path=/api/backend/**
        filters:
          - StripPrefix=0
          - name: RequestRateLimiter
            args:
              key-resolver: '#{@hostAddrKeyResolver}'
              redis-rate-limiter:
                replenishRate: 20
                burstCapacity: 50

这里面尤其要注意的是
StripPrefix
这个参数,之前没有太注意,还弄出个404,看了些讲解才知道这个就是去掉我一开始请求的路径上的字符,如:StripPrefix=1,那么请求到服务就是backend/user/detail?id=1;StripPrefix=2,user/detail?id=1,这个数字代表去掉几个/分割的字符串。

请求服务转发失败

这个又是什么呢,我照样子请求url,但是返回报500,然后我查看了下日志

ERROR 1399157 [reactor-http-epoll-4] org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler [bfc53f9d-5959]  500 Server Error for HTTP GET "/api/backend/appVersion/detail?id=12"

就是服务器错误,我再看目标服务器上的日志,没有新增日志,然后就因为报错信息有限,我前前后后花了靠一天时间最后通过“同义千问”找到灵感-好好对一下服务状态是否正常,打开:服务列表-服务详情,看到健康状态是ture啊,那为啥报500,再看ip地址,这个ip写的是:172.17.0.5这个好像docker或者内网地址啊,一开始怀疑内网,但是想想不会啊,我的服务器在外头的,然后再去目标服务器执行:
docker inspect xxx
,看到就是这个,哦~原来把docker分配的ip注册到nacos上了,我应该是要把服务器ip注册上去,通过千问需要设置

spring:
  cloud:
    nacos:
      discovery:
        ip: 123.123.123.123

然后重新发下,再去详情看下,ip变了,还有记得把端口号开放!最后再通过postman请求下就可以通过网关地址访问到了


小结

最近这个礼拜做的这几个任务都是我自发去做的,因为看到了配置比较乱现在用户数比较少可以折腾,避免后期人多了再折腾损耗比较大,同时对gateway这块知识查漏补缺也有了相应的提高。

简介

mybatis的启动入口一般有两个,在结合spring框架后由spring整合包下的SqlSessionFactoryBean启动

如果没有整合spring,则有XMLConfigBuilder启动

这两个启动入口都会初始化Configuration对象,该对象是mybatis配置文件的对象形式,我们实现的mybatis拦截器在此时被装载到configuration中

启动过程

一、SqlSessionFactoryBean在类加载完成后调用后置方法,执行buildSqlSessionFactory();该方法中初始化配置类configuration,在配置类初始化完成后,调用SqlSessionFactoryBuilder类的builder方法

二、SqlSessionFactoryBuilder.builder()方法中new DefaultSqlSessionFactory对象,使用配置类作为入参

三、SqlSession对象;

在MyBatis框架中,
SqlSession
对象是核心的数据库会话管理器,它提供了执行持久化操作的方法和管理持久化操作的生命周期。具体来说,
SqlSession
对象的作用包括以下几个方面:

  1. 数据库会话管理:
    SqlSession
    负责管理与数据库的连接,它是数据库操作的主要入口。在应用程序中,通过
    SqlSession
    对象可以执行SQL语句,提交事务,关闭连接等操作。

  2. SQL执行操作:
    SqlSession
    提供了执行SQL语句的方法,可以执行查询(select)、更新(update)、插入(insert)、删除(delete)等数据库操作。

  3. 事务管理: 在MyBatis中,
    SqlSession
    可以控制事务的生命周期。可以通过
    SqlSession
    开启事务、提交事务或回滚事务,确保数据操作的一致性和完整性。

  4. Mapper接口绑定: MyBatis通过
    SqlSession
    为Mapper接口提供了实现类(代理类)。Mapper接口定义了数据库操作的方法,而
    SqlSession
    通过加载Mapper接口和XML映射文件,将Mapper接口与实际的SQL语句绑定在一起,从而可以执行Mapper接口中定义的方法对数据库进行操作。

  5. 资源管理:
    SqlSession
    在创建时会获取到数据库连接,在关闭时释放连接,确保资源得到合理的管理和释放,避免资源泄漏。

  6. 可以将sqlsession看成是对connection的升级

四、创建sqlsession对象,mybatis提供了从datasource和connection两种方式创建sqlsession的方法;这两个方式的区别在于是否需要手动管理数据库连接和事务;在获得事务工厂后,通过事务工厂和执行器Executor类型通过配置类的方法创建executor对象;

  • 创建executor对象;executor对象封装了query和update等方法,事务操作和缓存也在该对象中管理;
  • 关于拦截器,mybaits的拦截器可以对executor、statementhandler、pameterhandler和resultsethandler的方法进行拦截;在executor的创建过程中,我们自定义的拦截器会对executor增强;具体来说,就是我们定义的拦截器链会对executor对象轮流执行plugin方法,在plugin方法中执行Plugin类的wrap方法,在wrap方法中解析拦截器的配置,生成executor的代理对象(jdk动态代理)。在执行executor中定义的方法时,根据该方法是否被拦截决定由代理对象执行或者executor对象执行

五、生成statementhandler;statementhandler对象在executor对象执行方法时被创建,statementhandler完成参数绑定等工作,并执行sql语句

image

在C#中,集合是用于存储和操作一组数据项的数据结构。这些集合通常位于
System.Collections

System.Collections.Generic
命名空间中。下面我将概述C#中几种常用的集合类型及其特点:

1.
System.Collections
命名空间中的集合

这个命名空间中的集合类型不支持泛型,因此在编译时不检查类型安全性。这意味着在运行时可能会遇到类型转换错误。

  • ArrayList


    • 动态数组,可以存储任意类型的对象。
    • 缺乏类型安全性。
    • 提供了
      Add
      ,
      Insert
      ,
      Remove
      ,
      Sort
      ,
      Reverse
      等方法。
    • 示例:
      ArrayList list = new ArrayList();
      list.Add(1);
      list.Add("two");
      
  • Hashtable


    • 键值对集合,键必须是
      object
      类型。
    • 键必须唯一。
    • 缺乏类型安全性。
    • 提供了
      Add
      ,
      Remove
      ,
      ContainsKey
      ,
      ContainsValue
      等方法。
    • 示例:
      Hashtable table = new Hashtable();
      table.Add("key", "value");
      
  • Stack


    • 后进先出 (LIFO) 集合。
    • 支持
      Push

      Pop
      方法。
    • 示例:
      Stack<object> stack = new Stack<object>();
      stack.Push(1);
      stack.Push("two");
      object top = stack.Pop(); // "two"
      
  • Queue


    • 先进先出 (FIFO) 集合。
    • 支持
      Enqueue

      Dequeue
      方法。
    • 示例:
      Queue<object> queue = new Queue<object>();
      queue.Enqueue(1);
      queue.Enqueue("two");
      object front = queue.Dequeue(); // 1
      

2.
System.Collections.Generic
命名空间中的集合

这个命名空间中的集合类型支持泛型,因此可以确保类型安全性。

  • List


    • 动态数组,可以存储特定类型的对象。
    • 提供了
      Add
      ,
      Insert
      ,
      Remove
      ,
      Sort
      ,
      Reverse
      等方法。
    • 示例:
      List<int> numbers = new List<int>();
      numbers.Add(1);
      numbers.Add(2);
      
  • HashSet


    • 用于存储唯一元素的集合。
    • 提供了
      Add
      ,
      Remove
      ,
      Contains
      等方法。
    • 示例:
      HashSet<int> uniqueNumbers = new HashSet<int>();
      uniqueNumbers.Add(1);
      uniqueNumbers.Add(2);
      uniqueNumbers.Add(1);
      
      foreach(var item in uniqueNumbers) {
          Console.WriteLine(item);
      }
      
      /* 输出结果
          1
          2
      */
      
  • Dictionary<TKey, TValue>


    • 键值对集合,键和值都可以是特定类型。
    • 键必须唯一。
    • 提供了
      Add
      ,
      Remove
      ,
      TryGetValue
      ,
      ContainsKey
      等方法。
    • 示例:
      Dictionary<string, int> scores = new Dictionary<string, int>();
      scores.Add("Alice", 90);
      scores.Add("Bob", 80);
      
  • SortedDictionary<TKey, TValue>


    • 键值对集合,按照键排序。
    • 键必须唯一。
    • 提供了
      Add
      ,
      Remove
      ,
      TryGetValue
      ,
      ContainsKey
      等方法。
    • 示例:
      SortedDictionary<string, int> sortedScores = new SortedDictionary<string, int>();
      sortedScores.Add("Alice", 90);
      sortedScores.Add("Bob", 80);
      
  • Queue


    • 泛型的先进先出 (FIFO) 集合。
    • 支持
      Enqueue

      Dequeue
      方法。
    • 示例:
      Queue<int> queue = new Queue<int>();
      queue.Enqueue(1);
      queue.Enqueue(2);
      int front = queue.Dequeue(); // 1
      
  • Stack


    • 泛型的后进先出 (LIFO) 集合。
    • 支持
      Push

      Pop
      方法。
    • 示例:
      Stack<int> stack = new Stack<int>();
      stack.Push(1);
      stack.Push(2);
      int top = stack.Pop(); // 2
      
  • LinkedList


    • 双向链表,适合频繁插入和删除的场景。
    • 支持
      AddFirst
      ,
      AddLast
      ,
      RemoveFirst
      ,
      RemoveLast
      等方法。
    • 示例:
      LinkedList<int> list = new LinkedList<int>();
      list.AddLast(1);
      list.AddLast(2);