一、Redis分布式锁中加锁与解锁、过期如何续命
实现要点:
- 互斥性,同一时刻,只能有一个客户端持有锁。
- 防止死锁发生,如果持有锁的客户端因崩溃而没有主动释放锁,也要保证锁可以释放并且其他客户端可以正常加锁。
- 加锁和释放锁必须是同一个客户端。
- 容错性,只要redis还有节点存活,就可以进行正常的加锁解锁操作。
加锁:直接使用set命令同时设置唯一id和过期时间;
解锁:加锁之后可以返回唯一id,标志此锁是该客户端锁拥有;释放锁时要先判断拥有者是否是自己,然后删除,这个需要redis的lua脚本保证两个命令的原子性执行。
@Slf4j
public class RedisDistributedLock {
private static final String LOCK_SUCCESS = "OK";
private static final Long RELEASE_SUCCESS = 1L;
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
// 锁的超时时间
private static int EXPIRE_TIME = 5 * 1000;
// 锁等待时间
private static int WAIT_TIME = 1 * 1000;
private Jedis jedis;
private String key;
public RedisDistributedLock(Jedis jedis, String key) {
this.jedis = jedis;
this.key = key;
}
// 不断尝试加锁
public String lock() {
try {
// 超过等待时间,加锁失败
long waitEnd = System.currentTimeMillis() + WAIT_TIME;
String value = UUID.randomUUID().toString();
while (System.currentTimeMillis() < waitEnd) {
String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, EXPIRE_TIME);
if (LOCK_SUCCESS.equals(result)) {
return value;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} catch (Exception ex) {
log.error("lock error", ex);
}
return null;
}
public boolean release(String value) {
if (value == null) {
return false;
}
// 判断key存在并且删除key必须是一个原子操作
// 且谁拥有锁,谁释放
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = new Object();
try {
result = jedis.eval(script, Collections.singletonList(key),
Collections.singletonList(value));
if (RELEASE_SUCCESS.equals(result)) {
log.info("release lock success, value:{}", value);
return true;
}
} catch (Exception e) {
log.error("release lock error", e);
} finally {
if (jedis != null) {
jedis.close();
}
}
log.info("release lock failed, value:{}, result:{}", value, result);
return false;
}
}
过期续命:守护线程续命,额外起一个线程,定期检查线程是否还持有锁,如果有则延长过期时间。Redisson 里面就实现了这个方案,使用 “看门狗” 定期检查(每1/3的锁时间检查1次),如果线程还持有锁,则刷新过期时间。
在获取锁成功后,给锁加一个 watchdog,watchdog 会起一个定时任务,在锁没有被释放且快要过期的时候会续期。
二、Spring的ApplicationEvent的使用场景
实现观察者模式的方法,ApplicationContextAware 我们可以把系统中所有ApplicationEvent传播给系统中所有的ApplicationListener;
三、SpringMVC拦截器和过滤器区别
1 拦截器是基于java的反射机制的,而过滤器是基于函数回调。
2 过滤器是servlet规范规定的,只能用于web程序中,而拦截器是在spring容器中,它不依赖servlet容器。
3 过滤器可以拦截几乎所有的请求(包含对静态资源的请求),而拦截器只拦截action请求(不拦截静态资源请求)。
4 拦截器可以访问action上下文、值栈里的对象,而过滤器不能访问。
5 在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。
6 拦截器可以获取IOC容器中的各个bean,而过滤器就不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑。
7 拦截器是被包裹在过滤器之中。
Filter pre -> doService -> dispatcher -> preHandle -> controller -> postHandle -> afterCompletion -> Filter after
四、一个项目中可以有多个dispatcherServelt吗?为什么?
可以配置,优先级不同;
<load-on-startup>1</load-on-startup>是启动顺序,让这个Servlet随Servletp容器一起启动。
<url-pattern>*.form</url-pattern>会拦截*.form结尾的请求。
<servlet-name>example</servlet-name>
五、SpringAOP涉及到什么设计模式?底层原理?
使用到的模式:适配器模式 单例模式 责任链模式 简单工厂 观察者 模版 代理 策略
【1】AOP的设计
在Spring的底层,如果我们配置了代理模式,Spring会为每一个Bean创建一个对应的ProxyFactoryBean的FactoryBean来创建某个对象的代理对象。
每个 Bean 都会被 JDK 或者 Cglib 代理。取决于是否有接口。
每个 Bean 会有多个“方法拦截器”。注意:拦截器分为两层,外层由 Spring 内核控制流程,内层拦截器是用户设置,也就是 AOP。
当代理方法被调用时,先经过外层拦截器,外层拦截器根据方法的各种信息判断该方法应该执行哪些“内层拦截器”。内层拦截器的设计就是职责连的设计。
【2】代理的创建
首先,需要创建代理工厂,代理工厂需要 3 个重要的信息:拦截器数组,目标对象接口数组,目标对象。
创建代理工厂时,默认会在拦截器数组尾部再增加一个默认拦截器 —— 用于最终的调用目标方法。
当调用 getProxy 方法的时候,会根据接口数量大余 0 条件返回一个代理对象(JDK or Cglib)。
注意:创建代理对象时,同时会创建一个外层拦截器,这个拦截器就是 Spring 内核的拦截器。用于控制整个 AOP 的流程。
【3】代理的调用
当对代理对象进行调用时,就会触发外层拦截器。
外层拦截器根据代理配置信息,创建内层拦截器链。创建的过程中,会根据表达式判断当前拦截是否匹配这个拦截器。而这个拦截器链设计模式就是职责链模式。
当整个链条执行到最后时,就会触发创建代理时那个尾部的默认拦截器,从而调用目标方法。最后返回。
五、SpringBoot配置文件加载优先级
1.命令行参数
2.jar包外部的application-{profile}.propertie或application.yml(带spring.profile)配置文件
3.jar包内部的application-{profile}.propertie或application.yml(带spring.profile)配置文件
4.jar包外部的application.propertie或application.yml(不带spring.profile)配置文件
5.jar包内部的application.propertie或application.yml(不带spring.profile)配置文件
六、SpringBoot启动原理
1.从MANIFEST.MF可以看到Main函数是JarLauncher
2.JarLauncher先找到自己所在的目标jar的路径,然后创建了一个Archive。
3.获取lib/下面的jar,并创建一个LaunchedURLClassLoader
4.再从MANIFEST.MF里读取到Start-Class,然后创建一个新的线程来启动应用的Main函数。
七、MySQL聚簇索引
数据库的索引从不同的角度可以划分成不同的类型,聚簇索引便是其中一种。
聚簇索引英文是 Clustered Index,有时候小伙伴们可能也会看到有人将之称为聚集索引等,与之相对的是非聚簇索引或者二级索引。
聚簇索引并不是一种单独的索引类型,而是一种数据的存储方式。在 MySQL 的 InnoDB 存储引擎中,所谓的聚簇索引实际上就是在同一个 B+Tree 中保存了索引和数据行:此时,数据放在叶子结点中,聚簇聚簇,意思就是说数据行和对应的键值紧凑的存在一起。
假设我有如下数据:
那么它的聚簇索引大概就是这个样子:
MySQL 表中的数据在磁盘中只可能保存一份,不可能保存两份,所以,在一个表中,聚簇索引只可能有一个,不可能有多个。
聚簇索引和主键
在 MySQL 中,如果表本身就有设置主键,那么主键就是聚簇索引;如果表本身没有设置主键,则会选择表中的一个唯一且非空的索引来作为聚簇索引;如果表中连唯一非空的索引都没有,那么就会自动选择表中的隐式主键来作为聚簇索引。
1.聚簇索引不一定是主键索引。
2.主键索引一定是聚簇索引。
最佳实践
在使用聚簇索引的时候,主键最好不要使用 UUID 这种随机字符串,使用 UUID 随机字符串至少存在两方面的问题:
1.插入效率低,因为插入可能会导致页分裂,这个前面已经说过了。
2.UUID 字符串所占用的存储空间远远大于一个 bigint,如果使用 UUID 来做主键,意味着在二级索引中,一个叶子结点能够存储的主键值就非常有限,进而可能会导致树增高,搜索时候 IO 次数增多,性能下降。
所以相对来说,主键自增会优于 UUID。
八、重写和重载,参数列表相同,只是泛型不同,会不会报错
重载: 发生在 同一个类中 ,方法名相同而参数列表不同(类型,个数,顺序),返回值类型 和 访问修饰符 可以相同也可以不同。
虽然在方法重载中可以使两个方法的返回值类型不同,但是只有返回值类型不同并不足以区分两个方法的重载,还需要通过参数列表来设置。
重写: 发生在 父子类中 ,方法名和参数列表必须相同 ,返回值类型小于等于父类(即与被重写的方法的返回值类型相同或者是其子类),抛出的异常范围小于等于父类,访问修饰符大于等于父类;如果父类方法访问修饰符为 private ,则子类不能重写该方法。
九、写一个单例(如何防止反射反序列化破坏)
饿汉式:线程安全,调用效率高,不可延时加载
public class Singleton {
//单例,构造方法不能给到外界调用
private Singleton() {};
//加载类时就直接创建对象
private static Singleton instance = new Singleton();
//将对象的调用方法暴露给外界
public static Singleton getInstance(){
return instance;
}
}
饱汉式:线程安全,调用效率低,可延时加载
public static volatic Singleton {
//单例,构造方法不能给到外界调用
private Singleton() {};
//饱汉,调用时才实例化,多线程需要禁止指令重排序,且变量的改变需要对其它线程都可见,必须使用volatile
private static volatile Singleton instance = null;
//将对象的调用方法暴露给外界
public static Singleton getInstance() {
if (null == instance){
//对象未创建时,进入实例化对象语句块
synchronized (Singleton.class){
//需要考虑并发,多个线程进入语句块,保证只有一个线程能实例化对象
if( null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
内部静态类: 具备饿汉式单例模式优点的同时,又可延迟加载
public class Singleton {
//单例,构造方法不能给到外界调用
private Singleton() {};
//静态内部类可以访问外部类的静态属性和静态方法
private static class Inner {
private static Singleton instance = new Singleton();
}
//将对象的调用方法暴露给外界
public static Singleton getInstance(){
return Inner.instance;
}
}
枚举类:枚举单例模式可以防止反射去创建实例
public enum EnumSingleton {
//创建一个枚举对象,该对象天生为单例
INSTANCE;
public EnumSingleton getInstance(){
return INSTANCE;
}
}
另一种防止反射的方式(修改构造函数,支持饿汉式、饱汉式、内部静态类):
/**
* 单例防反射测试 饿汉式试验
* @author Administrator
*/
public class SingletonTest {
private static boolean flag = false;
//单例,构造方法不能给到外界调用
private SingletonTest() {
synchronized(SingletonTest.class) {
if (false == flag) {
flag = !flag;
} else {
throw new RuntimeException("单例模式正在被攻击");
}
}
};
//加载类时就直接创建对象
private static SingletonTest instance = new SingletonTest();
//将对象的调用方法暴露给外界
public static SingletonTest getInstance(){
return instance;
}
public static void main(String[] args) {
try {
Class<SingletonTest> classType = SingletonTest.class;
Constructor<SingletonTest> constructor = classType.getDeclaredConstructor(null);
constructor.setAccessible(true);
SingletonTest singleton = (SingletonTest) constructor.newInstance();
SingletonTest singleton2 = SingletonTest.getInstance();
System.out.println(singleton == singleton2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
可以参考:单例线程池小工具,轻量级使用,可以在小项目中通过一两句话使用线程池。_getsinglepool()-CSDN博客
内存级缓存ConcurrentSkipListMap实现方式_folly concurrentskiplistmap-CSDN博客 中的MemoryCache类
十、ArrayList底层原理
1.使用数组存储元素
2.动态扩容 初始化10,自增1.5倍
3.线程不安全
4.插入和删除元素需要移动数组,导致效率下降
5.随机访问:读取元素时效率较高
十一、手写一个定时的线程池
一篇文章让你彻底搞懂定时线程池ScheduledThreadPoolExecutor(深度剖析)-CSDN博客
十二、Java自带的序列化方式
JDK 自带的序列化,只需实现 java.io.Serializable
接口即可
JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且存在安全问题。比较常用的序列化协议有 Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。
像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。
十三、项目中如何使用策略模式
if判断非常多的时候
public interface Strategy {
public int doOperation(int num1, int num2);
}
public class OperationAdd implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}
public class OperationSubtract implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}
public class Context {
private Strategy strategy;
public Context(Strategy strategy){
this.strategy = strategy;
}
public int executeStrategy(int num1, int num2){
return strategy.doOperation(num1, num2);
}
}
public class StrategyPatternDemo {
public static void main(String[] args) {
Context context = new Context(new OperationAdd());
System.out.println("10 + 5 = " + context.executeStrategy(10, 5));
context = new Context(new OperationSubtract());
System.out.println("10 - 5 = " + context.executeStrategy(10, 5));
}
}
参考: 五:策略模式 + 工厂方法
十四、Spring注解demo
注解类:
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
/**
* 模块
*/
String title() default "";
/**
* 功能
*/
BusinessType businessType() default BusinessType.OTHER;
/**
* 操作人类别
*/
OperatorType operatorType() default OperatorType.MANAGE;
/**
* 是否保存请求的参数
*/
boolean isSaveRequestData() default true;
/**
* 是否保存响应的参数
*/
boolean isSaveResponseData() default true;
/**
* 排除指定的请求参数
*/
String[] excludeParamNames() default {};
}
业务执行类:
/**
* 操作日志记录处理
*
* @author douzi
*/
@Slf4j
@Aspect
@Component
public class LogAspect {
/**
* 排除敏感属性字段
*/
public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };
/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
handleLog(joinPoint, controllerLog, null, jsonResult);
}
/**
* 拦截异常操作
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
handleLog(joinPoint, controllerLog, e, null);
}
protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
try {
// *========数据库日志=========*//
OperLogEvent operLog = new OperLogEvent();
operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
// 请求的地址
String ip = ServletUtils.getClientIP();
operLog.setOperIp(ip);
operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
operLog.setOperName(LoginHelper.getUsername());
if (e != null) {
operLog.setStatus(BusinessStatus.FAIL.ordinal());
operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
}
// 设置方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
operLog.setMethod(className + "." + methodName + "()");
// 设置请求方式
operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
// 处理设置注解上的参数
getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
// 发布事件保存数据库
SpringUtils.context().publishEvent(operLog);
} catch (Exception exp) {
// 记录本地异常日志
log.error("异常信息:{}", exp.getMessage());
exp.printStackTrace();
}
}
/**
* 获取注解中对方法的描述信息 用于Controller层注解
*
* @param log 日志
* @param operLog 操作日志
* @throws Exception
*/
public void getControllerMethodDescription(JoinPoint joinPoint, Log log, OperLogEvent operLog, Object jsonResult) throws Exception {
// 设置action动作
operLog.setBusinessType(log.businessType().ordinal());
// 设置标题
operLog.setTitle(log.title());
// 设置操作人类别
operLog.setOperatorType(log.operatorType().ordinal());
// 是否需要保存request,参数和值
if (log.isSaveRequestData()) {
// 获取参数的信息,传入到数据库中。
setRequestValue(joinPoint, operLog, log.excludeParamNames());
}
// 是否需要保存response,参数和值
if (log.isSaveResponseData() && ObjectUtil.isNotNull(jsonResult)) {
operLog.setJsonResult(StringUtils.substring(JsonUtils.toJsonString(jsonResult), 0, 2000));
}
}
/**
* 获取请求的参数,放到log中
*
* @param operLog 操作日志
* @throws Exception 异常
*/
private void setRequestValue(JoinPoint joinPoint, OperLogEvent operLog, String[] excludeParamNames) throws Exception {
Map<String, String> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
String requestMethod = operLog.getRequestMethod();
if (MapUtil.isEmpty(paramsMap)
&& HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {
String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
operLog.setOperParam(StringUtils.substring(params, 0, 2000));
} else {
MapUtil.removeAny(paramsMap, EXCLUDE_PROPERTIES);
MapUtil.removeAny(paramsMap, excludeParamNames);
operLog.setOperParam(StringUtils.substring(JsonUtils.toJsonString(paramsMap), 0, 2000));
}
}
/**
* 参数拼装
*/
private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) {
StringJoiner params = new StringJoiner(" ");
if (ArrayUtil.isEmpty(paramsArray)) {
return params.toString();
}
for (Object o : paramsArray) {
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
String str = JsonUtils.toJsonString(o);
Dict dict = JsonUtils.parseMap(str);
if (MapUtil.isNotEmpty(dict)) {
MapUtil.removeAny(dict, EXCLUDE_PROPERTIES);
MapUtil.removeAny(dict, excludeParamNames);
str = JsonUtils.toJsonString(dict);
}
params.add(str);
}
}
return params.toString();
}
/**
* 判断是否需要过滤的对象。
*
* @param o 对象信息。
* @return 如果是需要过滤的对象,则返回true;否则返回false。
*/
@SuppressWarnings("rawtypes")
public boolean isFilterObject(final Object o) {
Class<?> clazz = o.getClass();
if (clazz.isArray()) {
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection) o;
for (Object value : collection) {
return value instanceof MultipartFile;
}
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) o;
for (Object value : map.values()) {
return value instanceof MultipartFile;
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|| o instanceof BindingResult;
}
}