高并发下如何保证数据的准确与安全
闲聊:最近越来越发现,如何快速的理解需求也很重要,刚出来一两年的时候,觉着技术才是唯一,看业务感觉都在浪费我时间,所以我所有业务都只是过一下大脑,并不深记。
最近一年,换了新公司,公司的业务复杂了很多,每次做个看着很简单的需求都忙的焦头烂额,后边不断的反思,是我太菜了吗,完全体的代码和第一次写的代码到底差在哪,
并且代码都是我自己独自写的,不存在自己的技术瓶颈问题。其实很多bug也只是多一个参数少一个参数的问题,但是这个参数你不翻阅历史大量代码,根本不知道,就算是问同事,
同事也只是能告诉你个大概,比如查哪个表,但是同事也不能记住那么多细节,这里面的业务都得自己不断的去摸索。我突然意思到,业务逻辑清晰也是能力之一。
正文~
最近做了一个需求,实时的统计用户的数据变动,我当时提出了一个方案,把每天的用户数据汇聚成一个天表,比如想统计用户花了多少钱,那么如果直接sql统计,如果数据量一大,
肯定是抗不住的,可能千万的数据量就是瓶颈了,但是如果把用户的每一天的数据都做一个统计汇总,那么我们在统计用户的时候,只需要再次统计天表就可以了,甚至很多用户只关心
按照月统计的数据,那我们可以按月生成月表。在写这个需求的时候我遇到了很多并发问题。下面我们来一一分析。
1、 当用户数据变动的时候,如何保证事务提交成功了再发送消息。
非事务消息模式下,如果我们的mq发出去就不能撤回了,如果我们的新增报错,那么消息还是发出去了怎么办。
这个时候,我们其实出来处理起来也很简单,只需要获取事务的提交状态,当事务提交成功后,我们再执行发送消息操作,操作如下
if (TransactionSynchronizationManager.isActualTransactionActive()) { ReportMessage finalReportMessage = reportMessage; TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { @Override public void afterCommit() { publish(finalReportMessage); } }); } else { publish(reportMessage); logger.info("消息发送成功,消息id:{},消息体:{}", reportMessage.getMessageId(), JSON.toJSONString(reportMessage)); }
2、 怎么保证消息的id唯一
很多时候,我们要追踪消息,那么这个消息就需要一个唯一id,如果是在分布式环境下,生成唯一id好像也是一个比较麻烦的事情,这时候我们可以通过雪花算法生成唯一id
package com.raycloud.dingfinance.utils; /** * @Author: cxx * @Date: 2021/7/7 10:22 上午 * SnowFlake的结构如下(每部分用-分开): * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0 * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截) * 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69 * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号 * 加起来刚好64位,为一个Long型。 * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。 */ public class SnowflakeIdWorker { // ==============================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; /** * 备用的数据中心ID(0~31),当时钟回拨时,为了不抛异常,启用备用ID */ private long standbyDatacenterId; /** * 是否时钟回拨 */ private boolean isTimestampBack = false; /** * 毫秒内序列(0~4095) */ private long sequence = 0L; /** * 上次生成ID的时间截 */ private long lastTimestamp = -1L; //==============================Constructors===================================== /** * 构造函数 * * @param workerId 工作ID (0~31) * @param datacenterId 数据中心ID (0~31) */ public SnowflakeIdWorker(long workerId, long datacenterId, long standbyDatacenterId) { 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)); } if (standbyDatacenterId > maxDatacenterId || standbyDatacenterId < 0) { throw new IllegalArgumentException(String.format("standby datacenter Id can't be greater than %d or less than 0", maxDatacenterId)); } if (datacenterId == standbyDatacenterId) { throw new IllegalArgumentException("datacenter Id can't equal to standby datacenter Id."); } this.workerId = workerId; this.datacenterId = datacenterId; this.standbyDatacenterId = standbyDatacenterId; } // ==============================Methods========================================== /** * 获得下一个ID (该方法是线程安全的) * * @return SnowflakeId */ public synchronized long nextId() { long timestamp = timeGen(); //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过,这时启用备用的datacenterId if (timestamp < lastTimestamp) { isTimestampBack = true; } else { isTimestampBack = false; } //如果是同一时间生成的,则进行毫秒内序列 if (lastTimestamp == timestamp) { sequence = (sequence + 1) & sequenceMask; //毫秒内序列溢出 if (sequence == 0) { //阻塞到下一个毫秒,获得新的时间戳 timestamp = tilNextMillis(lastTimestamp); } } //时间戳改变,毫秒内序列重置 else { sequence = 0L; } //上次生成ID的时间截 lastTimestamp = timestamp; //要使用的datacenterId long datacenterIdToUse = isTimestampBack ? standbyDatacenterId : datacenterId; //移位并通过或运算拼到一起组成64位的ID return ((timestamp - twepoch) << timestampLeftShift) // | (datacenterIdToUse << 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============================================= /** * 测试 * * @throws InterruptedException */ public static void main(String[] args) throws InterruptedException { long l = System.currentTimeMillis(); SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 1, 0); for (int i = 0; i < 10000000; i++) { Long snowFlakeId = idWorker.nextId(); System.out.println(snowFlakeId); } long l1 = System.currentTimeMillis(); System.out.println(l1 - l); } }
3 、并发消费消息的时候,如何数据正常消费
我们维护用户数据的时候,由于想控制数据量,也就是用户的一段时间的数据合并成一条数据,那么当今天用户还没产生这条数据的时候,如果同时有多条数据进来,大家都去查询数据库,
都发现今天没数据呢,那么这时候就会都去新增,但是我们只想有一条新增的数据,所以先设置唯一索引,这时候只能新增成功一条,那剩下的几条怎么办,又不能舍弃掉,这时候我们可以try住
insert,并且在catch里面再此操作业务,但是因为都在一个事务下,所以我们catch里面也是查询不到数据的,这时候我们可以仿照1的写法,等事务提交之后再做业务。这是针对mysql5.6的处理方式,当mysql5.7的时候,ON DUPLICATE KEY UPDATE可以支持唯一索引冲突的时候变成update操作,具体sql为
insert into a (id,xx) values(1,1) ON DUPLICATE KEY UPDATE amount=ifnull(amount,0) +VALUES(amount);