一、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() {};//饱汉,调用时才实例化,多线程需要禁止指令重排序,且变量的改变需要对其它线程都可见,必须使用volatileprivate 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{@Overridepublic int doOperation(int num1, int num2) {return num1 + num2;}
}public class OperationSubtract implements Strategy{@Overridepublic 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;}
}