P2P项目过程中的细点与问题

文章目录

  • 一、多线程
    • 1. 缓存击穿
    • 2. 线程池
      • (1)体系结构
      • (2)Executors
      • (3)ExecutorService
    • 3. 掉单问题
  • 二、数据库相关
    • 1. cast与decimal
    • 2. 业务逻辑
      • (1)DQL
      • (2)DML
    • 3. resultMap
    • 4. Dao层方法起名字
    • 5*. Datetime时区问题
    • 6. 多表联查查出的值的存放位置
    • 7. \$()与$(document).ready()
    • *8. 属性和JS中均定义了某个事件
    • 9. MyBatis逆向工程
    • 10*. 数据库乐观锁机制
    • 11. 数据库中的日期
  • 三、JS相关
    • 1. 使用正则
    • 2*. $.each()中使用return
    • 3*. $().text()
    • 4. jQuery选取
    • 5. 对象失去焦点
    • 6. JQ中的md5加密
  • 四、一些问题
    • 1. dubbo超时重试问题
    • 2. 产品超卖问题
    • 3. 订单不过夜
  • 五、一些操作
    • 1. 随机
    • 3. TODO
    • 4. 脱敏操作
    • 5. 取小数点后两位
    • 6. 日期格式与整数相加
      • (1)使用Calendar
      • (2)使用Commons-lang3工具包
    • 7. 引入非(本地和中央)仓库的jar包
    • 8. 生成全局唯一单号
    • 9. 时序图
    • 10. 后台跳转页面
  • 六、一些功能
    • 1. 短信验证码进行登录
    • 2. 认证功能
    • 3. 登录后返回上次停留的页面
    • 4. 投资排行榜功能
    • 5. 支付宝网页支付功能
    • 6. 生成简单的图片验证码
    • 7. 生成二维码
  • 七、一些约定俗称
    • 8. 退出登录
  • 其它
    • 1. 注解参数
      • (1)@RequestParam
    • 2. maven的继承
    • 5. jsp中对字符串处理
    • 6. templates文件夹
    • 可变长参数

一、多线程

1. 缓存击穿

缓存击穿,是指查询一个数据库一定不存在的数据。正常的使用缓存流程大致是,数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象,放进缓存。如果数据库查询对象为空,则不放进缓存。在高并发下,当第一个线程将数据放入缓存中之前,会出现多个线程从数据库中进行查询的现象,即缓存穿透

解决方法:双重判断加同步代码块
固定语法:

if(第一次对值进行判空,为空时继续){
	synchronized (this){
		对值进行更新
		if(第二次对值进行判空,为空时继续){
			从数据库中获取值
		}
	}
}

一个小例子:

public Integer queryAllUserCount() {
    BoundValueOperations<Object, Object> ops = redisTemplate.boundValueOps(Const.ALL_USER_COUNT);
    Integer allUserCount = (Integer) ops.get();

    if (!ObjectUtils.allNotNull(allUserCount)) {
        synchronized (this) {
        	allUserCount = (Integer) ops.get();
        	
            if (!ObjectUtils.allNotNull(allUserCount)) {
                allUserCount = userMapper.queryAllUserCount();
                ops.set(allUserCount);
            }
        }
    }
    return allUserCount;
}

2. 线程池

线程池是提供一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁的额外开销,提高了响应的速度。

(1)体系结构

线程池的体系结构:

java.util.concurrent.Executor 负责线程的使用和调度的根接口
		|--ExecutorService 子接口: 线程池的主要接口
				|--ThreadPoolExecutor 线程池的实现类
				|--ScheduledExceutorService 子接口: 负责线程的调度
					|--ScheduledThreadPoolExecutor : 继承ThreadPoolExecutor,实现了ScheduledExecutorService

(2)Executors

Executors是一个工具类,用于创建线程池 :

  • newCachedThreadPool:用来创建一个可以无限扩大的线程池,适用于服务器负载较轻,执行很多短期异步任务。
  • newFixedThreadPool:创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于可以预测线程数量的业务中,或者服务器负载较重,对当前线程数量进行限制。
  • newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务,并且在任意时间点,不会有多个线程是活动的场景。
  • newScheduledThreadPool:可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景。
  • newWorkStealingPool:创建一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu数量的线程来并行执行,适用于大耗时的操作,可以并行来执行

(3)ExecutorService

ExecutorService 对象的一些方法:

  • submit()
    • 提交Callable对象,需要重写call()方法,返回Future
    • 提交Runnable对象,需要重写run()方法,返回Future,
  • shutdown()
    • 关闭线程池

3. 掉单问题

简单的一个处理:使用定时器去扫描掉单,然后再处理,使用锁解决定时器和支付同时运行的情况(在支付时(创建了订单但未支付完成),定时器同时扫描到了该条数据,使用锁解决冲突)

二、数据库相关

1. cast与decimal

  • CAST:进行数据类型转换,AS关键字分隔的源值和目标数据类型,前面是源值,后面是目标数据类型
  • DECIMAL:格式化数字(小数点前的数据表示最多显示的总位数,小数点后的数据表示保留几位小数)
    例如:求出学生的平均年龄,
select cast(avg(age) as decimal(10,2)) as avgAge from student

2. 业务逻辑

(1)DQL

一个查询语句就是一个完整的业务逻辑,当前台的响应要获取多个不相关的数据值(不同表或者没有关系的数据)的时候,在Controller层要多次调用相应的Service层,分别查出这些不相关的数据。

(2)DML

在Controller层,对于DML,要调用完整的业务,有时候是多个DML语言是一个业务。这时候给方法起的名字一般在控制层是业务名称,在业务层再细化成各个操作的名字

3. resultMap

MyBatis逆向工程生成的 mapper.xml 中使用了 标签,如果查询对应的实体类,则在 标签中使用 resultMap属性指定与实体类的映射关系,因为可能存在表中的字段与实体类的字段不同的情况

4. Dao层方法起名字

Dao层方法的名字要细化,以便方法可以复用

5*. Datetime时区问题

java中使用Date对象往MySQL数据库中存储 Datetime 类型的日期,总是少加个时区。

原因:指定数据库的url时,有个参数serverTimezone是用来指定时区的,而我写的是UTC(世界标准时间),将UTC改为CTT(Asia&Shanghai)即可。

spring.datasource.url=jdbc:mysql://localhost:3306/p2p?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=CTT

6. 多表联查查出的值的存放位置

可以在含有外键的表对应的实体类中,添加外键对应字段,该字段是外键对应的表的实体类

例如:
学生表和班级表:学生表中存放有班级表的外键,当多表联查时,可以在学生类中添加一个班级类的字段,在mapper.xml文件中,使用标签和标签来查出的值与实体类字段进行映射

<resultMap id="BaseResultMap" type="com.wkcto.springboot.model.Student">
    <id column="id" jdbcType="INTEGER" property="id"/>
    <result column="name" jdbcType="VARCHAR" property="name"/>
    <result column="age" jdbcType="INTEGER" property="age"/>
    <association property="classRoom" javaType="com.wkcto.springboot.model.ClassRoom">
    	<id column="id" jdbcType="INTEGER" property="id"/>
		<result column="name" jdbcType="VARCHAR" property="name"/>
    association>
resultMap>

7. $()与$(document).ready()

这两个函数的功能是等价的,都是在页面加载完毕后再执行

$(function(){})
//这两个方法是等价的,都是在页面加载完毕后,然后执行
$(document).ready(function(){})	//在文档加载后激活函数

*8. 属性和JS中均定义了某个事件

当html标签的属性和JS中均定义了某个相同的事件,则只会触发属性中定义的事件

9. MyBatis逆向工程

使用MyBatis逆向工程,推荐字段中分隔单词使用下划线"_",则逆向工程生成的实体类,会自动把下划线去掉,并转换为驼峰命名法,因此,要使用标签来将表中的字段与实体类中的字段一一映射起来

10*. 数据库乐观锁机制

实现:基于数据版本(Version)记录机制实现

具体可通过给表加一个版本号或时间戳字段实现,当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断当前版本信息与第一次取出来的版本值大小,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据,拒绝更新,让用户重新操作。

11. 数据库中的日期

current_date:只显示日期,2020-01-01
current_time:只显示时间,23:11:49
current_timestamp:显示日期加时间,2020-01-01 23:11:49

三、JS相关

1. 使用正则

常用的正则表达式:https://editor.csdn.net/md?articleId=103735776

在JS中使用正则表达式:

  • 语法:/正则表达式/.test(字符串),如果匹配则返回true,否则返回false

2*. $.each()中使用return

jquery 的 each 方法中如果使用了 return true 或者 return 相当于是 continue,而 return false 相当于是 break。并不会使方法结束

3*. $().text()

$().text() 会将$() 中所有取出的DOM元素中的文本内容进行拼接,而其它的方法如 $().html() 只会对第一个DOM元素进行操作

4. jQuery选取

  • $("div[class$=Err]"):选取所有class属性以Err结尾的div标签
  • $("div[class^=pro]"):选取所有class属性以pro开头的div标签

5. 对象失去焦点

  • 为 id 为 input 的标签绑定失去焦点事件:
    $("#input").onblur(function(){});
    
  • 触发 id 为 input 的标签的失去焦点事件:
    $("input").blur();
    

6. JQ中的md5加密

var password = $.md5(loginPassword)	//返回加密后的密码

四、一些问题

1. dubbo超时重试问题

在IDEA中进行debug时,因为发生了超时,dubbo自动进行重试,会多次发送请求,如果刚好是往数据库中插入数据,而且还不是在Service层的事务中,那么可能会发生往数据库中重复插入多条相同的数据问题(主键自增)

2. 产品超卖问题

使用数据库乐观锁解决

3. 订单不过夜

生成订单,如果不付款,会在一定时间内过期 ,比如说在15分钟内过期,但是如果是在23点55分生成的订单,则会在 0 点的时候过期,即时间并不够15分钟

五、一些操作

1. 随机

将要随机生成的元素存放在数组中,可以是任何元素,然后使用Math.Random()*array.length来随机生成数组的下标

注意:java中默认小数转为整数是去掉小数点后的部分,使用Math.round(num)可以将小数四舍五入进行取整

3. TODO

在IDEA中使用TODO来记录自己还没有完成的功能

4. 脱敏操作

脱敏操作是指:在页面中,将敏感的数据变得不敏感,比如金额,手机号等,使用***来对原数据进行一定修改,进行脱敏操作

5. 取小数点后两位

  1. 各种语言通用的一种思路:将小数乘以100,再取整,再将整数除以100

  2. java:使用DecimalFormat对象
    0:代表一个数字,如果不存在显示0
    #:代表一个或多个数字,如果不存在则显示为空

    DecimalFormat decimalFormat = new DecimalFormat("#.00");
    Double d = Double.valueOf(decimalFormat.format(13.13521));//
    
  3. js:toFixed(四舍五入保留的位数):

    var num = new Number(12.3863);
    document.write(num.toFixed(2));//输入出:12.39
    

6. 日期格式与整数相加

(1)使用Calendar

主要方法:

  • add(field, amount)
    • field:int类型,指定要加的整数的单位,是常量,Calendar.DATE表示天,
    • amount:int类型,指定数量
public static Date getDateByAddDays(Date date, Integer count) {
    //日期处理类对象
    Calendar instance = Calendar.getInstance();

    //设置日期处理类对象的日期值
    instance.setTime(date);

    //在指定日期上添加天数
    instance.add(Calendar.DATE, count);
    
    return instance.getTime();
}

(2)使用Commons-lang3工具包

直接使用 DateUtils 的相关函数即可,例如:

  • DateUtils.addDays(Date date, int amount):在给定的日期上加上指定的天数
  • DateUtils.addMonths(Date date, int amount):在给定的上期上加上指定的月数

7. 引入非(本地和中央)仓库的jar包

  • groupId/artifactId/version都可以随便填,但是不能有重复
  • 标签指定文件的位置,一般为system
  • 使用标签指定本地jar包的位置
  • ${basedir} 是项目的根目录
<dependency>
	<groupId>com.alipay.sdkgroupId>
	<artifactId>alipay-sdk-javaartifactId>
	<version>1.0version>
	<scope>systemscope>
	<systemPath>${basedir}/src/main/webapp/WEB-INF/lib/alipay-sdk-java20170324180803.jarsystemPath>
dependency>

8. 生成全局唯一单号

  1. 可以使用时间戳加上 redis 全局唯一数字
String s1 = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
Long l2 = redisTemplate.opsForValue().increment("onlyNum", 1L);
String result = s1 + l2;
  1. 使用电话号码加上 redis 全局唯一数字(将上面的时间戳换成电话号即可)

9. 时序图

要会看时序图,会画时序图,对于复杂的业务逻辑,可以画个时序图,使用Rational工具

实线是请求,虚线是响应
ctrl + d 是删除组件

10. 后台跳转页面

从后台访问其它网站的页面,比如说支付宝的页面,可以使用请求转发,但是请求转发只能是get请求方式,请求参数都在地址栏中,这时候可以采用如下的方式:

  • 先跳转到一个自己网站中的一个页面
  • 在该页面中使用from表单内加隐藏域,并使用js在页面加载完毕后自动提交的方式实现get转post请求

例如:

<form method="post" action="http://localhost:9094/pay/api/alipay">
    <input type="hidden" name="out_trade_no" th:value="${out_trade_no}">
    <input type="hidden" name="total_amount" th:value="${total_amount}">
    <input type="hidden" name="subject" th:value="${subject}">
form>

<script>document.forms[0].submit()script>

六、一些功能

1. 短信验证码进行登录

随机生成的验证码存放在redis中,键是手机号,值是验证码

主要的实现过程:https://blog.csdn.net/zyx1260168395/article/details/103747807

2. 认证功能

当只更新某个字段时,实体类中只需传入需要更新的字段即可,即便是实体类中的字段的值和数据库中的值相同,没必要将其传入,因为虽然值一样,但还是更新了一遍数据库,速度慢

比如认证时要更新用户的姓名和身份证号

思路一:从可以用从Session中取出的user(含有许多信息),加入身份证号和姓名,再传入dao层进行更新,但是虽然只用到了实体类中的id,姓名和身份证号,但是user表中其它的字段也更新了一遍,会造成速度缓慢,所以使用思路二

思路二:新建一个User,只加入需要改的字段即可

3. 登录后返回上次停留的页面

思路:跳往登录页时,在前端获取当前页的网址,当作请求参数传递到后台,然后跳到登录页后,存在登录页的一个隐藏域中,当登录成功时,获取到这个隐藏域中的地址,再跳到这个地址

获取当前页的网址(Thymeleaf):
主要属性:

  • #httpServletRequest.requestURL:获取当前页面的URL
  • #httpServletRequest.queryString:获取当前页面的请求参数,为null时省略
var redirectUrl = [[${#httpServletRequest.requestURL + (#httpServletRequest.queryString == null? "": "?" + #httpServletRequest.queryString)}]];
//这两种方法均可
var redirectUrl = [[${#strings.replace(#httpServletRequest.requestURL + '?' + #httpServletRequest.queryString,"?null","")}]];

4. 投资排行榜功能

使用redis中的zset集合(zset可以用来解决各种排行榜问题),key为电话号码,score为累计投资金额,用户每投资一笔,就往score中加一笔。

使用到的方法:
incrementScore(K, V, delta):元素分数增加,delta是增量
rangeWithScores(K,start,end):键为K的集合,索引start<=index<=end的元素子集,返回泛型接口(包括score和value),正序
reverseRangeWithScores(K,start,end):键为K的集合,索引start<=index<=end的元素子集,返回泛型接口(包括score和value),倒序

ZSetOperations 操作解释:https://www.cnblogs.com/pqy521/p/7009620.html

5. 支付宝网页支付功能

要学会看API文档,支付宝文档:https://docs.open.alipay.com/catalog

6. 生成简单的图片验证码

@RequestMapping(value = "/jcaptcha/captcha")
public void handleCaptchaRequest(HttpServletRequest request, HttpServletResponse response) {
    //生成6位随机验证码,这里就简单的使用一个固定的字符串代替了
    String captcha = "1Ag5Kw";
    try {
        //创建字节数组输出流
        ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream();
        //创建图片缓存对象,BufferedImage.TYPE_INT_RGB : 表示一个图像,该图像具有整数像素的 8 位 RGB 颜色
        BufferedImage bufferedImage = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
        //获取图片的画布
        Graphics graphics = bufferedImage.getGraphics();
        //设置画布背景色
        graphics.setColor(Color.GREEN);
        //设置画布填充区域
        graphics.fillRect(0, 0, WIDTH, HEIGHT);
        //边框区域
        graphics.drawRect(1, 1, WIDTH - 2, HEIGHT - 2);
        //设置字体颜色
        graphics.setColor(Color.red);
        //设置字体样式
        graphics.setFont(new Font("微软雅黑", Font.ITALIC, 32));
        //填充数据
        graphics.drawString(captcha, 10, 38);
        //将生成的验证码存放到session中
        request.getSession().setAttribute(Constants.CAPTCHA, captcha);

        ImageIO.write(bufferedImage, "jpeg", jpegOutputStream);
        byte[] captchaChallengeAsJpeg = jpegOutputStream.toByteArray();
        //将验证码输出到页面
        response.setHeader("Cache-Control", "no-store");
        response.setHeader("Pragma", "no-cache");
        response.setDateHeader("Expires", 0L);
        response.setContentType("image/jpeg");
        ServletOutputStream respOs = response.getOutputStream();
        respOs.write(captchaChallengeAsJpeg);
        respOs.flush();
        respOs.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

7. 生成二维码

使用的是Google生成二维码的依赖:


<dependency>
    <groupId>com.google.zxinggroupId>
    <artifactId>coreartifactId>
    <version>3.0.0version>
dependency>

<dependency>
    <groupId>com.google.zxinggroupId>
    <artifactId>javaseartifactId>
    <version>3.0.0version>
dependency>

生成二维码:

Map<EncodeHintType,Object> map = new HashMap<EncodeHintType, Object>();

//设置字符编码
map.put(EncodeHintType.CHARACTER_SET, "UTF-8");

/*创建一个二维码,
第一个参数是二维码内容,
第二个参数是常量,二维码的编码规则,
第三、四个参数是二维码的长和宽,
最后一个参数是设置的一些参数
*/
BitMatrix encode = new MultiFormatWriter().encode("https://blog.csdn.net/zyx1260168395", BarcodeFormat.QR_CODE, 200, 200, map);

//将二维码转换成图片,写到指定的路径上
Path path = FileSystems.getDefault().getPath("D://", "grcde.jpg");
MatrixToImageWriter.writeToPath(encode, "jpg", path);

//在浏览器使用输出流来将二维码图片响应到前台
OutputStream out = response.getOutputStream();
MatrixToImageWriter.writeToStream(encode, "jpg", out);

七、一些约定俗称

业务层:session;biz
jar包名字中有 source 的是源码包,不带 source 的是编译好的jar包
用例:测试中用的功能点(用例图,测试用例)
商户系统:开发者的系统

渠道、终端:
code:通信标识,10000表示通信成功
请求/响应参数又叫请求/响应报文
timestamp:时间戳
sign:签名
out_trade_no:商户订单号

8. 退出登录

@RequestMapping("/logout")
public Object logout(HttpServletRequest request) {
    HttpSession session = request.getSession();

    session.invalidate();

    return "redirect:/";
}

其它

1. 注解参数

当注解中的参数只有一个 value 属性时,可以省略value不写,当注解中有多个参数时,每个参数都必须指定属性名。

(1)@RequestParam

将请求参数绑定到你控制器的方法参数上(是springmvc中接收普通参数的注解)
属性:

  • value:参数名
  • required:是否包含该参数,默认为true,表示该请求路径中必须包含该参数,如果不包含就报错。
  • defaultValue:默认参数值,如果设置了该值,required=true将失效,自动为false,如果没有传该参数,就使用默认

2. maven的继承

父模版中的build标签中的内容是不能被子模块继承的

5. jsp中对字符串处理

使用 fn 标签:https://www.cnblogs.com/evolcq/p/3688443.html?utm_source=tuicool&utm_medium=referral

6. templates文件夹

templates文件夹中的文件是受保护的,必须要通过后台才能进行访问

可变长参数

可变长参数可以传递多个值,也可以传递数组(类型要对)

你可能感兴趣的:(分布式)