JAVA和Nginx 教程大全

网站首页 > 精选教程 正文

踩坑日记(四):记一次重复提交引发的线上事故

wys521 2024-12-05 15:43:33 精选教程 15 ℃ 0 评论

场景

活动商品是采用预售形式,用户在活动进行中需要提前将选购的商品进行提交(无支付逻辑),活动结束后采用“抽签”的方式决定用户抽中哪个商品,然后将结果短信通知个用户。流程如下:

问题

管理员反馈某次活动用户抽中了2次,并且商品一模一样。小编赶紧去查了数据库发现这个用户一共提交到数据库 200 个商品,一模一样的,抽签的逻辑中没有去重,导致该用户抽中的概率是普通用户的 200 倍。

原因排查

1、用户快速刷新页面导致重复提交?

检查了提交商品的代码逻辑,对同一个用户、同一场活动、同一款商品 在 Redis 中加了锁,也就是说用户在页面多次提交不会引起上述问题。

2、追踪用户提交链路

通过ELK 日志追踪到用户一次提交的入参里面存放了 200 个一模一样的商品,到这里问题基本找到了。

问题定位

用户是通过非正常手段触发的提交商品API,因为每次请求前端和后端都有做重复提交的校验,但是对于用户的一次提交内容没有做严格的去重校验,导致一次提交了 N 多商品进入到数据库。

解决方案

这里采用的是Set 集合独一无二的性质判断是否有相同的元素,代码示例如下:

订单类.java

public class Order {
    private String iteamId; // 商品ID
		省略其他......  
}

Set 去重

private static Boolean itemIsRepeat(List<Order> orderList) {
		Set<Order> set = new TreeSet<Order>(new Comparator<Order>() {
			public int compare(Order a, Order b) {
				// 字符串则按照asicc码升序排列
				return a.getItemId().compareTo(b.getItemId());
			}
		});
		set.addAll(orderList);
		if (set.size() < orderList.size()) {
			return true;
		}
		return false;
	}
public static void main(String[] args) {
    List<Order> orderList = new ArrayList<Order>(){{
        add(new Order("123"));
        add(new Order("123"));
        add(new Order("123"));
    }};
    if(registItemIsRepeat(registItemRqDtos)) {
       System.out.println("有重复元素"); 
    } else {
        System.out.println("没有重复元素");
    }
}

总结

到这里为止,线上问题已经解决了,有小伙伴会问 为什么数据库不增加联合主键呢?这样就保证用户提交信息的唯一性了。增加数据库联合主键是没问题的,可能之前小伙伴太相信自己写的 Redis 锁了。

那防止用户重复提交其实分为两部分:
防君子:

1、前端js提交禁止按钮可以用一些js组件。

2、使用Post/Redirect/Get模式

在收到用户提交请求后立即执行页面重定向逻辑,这就是所谓的Post-Redirect-Get (PRG)模式。简单的说,当用户提交了表单后,立马执行一个客户端的重定向,转到提交成功信息页面。这能避免用户按F5刷新导致的重复提交,而其也不会出现浏览器表单重复提交的警告,也能消除按浏览器前进和后退按导致的同样问题。

3、在session中存放一个加密字符串

在服务器端,生成一个唯一无规则加密字符串,将它写入session中,并且还需要将session它存放在 HTML 表单的隐藏字段中,然后将表单页面发给浏览器,用户录入信息后点击提交,在服务器端,获取表单中隐藏字段的值,与session中的唯一标识符比较,相等说明是首次提交,就处理本次请求,然后将session中的唯一标识符移除;不相等说明是重复提交,就不再处理。

防小人:

1、数据库唯一索引

insert 插入一条数据的时候 使用唯一索引 update使用 乐观锁 version版本法

但是这种方案在大数据量和高并发下效率是非常依赖数据库硬件能力。

2、借助悲观锁

使用select … for update ,这种和 synchronized 锁住先查再insert or update一样,但要避免死锁,效率也较差。

同样的在高并发下不建议使用。

3、本地锁(适合单机)

原理:

使用了 ConcurrentHashMap 并发容器 putIfAbsent 方法,和 ScheduledThreadPoolExecutor 定时任务,也可以使用guava cache的机制, gauva中有配有缓存的有效时间也是可以的key的生成

Content-MD5

Content-MD5 是指 Body 的 MD5 值,只有当 Body 非Form表单时才计算MD5,计算方式直接将参数和参数名称统一加密MD5

MD5在一定范围类认为是唯一的 近似唯一 当然在低并发的情况下足够了

本地锁只适用于单机部署的应用。

4、分布式锁

基于Redis实现分布式锁。

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表