1.解决前端接受 Long精度不足的问题
1.在Long类型字段上使用注解标明序列化方式,代码量不大的情况可以考虑
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
2.后端的Long类型的id转用String存储,不推荐,失去了其Long类型本身的意义
3.重写 ObjectMapper
@Configuration
public class LongClassMessageConverter implements InitializingBean {
@Resource
ObjectMapper objectMapper;
private SimpleModule getSimpleModule() {
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance);
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
// 暂时放弃对小long的转换,约定与前端交互数据时,大Long全部转换成字符串
// simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
objectMapper.registerModule(simpleModule);
return simpleModule;
}
@Override
public void afterPropertiesSet() {
SimpleModule simpleModule = getSimpleModule();
objectMapper.registerModule(simpleModule);
}
}
2.https http 混用问题
网络 | 服务 | 备注 |
---|---|---|
192.168.0.1 | sso-server | 基于ssm 使用spring redis session |
192.168.0.2 | business1 | ejb 无redis 使用本地session 本地session 过期则从sso-sever拉取同步session 失败则退出 |
192.168.0.3 | business2 | ssm 使用本地session |
192.168.0.4 | nginx | 一个域名,一个vpn ,两个是不同的域 |
故障现象
外部请求 从nginx 进入 sso-server 后 ,进入bussiness1 进行业务操作 15分钟后无法正常操作,点击按钮无响应,必须刷新整个页面后才能退出登录到首页,退出后首页是 域名 或者vpn地址 这是正确的,本地没有https环境 无法复现,在测试环境进行复现,通过浏览器进行追踪 发现 退出的时候 浏览器控制条报错 提示https 不能重定向到http, 排查后发现在web.xml 中配置的重定向地址是http://192.168.0.1:8080/sso-server/login 重定向的是个ip地址 ,最好的结果是如果使用的是域名访问 则返回域名的退出登录地址 否则是vpn登录地址
至于为什么时间是15分钟左右呢,开始想到了session的超时时间设置,经过排查发现 各个服务的sesion超时时间都是30分钟,中间件也未进行特殊设置,后来想到可能是 spring session 配置的时间有问题, 经过排查发现 配置的时间是1500秒,正好是15分钟 ,至此 问题原因发现了,关键在于怎么解决
需求是 将时间设置为30分钟 ,同时 做业务的时候 session 过期机制需要控制 未操作情况下退出登录 需要根据使用环境退出正确的地址。时间的问题好解决 但是 退出登录正确的地址却不好解决,因为内部是通过nginx的转发实现的,所以无法获取真正的地址,
解决方案:
1.统一设置session超时时间 为30分钟 redis session 也需要设置
2.nginx 添加请求头 表明是通过域名跳转还是vpn 跳转
3.bussiness 各个服务 过滤器添加请求头校验,判断是域名还是vpn session失效后重定向到正确的地址
3.nginx 上传文件 大小控制问题
故障现象: 用户在上传文件时 出现503报错, 经排查发现 是nginx 配置问题 client_max_body_size=1m 而用户上传的文件在100兆以上,故而出现故障
解决方案:
修改client_max_body_size=500M 重新加载配置
http {
# 其他配置
server {
listen 80;
server_name example.com;
client_max_body_size 800m;
location / {
# 设置允许客户端上传的最大文件大小为 200MB
# 其他 location 相关配置
}
# 其他 server 相关配置
}
# 其他 http 相关配置
}
4.spring 小结
spring 注入bean的集中方式
/**
* 1. @bean 注入
2. @Import
1. 直接导入配置类
2. 创建selector 实现ImportSelector 接口
3. 创建 beanDefination 实现ImportBeanDefinitionRegistrar接口
3. FactoryBean 接口实现
1. 通过getbean 拿到的可就是装配的类
2. 如果要获取 factoryBean 加&
3. 默认获取的是工厂bean调用 getObject()的结果
4. 要获取工厂bean本身 则在id 前加 &
bean的生命周期(创建 -初始化 销毁)
*
*
*/
/**
* 通过配置类的方式
* @Import 导入类
*/
@Configuration
@ComponentScan("com.me.domain.dto")
@Import({BaseService.class, BaseSelector.class})
public class Config {
@Bean
public BaseDomain baseDomain(){
return new BaseDomain();
}
}
## 实现 ImportSelector 接口 并导入
/**
* 导入第三方的包到类中
*/
public class BaseSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
/***
* 通过全类名导入 返回一个数组
*/
String string[] = {BaseVO.class.getName()};
return string;
}
}
重要接口
/**
* BeanNameAware, BeanFactoryAware , BeanClassLoaderAware
* BeanNameAware
* void setBeanName(String name); 自定义bean 在容器中的名称
*
* BeanFactoryAware
* public void setBeanFactory(BeanFactory beanFactory) 获取 bean factory
* BeanClassLoaderAware
* public void setBeanClassLoader(ClassLoader classLoader) 获取加载器
*/
public class BaseDomain implements BeanNameAware, BeanFactoryAware , BeanClassLoaderAware {
private Integer id;
private String name;
private BeanFactory beanFactory;
private String beanName;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "BaseDomain{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
@Override
public void setBeanName(String name) {
this.beanName = name;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
System.out.println(classLoader);
}
}
5.死锁
package com.me.deadlock;
/**
* jps
* jstack -l -m > dump.log 47804
*
* jmap -dump:format=b,file=heap.hprof 47804
*
*
*/
public class Test1 {
private static Object a = new Object();
private static Object b = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (a){
System.out.println("持有所A 尝试获取锁B");
synchronized (b){
System.out.println(Thread.currentThread().getId()+"持有所A 尝试获取锁B");
}
}
},"A").start();
new Thread(()->{
synchronized (b){
System.out.println("持有所B 尝试获取锁A");
synchronized (a){
System.out.println(Thread.currentThread().getId()+"持有所B 尝试获取锁A");
}
}
},"B").start();
}
}
6.多线程 线程池处理策略
AbortPolicy 直接抛出异常
CallerRunsPolicy 返回给调用者
DiscardOldestPolicy 抛弃 队列中等待最久的任务 ,加入新的任务
DiscardPolicy 丢弃任务 不处理
7.雪花算法生成 id
package com.me.snack;
public class SnowFlake {
// ==============================Fields===========================================
/**
* 开始时间截 (2015-01-01)
*/
private final long twepoch = 1420041600000L;
/**
* 机器id所占的位数
*/
private final long workerIdBits = 5L;
/**
* 数据标识id所占的位数
*/
private final long datacenterIdBits = 5L;
/**
* 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
*/
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
/**
* 支持的最大数据标识id,结果是31
*/
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
/**
* 序列在id中占的位数
*/
private final long sequenceBits = 12L;
/**
* 机器ID向左移12位
*/
private final long workerIdShift = sequenceBits;
/**
* 数据标识id向左移17位(12+5)
*/
private final long datacenterIdShift = sequenceBits + workerIdBits;
/**
* 时间截向左移22位(5+5+12)
*/
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
/**
* 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
*/
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/**
* 工作机器ID(0~31)
*/
private long workerId;
/**
* 数据中心ID(0~31)
*/
private long datacenterId;
/**
* 毫秒内序列(0~4095)
*/
private long sequence = 0L;
/**
* 上次生成ID的时间截
*/
private long lastTimestamp = -1L;
//==============================Constructors=====================================
/**
* 构造函数
*
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowFlake(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
*
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift) //
| (datacenterId << datacenterIdShift) //
| (workerId << workerIdShift) //
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
*
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
*
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}
//==============================Test=============================================
/**
* 测试
*/
public static void main(String[] args) {
long start = System.currentTimeMillis();
SnowFlake idWorker = new SnowFlake(1, 3);
for (int i = 0; i < 500000; i++) {
long id = idWorker.nextId();
System.out.println(id);
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
8.web3.0 session 配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<display-name></display-name>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<session-config>
<session-timeout>30</session-timeout>
<cookie-config>
<name>MYJSESSIONID</name>
<domain>192.168.83.121</domain>
<path>/</path>
<comment>no zuo no die</comment>
<http-only>true</http-only>
<secure>false</secure>
<max-age>1800</max-age>
</cookie-config>
</session-config>
</web-app>
9.tongweb 修改默认的jsessionID tongweb-web.xml
<?xml version="1.0" encoding="UTF-8"?>
<tongweb-web-app>
<session>
<cookie-properties>
<property name="cookieName" value="web_session"></property>
</cookie-properties>
</session>
</tongweb-web-app>
10.跨域问题
10.1 跨域问题 处理方案1
index.jsp
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
Cookie cookie = new Cookie("token","123456");
cookie.setPath("/");
response.addCookie(cookie);
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<base href="<%=basePath%>">
<title>My JSP 'index.jsp' starting page</title>
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">
<meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
<meta http-equiv="description" content="This is my page">
<script type="text/javascript" src="/t1/static/index.js"></script>
<script type="text/javascript" src="/t1/static/jquery-3.3.1.js"></script>
</head>
<body>
<div>
<button onclick="alertHand2()">btn2</button>
</div>
<script type="text/javascript">
function alertHand2(){
$.get('http://127.0.0.1:8080/t2/c01.jsp',function(resp){
console.log(resp)
})
}
</script>
</body>
</html>
c01.jsp
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
String token = UUID.randomUUID().toString();
Integer totalCount= (Integer) session.getAttribute("clickCount");
if(null==totalCount){
totalCount = 1;
//session.getAttribute("clickCount")
session.setAttribute("clickCount", totalCount);
}
else{
totalCount = totalCount+1;
session.setAttribute("clickCount", totalCount);
}
System.out.println("clickCount:"+totalCount);
response.getWriter().write(token);
//response.addHeader("Access-Control-Allow-Origin", "*");
%>
在传统的session体系之下 跨域问题难以避免 ,即请求地址端口之前的如果不通则会产生跨域问题,
eg: App1 http://localhost:8080/t1/index.jsp
App2 http://127.0.0.1:8080/t2/c01.jsp
当App1 的应用 通过ajax请求App2时 每次的session 都是新的 无法实现回话保持
举个例子 c01.jsp 中每次请求一次 session 中的clickCount 打印的永远都是1 无法实现 加1的操作
如果想实现 加1的效果 则需要做如下处理 即通过nginx 解决 如下方的配置文件
假设 系统的地址为 http://127.0.0.1/t1/index.jsp
ajax 的请求地址修改为 http://127.0.0.1/t2/c01.jsp
如此可解决跨域问题 同时也解决了 ajax session 丢失的问题
如果把应用的 cookie 的path 修改为 / 则可使用cookie传递到不同的应用
nginx.conf
server {
listen 80;
server_name localhost;
location /t1 {
proxy_pass http://127.0.0.1:8080/t1;
}
location /t2 {
proxy_pass http://127.0.0.1:8080/t2;
}
10.2 ajax session 跨域 处理方案2
function alertHand3(){
$.ajax({
method:"POST",
url:"http://127.0.0.1:8080/t2/c03.jsp",
success:function(res){
console.log("success")
console.log(res)
},
error:function(err){
console.log("err")
console.log(err)
}
})
}
/**
* xhrFields:{
withCredentials: true
},
crossDomain:true,
ajax 请求跨域
本地地址 http://localhost:8080/t1/v105.jsp
请求地址 http://127.0.0.1:8080/t1/c03.jsp
ajax 请求设置上述参数
http://127.0.0.1:8080/t1/c03.jsp 甚至响应头
## 客户端地址
response.addHeader("Access-Control-Allow-Origin","http://localhost:8080");
response.addHeader("Access-Control-Allow-Methods","*");
response.addHeader("Access-Control-Max-Age","100");
//允许客户端发送cookies true表示接收,false不接受 默认为false?
response.addHeader("Access-Control-Allow-Credentials","true");
通过上述操作后 发生跨域请求时不会产生新的session 导致意外
*
* */
10.3 cookie 跨域处理方案总结
cookie 包括 cookieName value domain path 在浏览器中 多开多个tab页 在cookie中 如果上面的几项全部一致的话会产生session 覆盖的问题 在App1 中嵌入App2 中的页面 (iframe 嵌入) 如果 App2的页面设置了session ,会把App1 中的session 覆盖掉, 导致 App1中的session中设置的值丢失造成业务无法进行 ,解决方法就是修改 JSESSIONID CookieName让App1 和App2的cookieName 设置不相同 或者 path 设置不相同
一般domain 可以设置为 二级域名 实现 多个App 共享cookie
hutool HttpUtil 发送get 当参数中有%请求报错
故障现象: 服务A后台通过hutool工具包 HttpUtil 发送get 请求时 出现报错,
cn.hutool.core.exceptions.UtilException: URISyntaxException: Malformed escape pair at index 32: http://localhost:8080/test?name=%$#@!
at cn.hutool.core.util.URLUtil.toURI(URLUtil.java:484)
at cn.hutool.core.util.URLUtil.toURI(URLUtil.java:454)
at cn.hutool.core.util.URLUtil.toURI(URLUtil.java:437)
at cn.hutool.http.cookie.GlobalCookieManager.getURI(GlobalCookieManager.java:105)
at cn.hutool.http.cookie.GlobalCookieManager.add(GlobalCookieManager.java:72)
at cn.hutool.http.HttpRequest.initConnection(HttpRequest.java:1237)
at cn.hutool.http.HttpRequest.doExecute(HttpRequest.java:1186)
at cn.hutool.http.HttpRequest.execute(HttpRequest.java:1051)
at cn.hutool.http.HttpRequest.execute(HttpRequest.java:1027)
at cn.hutool.http.HttpUtil.get(HttpUtil.java:146)
经过日志排查后发现 是参数中有% 没有被转义,在浏览器中 如果参数涉及中文等特殊符号 会转码处理 出现诡异的报错
修改方案:
1.使用post方式 最安全
- 对参数进行转码处理
void test1() throws UnsupportedEncodingException {
String name ="%$#@!";
if (name.indexOf('%')>-1){
name = URLEncoder.encode(name,"utf-8");
}
String url = "http://localhost:8080/test?name="+name;
String s = HttpUtil.get(url);
System.out.println(s);
}
为什么涉及金额的都要用BigDecimal 而不是double
工作中涉及金额的字段务必使用BigDecimal,虽然double类型可以表示小数点后15位有效数字,但在进行浮点数运算时,可能会由于精度损失和误差扩散等原因,导致结果无法完全精确到15位小数 特别实际是在RPC调用场景下 ,避免在序列化和反序列化中的异常,传递金额能用字符串就用字符串
背景介绍 用户在贷款的时候查看授信额度 但是显示的科学技术法 分析原因 用户 查看授信的时候 是请求服务A的接口 所有数据类型都用的是 BigDecimal 但是 授信额度的获取是调用了服务B 该系统使用的几乎都是double来表示金额 服务A在返回响应的时候没有对于授信额度做特殊处理,导致用户的授信额度展示出现问题 处理方案: 由于流水线产品打包 已经升级到3.6的版本,我们的版本仍然是3.0的 ,无法在打包平台进行打包部署 为了解决问题,我们直接使用本地打补丁的方式,把补丁替换到生产环境解决了这个问题
模拟远程调用接口 返回结果 是 科学计数法
String str = "{\"code\":200,\"amount\":1.0000000908765431E7}";
Amount amount = JSON.parseObject(str,Amount.class);
System.out.println(amount);
// 把 Double 转换位 BigDecimal
BigDecimal bigDecimal = new BigDecimal(amount.getAmount().toString());
System.out.println(bigDecimal);
使用自动化流水线部署过承诺中的 痛
触发自动部署的频率如何设置 我们最开始 是合并提交后自动开始部署, 但是对于开发来说, 有提交频率过高,自动化部署频率也高了 结果就是并未提高效率 后来,使用固定时间点自动部署, 每天 9点 11:00 14:00 16:30 自动化部署
但是如果需要紧急部署的话 又是需要联系很多人配合,无法胜任紧急部署,需要验证问题, 只能等待下次部署
在部署过程中 后端的效率还比较高 基本上1分钟就可以 但是 遇到过前端 打包 从20分钟 到40分钟的变化 而且中间可能失败, 吃饭前 部署 ,结果回来发现 部署失败了 又要重新部署 很是麻烦
mysql update where 的条件没有索引 导致锁表
SELECT @@tx_isolation; SHOW VARIABLES LIKE 'tx_isolation'; 在 可重复读的条件下 update where 后如果没有索引的字段 会导致锁表
在 读已提交的条件下不会有该问题 BEGIN update t set name ='ls-11' where name='ls' COMMIT
BEGIN update t set name ='zs-11' where name='zs' COMMIT
pg 默认的事务隔离级别
SHOW TRANSACTION ISOLATION LEVEL;
读已提交
read committed
短链生成算法
guava
//Guava 自带的 MurmurHash 算法实现
String url = "https://time.geekbang.org/column/intro/100020801";
long s = Hashing.murmur3_32().hashUnencodedChars(url).padToLong();// 3394174629
/**
* 10进制 转换位 62进制
*/
public static String NumberToText_SIXTWO_LE(long number)
{
final char[] NumberToText_SIXTWO_ARR = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".toCharArray();
final int scale = 62;
StringBuilder sb = new StringBuilder(12);
boolean negative=number<0;
if(negative) number=-number;
if(number<0) return "8m85Y0n8LzA-";
//SU.Log("NumberToText_SIXTWO_LE", number, -(number+1));
long remainder;
while (number != 0) {
remainder = number % scale;
sb.append(NumberToText_SIXTWO_ARR[(int) remainder]);
number = number / scale;
}
if(negative) sb.append('-');
return sb.toString();
}
报错处理 springboot
java.lang.IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available, and ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context.
@EnableAspectJAutoProxy(exposeProxy = true)
该注解添加到 配置类上