尚硅谷Java大厂面试题第4季笔记
基础篇
i++值
服务可用性能达到几个9
- 就是一年时间内服务不可用的时间最多多少,一一年为单位计算百分比。
- 阿里等大厂宣称是4个9,即99.99%,也就是一年不可用时间最多,52.6分钟。
Arrays.asList()把数组转换成集合大坑
- Arrays.asList()转的List对象是Arrays的内部类,没有重写add和remove方法,AbstractList里的add和remove方法是抛出异常的。
遍历集合时remove或add操作注意事项
- 遍历集合时中间进行添加或删除数据要使用迭代器进行删除
hashcode冲突
- 属于Object类的方法
- 类似于号码牌冲突了,两个不同的人挂相同的号码牌。
- 通过HashSet来测试hash冲突
- 也可以自定义一个类,然后重写hashcode方法,使其对10取余,返回余数,这样就很容易发生hash冲突。
Integer包装类
- Integer包装类与int 数值进行 == 比较会进行自动拆箱,所以比较的是值
- Integer创建的对象不在-128到127之间,用== 比较的是堆对象的地址
- 包装类比较要用equals方法。
BigDecimal的大坑
- 金额等值比较用compareTo方法
- 构造BigDecimal要用字符串参数或valueOf方法避免精度风险。
- 浮点数据等值判断也是用compareTo方法,基本数据类型不能使用==进行比较。
工具类
package com.atguigu.interview2.utils;
import java.math.BigDecimal;
/**用于高精确处理常用的数学运算
* @auther zzyy
* @create 2024-05-02 17:21
*/
public class ArithmeticUtils
{
//默认除法运算精度
private static final int DEF_DIV_SCALE = 10;
/**
* 提供精确的加法运算
*
* @param v1 被加数
* @param v2 加数
* @return 两个参数的和
*/
public static double add(double v1, double v2) {
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.add(b2).doubleValue();
}
/**
* 提供精确的加法运算
*
* @param v1 被加数
* @param v2 加数
* @return 两个参数的和
*/
public static BigDecimal add(String v1, String v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.add(b2);
}
/**
* 提供精确的加法运算
*
* @param v1 被加数
* @param v2 加数
* @param scale 保留scale 位小数
* @return 两个参数的和
*/
public static String add(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.add(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
}
/**
* 提供精确的减法运算
*
* @param v1 被减数
* @param v2 减数
* @return 两个参数的差
*/
public static double sub(double v1, double v2) {
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.subtract(b2).doubleValue();
}
/**
* 提供精确的减法运算。
*
* @param v1 被减数
* @param v2 减数
* @return 两个参数的差
*/
public static BigDecimal sub(String v1, String v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.subtract(b2);
}
/**
* 提供精确的减法运算
*
* @param v1 被减数
* @param v2 减数
* @param scale 保留scale 位小数
* @return 两个参数的差
*/
public static String sub(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.subtract(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
}
/**
* 提供精确的乘法运算
*
* @param v1 被乘数
* @param v2 乘数
* @return 两个参数的积
*/
public static double mul(double v1, double v2) {
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.multiply(b2).doubleValue();
}
/**
* 提供精确的乘法运算
*
* @param v1 被乘数
* @param v2 乘数
* @return 两个参数的积
*/
public static BigDecimal mul(String v1, String v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.multiply(b2);
}
/**
* 提供精确的乘法运算
*
* @param v1 被乘数
* @param v2 乘数
* @param scale 保留scale 位小数
* @return 两个参数的积
*/
public static double mul(double v1, double v2, int scale) {
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return round(b1.multiply(b2).doubleValue(), scale);
}
/**
* 提供精确的乘法运算
*
* @param v1 被乘数
* @param v2 乘数
* @param scale 保留scale 位小数
* @return 两个参数的积
*/
public static String mul(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.multiply(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
}
/**
* 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
* 小数点以后10位,以后的数字四舍五入
*
* @param v1 被除数
* @param v2 除数
* @return 两个参数的商
*/
public static double div(double v1, double v2) {
return div(v1, v2, DEF_DIV_SCALE);
}
/**
* 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
* 定精度,以后的数字四舍五入
*
* @param v1 被除数
* @param v2 除数
* @param scale 表示表示需要精确到小数点以后几位。
* @return 两个参数的商
*/
public static double div(double v1, double v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}
/**
* 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
* 定精度,以后的数字四舍五入
*
* @param v1 被除数
* @param v2 除数
* @param scale 表示需要精确到小数点以后几位
* @return 两个参数的商
*/
public static String div(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v1);
return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).toString();
}
/**
* 提供精确的小数位四舍五入处理
*
* @param v 需要四舍五入的数字
* @param scale 小数点后保留几位
* @return 四舍五入后的结果
*/
public static double round(double v, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
BigDecimal b = new BigDecimal(Double.toString(v));
return b.setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}
/**
* 提供精确的小数位四舍五入处理
*
* @param v 需要四舍五入的数字
* @param scale 小数点后保留几位
* @return 四舍五入后的结果
*/
public static String round(String v, int scale) {
if (scale < 0)
{
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
BigDecimal b = new BigDecimal(v);
return b.setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
}
/**
* 取余数
*
* @param v1 被除数
* @param v2 除数
* @param scale 小数点后保留几位
* @return 余数
*/
public static String remainder(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.remainder(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
}
/**
* 取余数 BigDecimal
*
* @param v1 被除数
* @param v2 除数
* @param scale 小数点后保留几位
* @return 余数
*/
public static BigDecimal remainder(BigDecimal v1, BigDecimal v2, int scale) {
if (scale < 0)
{
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
return v1.remainder(v2).setScale(scale, BigDecimal.ROUND_HALF_UP);
}
/**
* 比较大小
*
* @param v1 被比较数
* @param v2 比较数
* @return 如果v1 大于v2 则 返回true 否则false
*/
public static boolean compare(String v1, String v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
int bj = b1.compareTo(b2);
boolean res;
if (bj > 0)
res = true;
else
res = false;
return res;
}
}
如何去除list中的重复元素?
- 暴力破解,双重遍历循环判断。
- HashSet去重,需要保留顺序就用LinkedHashSet
- Stream流式去重,distinct()
- 双指针前后遍历比较下标,下标不同表示有重复元素
==和equals对比
- 主要注意的是比较的是基本类型还是引用类型,基本类型比较的是值,引用类型比较的是地址。
- 字符串比较还要注意都是new出来的还是字符串常量池里的。 常量池里 == 会相同,new出来的还是比较地址就不同。
方法调用时值的传递——传值还是传引用
基本类型和String是值传递
-
对象是引用传递
image.png
深拷贝与浅拷贝
- 浅拷贝只是复制引用指针
- 深拷贝可以避免共享引用间修改值会互相影响
- 深拷贝三线程安全的。
- clone()方法是浅拷贝,且拷贝对象必须实现Cloneable接口,否则抛异常。如果要实现深拷贝就要重写clone()方法。没意义。
IDEA篇
- ArrayList每次扩容都是原值的一半。
JUnit篇
-
添加测试覆盖率参数
-Djava.io.tmpdir=D:\devSoft\IDEA2023.2\temp2024
-
自动测试框架
- 切面+反射+注解
@MockBean用于测试跨服务跨部门接口时的模拟数据。
@SpyBean用于有设置模拟规则则走规则,没有走真实调用,即半真半假的环境下调用。
自动测试框架
mock
自动测试类
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @auther zzyy
* @create 2024-05-05 10:05
*
* Junit+反射+注解浅谈自动测试框架设计
*
* 需求
* 1 我们自定义注解@AtguiguTest
* 2 将注解AtguiguTest加入需要测试的方法
* 3 类AutoTestClient通过反射检查,哪些方法头上标注了@AtguiguTest注解会自动进行单元测试
*/
@Slf4j
public class AutoTestClient
{
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException
{
CalcHelpDemo calcHelpDemo = new CalcHelpDemo();
int para1 = 10;
int para2 = 0;
Method[] methods = calcHelpDemo.getClass().getMethods();
AtomicInteger bugCount = new AtomicInteger();
// 要写入的文件路径(如果文件不存在,会创建该文件)
String filePath = "BugReport"+ (DateUtil.format(new Date(), "yyyyMMddHHmmss"))+".txt";
for (int i = 0; i < methods.length; i++)
{
if (methods[i].isAnnotationPresent(AtguiguTest.class))
{
try
{
methods[i].invoke(calcHelpDemo,para1,para2);
} catch (Exception e) {
bugCount.getAndIncrement();
log.info("异常名称:{},异常原因:{}",e.getCause().getClass().getSimpleName(),e.getCause().getMessage());
FileUtil.writeString(methods[i].getName()+"\t"+"出现了异常"+"\n", filePath, "UTF-8");
FileUtil.appendString("异常名称:"+e.getCause().getClass().getSimpleName()+"\n", filePath, "UTF-8");
FileUtil.appendString("异常原因:"+e.getCause().getMessage()+"\n", filePath, "UTF-8");
}finally {
FileUtil.appendString("异常数:"+bugCount.get()+"\n", filePath, "UTF-8");
}
}
}
}
}
/**
* 在Hutool工具包中,使用FileUtil类进行文件操作时,通常不需要显式地“关闭”文件。
* 这是因为Hutool在内部处理文件I/O时,已经考虑了资源的自动管理和释放。
*
* 具体来说,当你使用FileUtil的静态方法(如writeString、appendString、readUtf8String等)时,
* 这些方法会在执行完毕后自动关闭与文件相关的流和资源。因此,你不需要(也不能)
* 调用类似于close这样的方法来关闭文件。
*
* 这是因为这些静态方法通常使用Java的try-with-resources语句或其他类似的机制来确保资源在
* 不再需要时得到释放。try-with-resources是Java 7及更高版本中引入的一个特性,
* 它允许你在try语句块结束时自动关闭实现了AutoCloseable或Closeable接口的资源。
*
* 所以,当你使用Hutool的FileUtil类进行文件操作时,你可以放心地编写代码,
* 而无需担心资源泄露或忘记关闭文件的问题。Hutool已经为你处理了这些细节。
*/
JUC篇
ThreadLocal
- 要保证线程安全,要嘛加锁访问,要嘛线程人手一份不争抢资源。
- TransmittableThreadLocal是阿里用于父子线程间的数据传递同步,特别是线程池使用时,如果用InheritableThreadLocal为了解决线程池中线程因为复用而不能取得外部线程数据变更的问题。
线程池
- 线程池优雅关闭:先shutdown(),再awaitTermination()一段时间后调用shutdownNow()。也就是先拒绝任务继续进来,再等待一段时间后执行真正的线程池关闭。类似于商场打烊,先拒绝客户进来,处理掉还在商场的人流,最后关闭商场。
优雅关闭线程池代码示例
/**
* 参考官网使用,最后的终结,优雅关停,but有点费事
* @param threadPool
*/
public static void finalOK_shutdownAndAwaitTermination(ExecutorService threadPool)
{
if (threadPool != null && !threadPool.isShutdown())
{
threadPool.shutdown();
try
{
if (!threadPool.awaitTermination(120, TimeUnit.SECONDS))
{
threadPool.shutdownNow();
if (!threadPool.awaitTermination(120, TimeUnit.SECONDS))
{
System.out.println("Pool did not terminate");
}
}
} catch (InterruptedException ie) {
threadPool.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
优雅处理线程池异常
- 重写线程池的afterExecute方法处理异常。
package com.d.juc;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.*;
@Slf4j
public class MySelfThreadPoolDemo
{
public static void main(String[] args)
{
ExecutorService threadPool = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(),
Runtime.getRuntime().availableProcessors() * 2,
1L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100)) {
@Override
protected void afterExecute(Runnable runnable, Throwable throwable)
{
//execute运行
if (throwable != null)
{
log.error(throwable.getMessage(), throwable);
}
//submit运行
if (throwable == null && runnable instanceof Future<?>)
{
try
{
Future<?> future = (Future<?>) runnable;
if (future.isDone())
{
future.get();
}
} catch (CancellationException ce){
throwable = ce;
ce.printStackTrace();
} catch (ExecutionException ee){
throwable = ee.getCause();
ee.printStackTrace();
} catch (InterruptedException ie){
ie.printStackTrace();
Thread.currentThread().interrupt();
}
}
}
};
try
{
//业务逻辑编写,将需求提交给池中工作线程处理
}catch (Exception e){
e.printStackTrace();
}finally {
finalOK_shutdownAndAwaitTermination(threadPool);
}
}
/**
* 参考官网使用,线程池优雅关停,可以自己写个工具类单独调用
* @param threadPool
*/
public static void finalOK_shutdownAndAwaitTermination(ExecutorService threadPool)
{
if (threadPool != null && !threadPool.isShutdown())
{
threadPool.shutdown();
try
{
if (!threadPool.awaitTermination(120, TimeUnit.SECONDS))
{
threadPool.shutdownNow();
if (!threadPool.awaitTermination(120, TimeUnit.SECONDS))
{
System.out.println("Pool did not terminate");
}
}
} catch (InterruptedException ie) {
threadPool.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
}
一次性下发100W优惠卷/短信/二维码,兼顾线程池参数可配置
- 核心思路就是线程池多线程并行处理,具体业务细节探讨,比如如何保证不丢包,如何做到通用性以适应其他类型的批处理。
代码实践
配置类,配合apollo或nacos进行线程池核心参数的动态配置。
package com.c.config;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
public class ThreadPoolConfig
{
/*
@Value("${thread.pool.corePoolSize}")
private String corePoolSize;
@Value("${thread.pool.maxPoolSize}")
private String maxPoolSize;
@Value("${thread.pool.queueCapacity}")
private String queueCapacity;
@Value("${thread.pool.keepAliveSeconds}")
private String keepAliveSeconds;
*/
//线程池配置
@Resource
private ThreadPoolProperties threadPoolProperties;
@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutor()
{
ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();
// 核心线程池大小
threadPool.setCorePoolSize(threadPoolProperties.getCorePoolSize());
// 最大可创建的线程数
threadPool.setMaxPoolSize(threadPoolProperties.getMaxPoolSize());
// 等待队列最大长度
threadPool.setQueueCapacity(threadPoolProperties.getQueueCapacity());
// 线程池维护线程所允许的空闲时间
threadPool.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds());
//异步方法内部线程名称
threadPool.setThreadNamePrefix("spring默认线程池-");
// 线程池对拒绝任务(无线程可用)的处理策略
threadPool.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 任务都完成再关闭线程池
threadPool.setWaitForTasksToCompleteOnShutdown(true);
// 任务初始化
threadPool.initialize();
return threadPool;
}
}
package com.c.interview2.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "thread.pool")
public class ThreadPoolProperties {
/**
* 核心线程池大小
*/
private int corePoolSize;
/**
* 最大可创建的线程数
*/
private int maxPoolSize;
/**
* 队列最大长度
*/
private int queueCapacity;
/**
* 线程池维护线程所允许的空闲时间
*/
private int keepAliveSeconds;
}
# ========================Thread Pool Config==================
# 线程池配置,注意如果是容器部署就不能用这种获取cpu核心数方式,System.out.println(Runtime.getRuntime().availableProcessors());
thread.pool.corePoolSize=16
thread.pool.maxPoolSize=32
thread.pool.queueCapacity=50
thread.pool.keepAliveSeconds=2
工具类
package com.c.utils;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
public class TaskBatchSendUtils
{
public static <T> void send(List<T> taskList, Executor threadPool, Consumer<? super T> consumer) throws InterruptedException
{
if (taskList == null || taskList.size() == 0)
{
return;
}
if(Objects.isNull(consumer))
{
return;
}
CountDownLatch countDownLatch = new CountDownLatch(taskList.size());
for (T couponOrShortMsg : taskList)
{
threadPool.execute(() ->
{
try
{
consumer.accept(couponOrShortMsg);
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await();
}
public static void disposeTask(String task)
{
System.out.println(String.format("【%s】disposeTask下发优惠卷或短信成功", task));
}
public static void disposeTaskV2(String task)
{
System.out.println(String.format("【%s】disposeTask下发邮件成功", task));
}
}
package com.c.service.impl;
import com.atguigu.interview2.service.CouponServiceV2;
import com.atguigu.interview2.utils.TaskBatchSendUtils;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* service方法
* @create 2024-05-08 17:06
*/
@Service
public class CouponServiceImplV2 implements CouponServiceV2
{
//下发优惠卷数量
public static final Integer COUPON_NUMBER = 50;
@Resource
private ThreadPoolTaskExecutor threadPool;
@SneakyThrows
@Override
public void batchTaskActionV2()
{
//1 模拟要下发的50条优惠卷,上游系统给我们的下发优惠卷源头
List<String> coupons = getCoupons();
long startTime = System.currentTimeMillis();
//2 调用工具类批处理任务,这些优惠卷coupons,放入线程池threadPool,做什么业务disposeTask下发
TaskBatchSendUtils.send(coupons,threadPool,TaskBatchSendUtils::disposeTask);
//TaskBatchSendUtils.send(coupons,threadPool,TaskBatchSendUtils::disposeTaskV2);
long endTime = System.currentTimeMillis();
System.out.println("----costTime: "+(endTime - startTime) +" 毫秒");
}
private static List<String> getCoupons()
{
List<String> coupons = new ArrayList<>(COUPON_NUMBER);
for (int i = 1; i <= COUPON_NUMBER; i++)
{
coupons.add("优惠卷--"+i);
}
return coupons;
}
}
异步编排和并行优化
- app的UI渲染需要并行获取区块和banner数据进行展示,这部分就需要进行并行请求获取数据处理。
数据结构和算法篇
力扣经典150+热题100
衡量一个算法的好坏——通过时间复杂度和空间复杂度来衡量
-
常见时间复杂度案例,常数阶,对数阶等等。
- 常量阶:HashMap
- 对数阶:二分查找
- 线性阶:单层for循环
- 线性对数阶:单层for循环嵌套对数乘法
- 平方阶:双层for循环
- 指数阶:HashMap扩容,是原值的一倍。
刷题按知识图谱
刷题方式:先找母题,会母题对应的技巧,如双指针,那么所有这类题目就都可以用该技巧解题。
-
双指针
- 只要是原地对数组进行操作就是双指针。
- 左右指针:指针分别指向队列的头和尾,多用于数组。
- 反转字符串
- 两数之和-输入有序数组
- 二分查找
- 快慢指针:指针同向,一快一慢,多用于链表。
- 26题:删除有序数组的重复项 ,O(n)
- 283、移动零 ,移除元素
非严格递增排列的数组,指的是总体是递增的,只是里面包含有重复的元素。
知识图谱
MySQL篇
- 58同城30条mysql军规
- 复合索引的字段先后顺序规则:区分度最高,重复率最低的放在最左边,区分度定最佳左前缀位置。
- InnoDB的行锁是通过锁住索引来实现的,如果加锁查询时没有使用到索引,就会退化成锁表。
- 回表就是数据库根据非主键索引查到到指定记录的主键id后,再通过主键id到聚簇索引树中查找对应行具体数据的过程。
- 大表分页查询优化
- 使用索引覆盖+子查询优化
- 游标查询,即记录上次查询结果的主键位置进行过滤查找,避免使用偏移量offset。
- 服务降级不让使用
如何建立复合索引
Innodb的行锁锁的是什么
什么是回表
千万级以上大表如何添加索引
MySQL如何删除重复数据
千万级大表数据如何进行分页查询优化
58同城30条mysql军规
一、基础规范
(1)必须使用InnoDB存储引擎
解读:支持事务、行级锁、并发性能更好、CPU及内存缓存页优化使得资源利用率更高
(2)必须使用UTF8字符集 UTF-8MB4
解读:万国码,无需转码,无乱码风险,节省空间
(3)数据表、数据字段必须加入中文注释
解读:N年后谁tm知道这个r1,r2,r3字段是干嘛的
(4)禁止使用存储过程、视图、触发器、Event
解读:高并发大数据的互联网业务,架构设计思路是“解放数据库CPU,将计算转移到服务
层”,并发量大的情况下,这些功能很可能将数据库拖死,业务逻辑放到服务层具备更好的
扩展性,能够轻易实现“增机器就加性能”。数据库擅长存储与索引,CPU计算还是上移吧
(5)禁止存储大文件或者大照片
解读:为何要让数据库做它不擅长的事情?大文件和照片存储在文件系统,数据库里存URI
多好
二、命名规范
(6)只允许使用内网域名,而不是ip连接数据库
(7)线上环境、开发环境、测试环境数据库内网域名遵循命名规范
业务名称:xxx
线上环境:dj.xxx.db
开发环境:dj.xxx.rdb
测试环境:dj.xxx.tdb
从库在名称后加-s标识,备库在名称后加-ss标识
线上从库:dj.xxx-s.db
线上备库:dj.xxx-sss.db
(8)库名、表名、字段名:小写,下划线风格,不超过32个字符,必须见名知意,禁止
拼音英文混用
(9)表名t_xxx,非唯一索引名idx_xxx,唯一索引名uniq_xxx
三、表设计规范
(10)单实例表数目必须小于500
(11)单表列数目必须小于30
(12)表必须有主键,例如自增主键
解读:
a)主键递增,数据行写入可以提高插入性能,可以避免page分裂,减少表碎片提升空间和
内存的使用
b)主键要选择较短的数据类型, Innodb引擎普通索引都会保存主键的值,较短的数据类
型可以有效的减少索引的磁盘空间,提高索引的缓存效率
c) 无主键的表删除,在row模式的主从架构,会导致备库夯住
(13)禁止使用外键,如果有外键完整性约束,需要应用程序控制
解读:外键会导致表与表之间耦合,update与delete操作都会涉及相关联的表,十分影响
sql 的性能,甚至会造成死锁。高并发情况下容易造成数据库性能,大数据高并发业务场景
数据库使用以性能优先
四、字段设计规范
(14)必须把字段定义为NOT NULL并且提供默认值
解读:
a)null的列使索引/索引统计/值比较都更加复杂,对MySQL来说更难优化
b)null 这种类型MySQL内部需要进行特殊处理,增加数据库处理记录的复杂性;同等条
件下,表中有较多空字段的时候,数据库的处理性能会降低很多
c)null值需要更多的存储空,无论是表还是索引中每行中的null的列都需要额外的空间来标
识 d)
对null 的处理时候,只能采用is null或is not null,而不能采用=、in、<、<>、!=、
not in这些操作符号。如:where name!=’shenjian’,如果存在name为null值的记
录,查询结果就不会包含name为null值的记录
(15)禁止使用TEXT、BLOB类型
解读:会浪费更多的磁盘和内存空间,非必要的大量的大字段查询会淘汰掉热数据,导致内
存命中率急剧降低,影响数据库性能
(16)禁止使用小数存储货币
解读:使用整数吧,小数容易导致钱对不上
(17)必须使用varchar(20)存储手机号
解读:
a)涉及到区号或者国家代号,可能出现+-()
b)手机号会去做数学运算么?
c)varchar可以支持模糊查询,例如:like“138%”
(18)禁止使用ENUM,可使用TINYINT代替
解读:
a)增加新的ENUM值要做DDL操作
b)ENUM的内部实际存储就是整数,你以为自己定义的是字符串?
五、索引设计规范
(19)单表索引建议控制在5个以内
(20)单索引字段数不允许超过5个
解读:字段超过5个时,实际已经起不到有效过滤数据的作用了
(21)禁止在更新十分频繁、区分度不高的属性上建立索引
解读:
a)更新会变更B+树,更新频繁的字段建立索引会大大降低数据库性能
b)“性别”这种区分度不大的属性,建立索引是没有什么意义的,不能有效过滤数据,性
能与全表扫描类似
(22)建立组合索引,必须把区分度高的字段放在前面
解读:能够更加有效的过滤数据
六、SQL使用规范
(23)禁止使用SELECT *,只获取必要的字段,需要显示说明列属性
解读:
a)读取不需要的列会增加CPU、IO、NET消耗
b)不能有效的利用覆盖索引
(24)禁止使用INSERT INTO t_xxx VALUES(xxx),必须显示指定插入的列属性
解读:容易在增加或者删除字段后出现程序BUG
(25)禁止使用属性隐式转换
解读:SELECT uid FROM t_user WHERE phone=13812345678 会导致全表扫描,而不
能命中phone索引
(26)禁止在WHERE条件的属性上使用函数或者表达式
解读:SELECT uid FROM t_user WHERE from_unixtime(day)>='2017-02-15' 会导致全
表扫描
正确的写法是:SELECT uid FROM t_user WHERE day>= unix_timestamp('2017-02-15
00:00:00')
(27)禁止负向查询,以及%开头的模糊查询
解读:
a)负向查询条件:NOT、!=、<>、!<、!>、NOT IN、NOT LIKE等,会导致全表扫描
b)%开头的模糊查询,会导致全表扫描
(28)禁止大表使用JOIN查询,禁止大表使用子查询
解读:会产生临时表,消耗较多内存与CPU,极大影响数据库性能
(29)禁止使用OR条件,必须改为IN查询
解读:旧版本Mysql的OR查询是不能命中索引的,即使能命中索引,为何要让数据库耗费
更多的CPU帮助实施查询优化呢?
(30)应用程序必须捕获SQL异常,并有相应处理
架构场景设计篇
- 每个项目的业务架构和总体技术架构能画图出来
- 负责的功能和模块用到了哪些技术,做了哪些设计,碰到哪些难题,细节总结。
说说AOP的全部通知加载顺序,异常发生后环绕通知和后置通知还会执行吗?
如何在微服务的日志中记录每个接口URL、状态码和耗时信息?
通过AOP+反射+自定义注解实现。
自定义注解
package com.atguigu.interview2.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @create 2024-05-16 18:51
*/
@Target({ElementType.METHOD})//作用在方法上
@Retention(RetentionPolicy.RUNTIME)//运行时生效
public @interface MethodExporter
{
}
业务Controller
package com.atguigu.interview2.controller;
import com.atguigu.interview2.annotations.MethodExporter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* @create 2024-05-16 18:55
*/
@RestController
@Slf4j
public class MethodExporterController
{
//http://localhost:24618/method/list?page=1&rows=7
@GetMapping(value = "/method/list")
@MethodExporter
public Map list(@RequestParam(value = "page",defaultValue = "1") int page,
@RequestParam(value = "rows",defaultValue = "5")int rows)
{
Map<String,String> result = new LinkedHashMap<>();
result.put("code","200");
result.put("message","success");
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000)); } catch (InterruptedException e) { e.printStackTrace(); }
return result;
}
@MethodExporter
@GetMapping(value = "/method/get")
public Map get()
{
Map<String,String> result = new LinkedHashMap<>();
result.put("code","404");
result.put("message","not-found");
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000)); } catch (InterruptedException e) { e.printStackTrace(); }
return result;
}
@GetMapping(value = "/method/update")
public String update()
{
System.out.println("update method without @MethodExporter");
return "ok update";
}
}
切面类
package com.atguigu.interview2.aops;
import com.atguigu.interview2.annotations.MethodExporter;
import com.atguigu.interview2.annotations.MyRedisCache;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
/**
* @create 2024-05-16 18:52
*/
@Aspect
@Component
@Slf4j
public class MethodExporterAspect
{
//容器捞鱼,谁带着使用了MethodExporter注解将会触发Around业务逻辑
@Around("@annotation(com.atguigu.interview2.annotations.MethodExporter)")
public Object methodExporter(ProceedingJoinPoint proceedingJoinPoint) throws Throwable
{
Object retValue = null;
long startTime = System.currentTimeMillis();
System.out.println("-----@Around环绕通知AAA-methodExporter");
retValue = proceedingJoinPoint.proceed(); //放行
long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;
//1 获得重载后的方法名
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = signature.getMethod();
//2 确定方法名后获得该方法上面配置的注解标签MyRedisCache
MethodExporter methodExporterAnnotation = method.getAnnotation(MethodExporter.class);
if (methodExporterAnnotation != null)
{
//3 获得方法里面的形参信息
StringBuffer jsonInputParam = new StringBuffer();
Object[] args = proceedingJoinPoint.getArgs();
DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
String[] parameterNames = discoverer.getParameterNames(method);
for (int i = 0; i < parameterNames.length; i++)
{
jsonInputParam.append(parameterNames[i] + "\t" + args[i].toString()+";");
}
//4 将返回结果retValue序列化
String jsonResult = null;
if(retValue != null){
jsonResult = new ObjectMapper().writeValueAsString(retValue);
}else{
jsonResult = "null";
}
log.info("\n方法分析上报中 " +
"\n类名方法名:"+proceedingJoinPoint.getTarget().getClass().getName()+"."+proceedingJoinPoint.getSignature().getName()+"()"+
"\n执行耗时:"+costTime+"毫秒"+
"\n输入参数:"+jsonInputParam+""+
"\n返回结果:"+jsonResult+"" +
"\nover"
);
System.out.println("-----@Around环绕通知BBB-methodExporter");
}
return retValue;
}
}
不能引入第3方组件,如何自研限流组件框架,赋能团队
Redis+Lua脚本(原子性)+AOP+反射+自定义注解
Redis配置类
package com.atguigu.interview2.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @auther zzyy
* @create 2024-05-16 19:06
*/
@Configuration
@EnableAspectJAutoProxy //V2 开启AOP自动代理
public class RedisConfig
{
/**
* @param lettuceConnectionFactory
* @return
*
* redis序列化的工具配置类,下面这个请一定开启配置
* 127.0.0.1:6379> keys *
* 1) "ord:102" 序列化过
* 2) "\xac\xed\x00\x05t\x00\aord:102" 野生,没有序列化过
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory)
{
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
//设置key序列化方式string
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置value的序列化方式json
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
注解类
package com.atguigu.interview2.annotations;
import java.lang.annotation.*;
/**
* @create 2024-05-23 15:44
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface RedisLimitAnnotation
{
/**
* 资源的key,唯一
* 作用:不同的接口,不同的流量控制
*/
String key() default "";
/**
* 最多的访问限制次数
*/
long permitsPerSecond() default 2;
/**
* 过期时间也可以理解为单位时间或滑动窗口时间,单位秒,默认60
*/
long expire() default 60;
/**
* 得不到令牌的提示语
*/
String msg() default "default message:系统繁忙or你点击太快,请稍后再试,谢谢";
}
切面类
package com.atguigu.interview2.aops;
import com.atguigu.interview2.annotations.RedisLimitAnnotation;
import com.atguigu.interview2.exception.RedisLimitException;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;
import org.springframework.core.io.ClassPathResource;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/**
* @auther zzyy
* @create 2024-05-23 15:45
*/
@Slf4j
@Aspect
@Component
public class RedisLimitAop
{
Object result = null;
@Resource
private StringRedisTemplate stringRedisTemplate;
private DefaultRedisScript<Long> redisLuaScript;
@PostConstruct
public void init()
{
redisLuaScript = new DefaultRedisScript<>();
redisLuaScript.setResultType(Long.class);
redisLuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimiter.lua")));
}
@Around("@annotation(com.atguigu.interview2.annotations.RedisLimitAnnotation)")
public Object around(ProceedingJoinPoint joinPoint)
{
System.out.println("---------环绕通知1111111");
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
//拿到RedisLimitAnnotation注解,如果存在则说明需要限流,容器捞鱼思想
RedisLimitAnnotation redisLimitAnnotation = method.getAnnotation(RedisLimitAnnotation.class);
if (redisLimitAnnotation != null)
{
//获取redis的key
String key = redisLimitAnnotation.key();
String className = method.getDeclaringClass().getName();
String methodName = method.getName();
String limitKey = key +"\t"+ className+"\t" + methodName;
log.info(limitKey);
if (null == key)
{
throw new RedisLimitException("it's danger,limitKey cannot be null");
}
long limit = redisLimitAnnotation.permitsPerSecond();
long expire = redisLimitAnnotation.expire();
List<String> keys = new ArrayList<>();
keys.add(key);
Long count = stringRedisTemplate.execute(
redisLuaScript,
keys,
String.valueOf(limit),
String.valueOf(expire));
System.out.println("Access try count is "+count+" \t key= "+key);
if (count != null && count == 0)
{
System.out.println("启动限流功能key: "+key);
//throw new RedisLimitException(redisLimitAnnotation.msg());
return redisLimitAnnotation.msg();
}
}
try {
result = joinPoint.proceed();//放行
} catch (Throwable e) {
throw new RuntimeException(e);
}
System.out.println("---------环绕通知2222222");
System.out.println();
System.out.println();
return result;
}
/**
* 构建redis lua脚本,防御性编程,程序员自我保护,闲聊
* 能用LuaScript,就不要用java拼装
* @return
*/
/*private String buildLuaScript() {
StringBuilder luaString = new StringBuilder();
luaString.append("local key = KEYS[1]");
//获取ARGV内参数Limit
luaString.append("\nlocal limit = tonumber(ARGV[1])");
//获取key的次数
luaString.append("\nlocal curentLimit = tonumber(redis.call('get', key) or '0')");
luaString.append("\nif curentLimit + 1 > limit then");
luaString.append("\nreturn 0");
luaString.append("\nelse");
//自增长 1
luaString.append("\n redis.call('INCRBY', key, 1)");
//设置过期时间
luaString.append("\nredis.call('EXPIRE', key, ARGV[2])");
luaString.append("\nend");
return luaString.toString();
}*/
}
lua脚本
--获取KEY,针对那个接口进行限流
local key = KEYS[1]
--获取注解上标注的限流次数
local limit = tonumber(ARGV[1])
local curentLimit = tonumber(redis.call('get', key) or "0")
--超过限流次数直接返回零,否则再走else分支
if curentLimit + 1 > limit
then return 0
-- 首次直接进入
else
-- 自增长 1
redis.call('INCRBY', key, 1)
-- 设置过期时间
redis.call('EXPIRE', key, ARGV[2])
return curentLimit + 1
end
--@RedisLimitAnnotation(key = "redisLimit", permitsPerSecond = 2, expire = 1, msg = "当前排队人数较多,请稍后再试!")
应用Controller
package com.atguigu.interview2.controller;
import cn.hutool.core.util.IdUtil;
import com.atguigu.interview2.annotations.RedisLimitAnnotation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @auther zzyy
* @create 2024-05-23 15:44
*/
@Slf4j
@RestController
public class RedisLimitController
{
/**
* Redis+Lua脚本+AOP+反射+自定义注解,打造我司内部基础架构限流组件
* http://localhost:24521/redis/limit/test
*
* 在redis中,假定一秒钟只能有3次访问,超过3次报错
* key = redisLimit
* Value = permitsPerSecond设置的具体值
* 过期时间 = expire设置的具体值,
* permitsPerSecond = 3, expire = 10
* 表示本次10秒内最多支持3次访问,到了3次后开启限流,过完本次10秒钟后才解封放开,可以重新访问
*/
@GetMapping("/redis/limit/test")
@RedisLimitAnnotation(key = "redisLimit", permitsPerSecond = 3, expire = 10, msg = "当前排队人数较多,请稍后再试!")
public String redisLimit()
{
return "正常业务返回,订单流水:"+ IdUtil.fastUUID();
}
}
架构优化设计之redis缓存实战篇
- 类似与SpringCache功能
- AOP+反射+自定义缓存注解+EL表达式
占位符熟悉
package com.atguigu.interview2.mytest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
/**
* @auther zzyy
* @create 2024-05-24 19:52
*/
@Slf4j
public class SpringELDemo
{
public static void main(String[] args)
{
//1 log日志占位符替换
log.info("log:{}","abcd");
System.out.println();
//2 String.format占位符替换
String result = String.format("%s,java","尚硅谷 study");
System.out.println(result);
System.out.println();
//3 SpringELExpress表达式,#号后面的内容可以被具体值替换
String var = "#userid";
SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
Expression expression = spelExpressionParser.parseExpression(var);
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("userid","1253");
String s = expression.getValue(context).toString();
System.out.println(s);
}
}
测试Service
package com.atguigu.interview2.service.impl;
import com.atguigu.interview2.annotations.MyRedisCache;
import com.atguigu.interview2.entities.User;
import com.atguigu.interview2.mapper.UserMapper;
import com.atguigu.interview2.service.UserService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
/**
* @auther zzyy
* @create 2024-02-17 16:15
*/
@Service
@Slf4j
public class UserServiceImpl implements UserService
{
//==========第一组,only mysql==========
/*@Resource
private UserMapper userMapper;
@Override
public int addUser(User user)
{
return userMapper.insertSelective(user);
}
@Override
public User getUserById(Integer id)
{
return userMapper.selectByPrimaryKey(id);
}*/
//==========第二组,redis+mysql==========
/*public static final String CACHE_KEY_USER = "user:";
@Resource
private UserMapper userMapper;
@Resource
private RedisTemplate redisTemplate;
@Override
public int addUser(User user)
{
log.info("插入之前user:{}",user);
int retValue = userMapper.insertSelective(user);
log.info("插入之后user:{}",user);
log.info("=================================");
if(retValue > 0)
{
//到数据库里面,重新捞出新数据出来,做缓存
user=this.userMapper.selectByPrimaryKey(user.getId());
//缓存key
String key=CACHE_KEY_USER+user.getId();
//往mysql里面插入成功随后再从mysql查询出来,再插入redis
redisTemplate.opsForValue().set(key,user);
}
return retValue;
}
@Override
public User getUserById(Integer id)
{
User user = null;
//缓存key
String key=CACHE_KEY_USER+id;
//1 查询redis
user = (User) redisTemplate.opsForValue().get(key);
//redis无,进一步查询mysql
if(user==null)
{
//从mysql查出来user
user=this.userMapper.selectByPrimaryKey(id);
// mysql有,redis无
if (user != null)
{
//把mysql捞到的数据写入redis,方便下次查询能redis命中。
redisTemplate.opsForValue().set(key,user);
}
}
return user;
}*/
//==========第三组,@MyRedisCache+mysql==========
public static final String CACHE_KEY_USER = "user:";
@Resource
private UserMapper userMapper;
@Resource
private RedisTemplate redisTemplate;
@Override
public int addUser(User user)
{
log.info("插入之前user:{}",user);
int retValue = userMapper.insertSelective(user);
log.info("插入之后user:{}",user);
log.info("=================================");
if(retValue > 0)
{
//到数据库里面,重新捞出新数据出来,做缓存
user=this.userMapper.selectByPrimaryKey(user.getId());
//缓存key
String key=CACHE_KEY_USER+user.getId();
//往mysql里面插入成功随后再从mysql查询出来,再插入redis
redisTemplate.opsForValue().set(key,user);
}
return retValue;
}
@Override
//会将返回值存进redis里,key生成规则需要程序员用SpEL表达式自己指定,value就是程序从mysql查出并返回的user
//redis的key 等于 keyPrefix:matchValue
@MyRedisCache(keyPrefix = "user",matchValue = "#id")
public User getUserById(Integer id)
{
return userMapper.selectByPrimaryKey(id);
}
}
RedisConfig
package com.atguigu.interview2.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @auther zzyy
* @create 2024-05-16 19:06
*/
@Configuration
@EnableAspectJAutoProxy //V2 开启AOP自动代理
public class RedisConfig
{
/**
* @param lettuceConnectionFactory
* @return
*
* redis序列化的工具配置类,下面这个请一定开启配置
* 127.0.0.1:6379> keys *
* 1) "ord:102" 序列化过
* 2) "\xac\xed\x00\x05t\x00\aord:102" 野生,没有序列化过
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory)
{
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
//设置key序列化方式string
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置value的序列化方式json
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
自定义缓存注解
package com.atguigu.interview2.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @auther zzyy
* @create 2024-05-16 19:28
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRedisCache //@EnableAspectJAutoProxy //启AOP自动代理
{
//约等于键的前缀prefix,
String keyPrefix();
//SpringEL表达式,解析占位符对应的匹配value值
String matchValue();
}
AOP切面类
package com.atguigu.interview2.aops;
import com.atguigu.interview2.annotations.MyRedisCache;
import jakarta.annotation.Resource;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.aspectj.lang.reflect.MethodSignature;
import java.lang.reflect.Method;
import java.util.Objects;
/**
* @auther zzyy
* @create 2024-05-16 19:29
*/
@Component
@Aspect
public class MyRedisCacheAspect
{
@Resource
private RedisTemplate redisTemplate;
//配置织入点
@Pointcut("@annotation(com.atguigu.interview2.annotations.MyRedisCache)")
public void cachePointCut(){}
@Around("cachePointCut()")
public Object doCache(ProceedingJoinPoint joinPoint)
{
Object result = null;
try
{
//1 获得重载后的方法名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
//2 确定方法名后获得该方法上面配置的注解标签MyRedisCache
MyRedisCache myRedisCacheAnnotation = method.getAnnotation(MyRedisCache.class);
//3 拿到了MyRedisCache这个注解标签,获得该注解上面配置的参数进行封装和调用
String keyPrefix = myRedisCacheAnnotation.keyPrefix();
String matchValueSpringEL = myRedisCacheAnnotation.matchValue();
//4 SpringEL 解析器
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(matchValueSpringEL);
EvaluationContext context = new StandardEvaluationContext();
//5 获得方法里面的形参个数
Object[] args = joinPoint.getArgs();
DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
String[] parameterNames = discoverer.getParameterNames(method);
for (int i = 0; i < parameterNames.length; i++)
{
System.out.println(parameterNames[i] + "\t" + args[i].toString());
context.setVariable(parameterNames[i], args[i].toString());
}
//6 通过上述,拼接redis的最终key形式
String key = keyPrefix + ":" + expression.getValue(context).toString();
System.out.println("------redis 查询 key: " + key);
//7 先去redis里面查询看有没有
result = redisTemplate.opsForValue().get(key);
if (result != null)
{
System.out.println("------redis里面有,我直接返回结果不再打扰mysql: " + result);
return result;
}
//8 redis里面没有,去找msyql查询或叫进行后续业务逻辑
//-------aop精华部分,才去找findUserById方法干活
result = joinPoint.proceed();//放行
//9 mysql步骤结束,还需要把结果存入redis一次,缓存补偿
if (result != null)
{
redisTemplate.opsForValue().set(key, result);
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return result;
}
}
抖音淘宝直播卖货和B站弹幕技术
-
使用ZSet来实现弹幕的存储,每条弹幕生成的时间戳作为score分数保存。
- key:
room:id
- key:
-
记录用户首次进入直播间的时间戳,每次都拉取当前时间到之前时间戳之间的内容。
- key:
user:uid:room:id
- key:
-
用户首次进入查询命令
-
zrevrange room:id 0 10 withscores
查询10条数据 set user:uid:room:id timestamp
-
-
用户持续观看直播时定时拉取弹幕,按照分数范围查询,分页带分数返回
ZREVRANGEBYSCORE room:id 1905 1900 LIMIT 0 10
ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
注意max在前面
弹幕对象
package com.atguigu.interview2.entities;
import lombok.Data;
import java.io.Serializable;
/**
* @auther zzyy
* @create 2024-05-25 17:58
*/
@Data
public class Content implements Serializable
{
private Integer id;
private Integer userId;
private String content;
}
Redis常量
package com.atguigu.interview2.entities;
/**
* @auther zzyy
* @create 2024-05-25 18:01
*/
public class Constants {
//room:100 尚硅谷直播间 即是redis key
public static final String ROOM_KEY = "room:";
//用户读取点播数据的时间点,某个观众什么时间戳进入到了直播间
public static final String ROOM_USER_TIME_KEY = "user:room:time:";//user:room:time:12 12就是UserID观众ID
}
模拟弹幕发生Service类
package com.atguigu.interview2.service;
import com.atguigu.interview2.entities.Constants;
import com.atguigu.interview2.entities.Content;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import cn.hutool.core.util.RandomUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @auther zzyy
* @create 2024-05-25 18:03
*/
@Service
@Slf4j
public class TaskService {
@Resource
private RedisTemplate redisTemplate;
/**
*模拟直播间的数据
*/
@PostConstruct
public void init(){
log.info("启动初始化,淘宝直播弹幕case开始 ..........");
System.out.println();
//1 微服务启动一个线程,模拟直播间各个观众发言
new Thread(() ->
{
AtomicInteger atomicInteger = new AtomicInteger();
while (true)
{
//2 模拟观众各种发言,5秒一批数据,自己模拟造一批发言数据到100后break
if(atomicInteger.get() == 100)
{
break;
}
//3 模拟直播100房间号 的弹幕数据,拼接redis的Key room:100
String key= Constants.ROOM_KEY+100;
Random rand = new Random();
for(int i=1;i<=5;i++){
Content content = new Content();
int id= rand.nextInt(1000) + 1;
content.setUserId(id);
int temp= rand.nextInt(100) + 1;
content.setContent("发表言论: "+temp+"\t"+RandomUtil.randomString(temp));
long time=System.currentTimeMillis()/1000;
//4 对应的redis命令 zadd room:100 time content
/**
* 4.1 redis的原生命令
* ZADD key score1 member1 [score2 member2] [score3 member3]
* 向有序集合添加一个或多个成员,或者更新已存在成员的分数
*
* 4.2 redisTemplate操作Zset的API
* Boolean add(K key, V value, double score);
*/
this.redisTemplate.opsForZSet().add(key,content,time);
log.info("模拟直播间100房间号的发言弹幕数据={}",content);
}
//TODO 在分布式系统中,建议用xxl-job来实现定时,此处阳哥为了直播方便讲解,简单模拟
try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
//模拟观众各种发言,5秒一批数据,到100自动退出break
atomicInteger.getAndIncrement();
System.out.println("-------每间隔5秒钟,拉取一次最新聊天记录");
}
},"init_Live_Data").start();
}
}
弹幕拉取接口类
package com.atguigu.interview2.controller;
import com.atguigu.interview2.entities.Constants;
import com.atguigu.interview2.entities.Content;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* @auther zzyy
* @create 2024-05-25 18:04
*/
@RestController
@Slf4j
public class LiveController
{
@Resource
private RedisTemplate redisTemplate;
/**
*某个用户(userId=12)第一次进入房间,返回最新的前5条弹幕
* http://localhost:24618/goRoom?roomId=100&userId=12
*/
@GetMapping(value = "/goRoom")
public List<Content> goRoom(Integer roomId, Integer userId){
List<Content> list= new ArrayList<>();
String key= Constants.ROOM_KEY+roomId;
//进入房间,返回最新的前5条弹幕
//对应redis命令,ZREVRANGE room:100 0 4 WITHSCORES
Set<ZSetOperations.TypedTuple<Content>> rang= this.redisTemplate.opsForZSet().reverseRangeWithScores(key,0,4);
for (ZSetOperations.TypedTuple<Content> obj:rang){
list.add(obj.getValue());
log.info("首次进房间取最新前5条弹幕content={},score={}",obj.getValue(),obj.getScore().longValue());
}
String userkey=Constants.ROOM_USER_TIME_KEY+userId;
//把当前的时间T,保持到redis,供下次拉取用
Long now=System.currentTimeMillis()/1000;
this.redisTemplate.opsForValue().set(userkey,now);
return list;
}
/**
*登录房间后 客户端间隔5秒钟来拉取数据
* http://localhost:24618/commentList?roomId=100&userId=12
*/
@GetMapping(value = "/commentList")
public List<Content> commentList(Integer roomId,Integer userId){
List<Content> list= new ArrayList<>();
String key= Constants.ROOM_KEY+roomId;
String userkey=Constants.ROOM_USER_TIME_KEY+userId;
long now=System.currentTimeMillis()/1000;
//拿取上次的读取时间
Long ago=Long.parseLong(this.redisTemplate.opsForValue().get(userkey).toString());
log.info("查找时间范围:{} {}",ago,now);
//获取上次到现在的数据 ZREVRANGE room:100 0 4 WITHSCORES
Set<ZSetOperations.TypedTuple<Content>> rang= this.redisTemplate.opsForZSet().rangeByScoreWithScores(key,ago,now);
for (ZSetOperations.TypedTuple<Content> obj:rang){
list.add(obj.getValue());
log.info("持续观看直播content={},score={}",obj.getValue(),obj.getScore().longValue());
}
//把当前的时间Time,保持到redis,供下次拉取用
now=System.currentTimeMillis()/1000;
this.redisTemplate.opsForValue().set(userkey,now);
return list;
}
}
你项目中用过哪些设计模式?谈谈落地案例,不要背书
- 面向接口编程而不是面向实现编程,优先使用组合而不是使用继承。
- 两个减少。减少if else 判断分支本身,减少每个分支里面的业务内容。
- InitializingBean赋能。
- 模版方法:定好模版,抽象封装,父子分工,各归其位。
- 下面代码里实现的模版方法并不是很好的例子,目标方法应该是getCoca()方法是一个复杂的实现过程,可以抽取实现步骤,不同的子类可以实现自己自定义的子类方法。
代码实现
重点
-
自我介绍
- 结合案例,突出自己的优势
-
项目
- 说说你最熟悉的项目?
- 项目介绍
- 要求把简历上每个项目的整体架构和技术、业务亮点整理出来。
- 每个项目中有没有遇到什么问题?是如何解决的。
- 说说你最熟悉的项目?
-
案例(场景题)
- 如何使用简历上的这些技术栈,实现了哪些功能。
- 可以结合自己最熟悉的项目来讲。
- 如何使用简历上的这些技术栈,实现了哪些功能。
-
算法
- leetcode
参考
- 尚硅谷Java大厂面试题第4季
- 阿里Java规约