并发是一个让人很头疼的问题,通常会在服务端数据库端做处理,保证在并发下数据的准确性。

为此,简要讨论一下,如何通过解决全局生成唯一编号的并发问题。

1 MySQL数据库的锁

1-0 锁的分类

  • 按锁定的数据粒度

    • 表级锁
    • 页级锁
    • 行级锁
  • 按锁定的方式
    • 共享锁
    • 排他锁

读锁 := 共享锁(shared Lock) / 写锁:= 排他锁(exclusive Lock)

1-1 读锁/共享锁

1-1-1 定义

事务A 使用共享锁 获取了某条(或者某些)记录时:

事务B 可以读取这些记录; 可以继续添加共享锁,但是不能修改或删除这些记录;

否则,当事务B 对这些数据修改删除时,会进入阻塞状态,直至:锁等待超时或者事务A提交

1-1-2 用法

[表级读锁]

SET AUTOCOMMIT=0;
LOCK TABLES tb_student READ, tb_student READ, ...; -- 加锁
[do something with tables tb_student and tb_student , ... here];
COMMIT; UNLOCK TABLES; -- 解锁

[行级读锁]

SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE;

1-1-3 注意事项

当使用读锁时,避免产生如下操作

[事务1]
BEGIN;
select * from sys_user where id = 1 LOCK IN SHARE MODE; (步骤1)
update sys_user set username = "taven" where id = 1; (步骤3,发生阻塞)
COMMIT; [事务2]
BEGIN;
select * from sys_user where id = 1 LOCK IN SHARE MODE; (步骤2)
update sys_user set username = "taven" where id = 1; (步骤4,死锁)
COMMIT;

根据我们之前对读锁定义可知:

当有事务拿到一个结果集的读锁时: 其他事务想要更新该结果集,需要拿到读锁的事务提交(释放锁)。

而上述情况2个事务分别拿到了读锁,且都有update 操作,2个事务互相等待造成死锁(2个事务都在等待对方释放读锁)

1-1-4 使用场景

1)  读取结果集的最新版本,同时防止其他事务产生更新该结果集。
主要用在需要数据依存关系时,确认某行记录是否存在,并确保没有其它人对这个记录进行UPDATE或者DELETE操作

1-2 写锁/排他锁

1-2-1 定义

1个写锁会阻塞其他的读锁写锁

事务A 对某些记录添加写锁时:

  • 事务B 无法向这些记录添加写锁或读锁;(注:未添加锁的数据,是可以读取的)
  • 事务B 也无法执行对 锁住的数据进行update / delete操作

1-2-2 使用场景

读取结果集的最新版本,同时防止其他事务产生读取或者更新该结果集。
例如:并发下对商品库存的操作

1-2-3 注意事项

在使用读锁、写锁时,都需要注意,读锁写锁属于行级锁

即 事务1 对商品A 获取写锁,和事务2 对商品B 获取写锁互相不会阻塞的。

需要我们注意的是:我们的SQL要合理使用索引,当我们的SQL全表扫描的时候,行级锁会变成表锁

使用EXPLAIN查看 SQL是否使用了索引,扫描了多少行。

1-2-4 用法

SELECT * FROM table_name WHERE ... FOR UPDATE

1-3 乐观锁(非数据库锁的逻辑锁) [推荐]

上述介绍的是行级锁,可以最大程度地支持并发处理(同时也带来了最大的锁开销)

乐观锁是一种逻辑锁,通过数据的版本号(vesion)的机制来实现,极大降低了数据库的性能开销。

(此处的版本号仅仅是对逻辑标记字段一种代称)


我们为表添加1个字段 version;

读取数据行时,将此版本号一同读出;

更新数据行时:

  • 修改此数据行所在的version: version = version + 1
  • 同时,将提交数据的 version 与数据库中对应记录的当前 version 进行比对:
    • 如果提交的数据版本号>数据库表当前版本号,则:予以更新;否则,认为是过期数据
update t_goods
set status=2,version=version+1
where id=#{id} and version < #{version}; // 更新前将 代码的#{version} 自增 -- SQL: 先查询(where), 后修改(update) 或者 update t_goods
set status=2,version=version+1
where id=#{id} and version = #{version}; // 更新前 代码的#{version} 不自增 -- SQL: 先查询(where), 后修改(update)

2 解决方案

2-1 方案1: [服务器端] synchronize(锁 方法或锁对象) + Java事务(锁定业务流程原子性) [推荐]

@Transnational
public synchronize generateOrderCode(){
...
}

2-2 方案2: 数据库【写锁/排他锁】(锁定多线程/进程的共享数据) + Java事务(锁定业务流程原子性)

维护数据库内1张存储最大值的表,多行

每一行为过去所使用的历史最大值的记录,最后插入的一行为当前最新最大值所在行。

Thread1 (Lock) 读取表内目标字段最大值Max前,加写锁;(此后,在Thread1未释放写锁前,其它Thread将读取目标字段最大值失败)
Thread1 (Query) 读取表内目标字段最大值Max;
Thread1 (Insert) 插入新增的值————Max=Max+1;
Thread1 (UnLock) 释放写锁;

2-3 方案3:数据库【乐观锁】(锁定多线程/进程的共享数据) + Java事务(锁定业务流程原子性)

类似 方案1,需要区别2点: 1)数据库的锁的变化;2) 维护数据库的行的意义/行数的变化

维护数据库内1张存储最大值的表,1行

字段等于目标编号业务属性的字段所在的行 即为当前最大值的所在行记录;其它行 为其它编号业务属性的字段所在的行。

2-4 方案4:Redis [推荐]

基于redis单线程的特点,生成全局唯一id,redis性能高,支持集群分片。

亦可实现分布式锁

[核心代码]

思路:日期(yyyyMMddHHmmss)+redis原子生成的数字(不足6位前面补0) / 类似:20191206221953000001

理论上6位后缀支持每秒最多生成999999个订单号,具体可以根据业务调整日期格式或日期后面的位数。

核心在于对象RedisAtomicLong (可以想下juc包下的AtomicLong),它对于同一个key会一直自增生成数字,这里我设置的key过期时间为20s,减轻redis的压力。

@Resource
private RedisTemplate<String,Serializable> redisTemplate; /**
* 获取有过期时间的自增长ID
* @param key
* @param expireTime
* @return
*/
public long generate(String key,Date expireTime) {
RedisAtomicLong counter = new RedisAtomicLong(key, redisTemplate.getConnectionFactory());
Long expire = counter.getExpire();
if(expire==-1){
counter.expireAt(expireTime);
}
return counter.incrementAndGet();
} public String generateOrderId() {//生成id为当前日期(yyMMddHHmmss)+6位(从000000开始不足位数补0)
LocalDateTime now = LocalDateTime.now();
String orderIdPrefix = getOrderIdPrefix(now);//生成yyyyMMddHHmmss
String orderId = orderIdPrefix+String.format("%1$06d", generate(orderIdPrefix,getExpireAtTime(now)));
return orderId;
} public static String getOrderIdPrefix(LocalDateTime now){
return now.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
} public Date getExpireAtTime(LocalDateTime now){
ZoneId zoneId = ZoneId.systemDefault();
LocalDateTime localDateTime = now.plusSeconds(20);
ZonedDateTime zdt = localDateTime.atZone(zoneId);
Date date = Date.from(zdt.toInstant());
return date;
}

Redis在集群环境中生成唯一ID:

https://blog.csdn.net/csujiangyu/article/details/52348003

高并发下使用Redis生成唯一id:

https://blog.csdn.net/heroguo007/article/details/78490278

Redis生成分布式自增ID:

https://blog.csdn.net/chengbinbbs/article/details/80437334

可读性好,不能太长。一般订单都是全数字的。可使用redis的incr命令生成订单号。

优点:可读性好,不会重复

缺点:需要搭建redis服务器

2-5 方案5:UUID 或 雪花算法

uuid生成全球唯一id,生成方式简单粗暴;常用于生成token令牌。

基于雪花算法snowflake 生成全局id,本地生成,没有网络开销,效率高,但是依赖机器时钟。

个人建议: 存储最新值到数据库前,检查此值是否已存储,防止极小概率事件发生

优点:简单,很难很难很难重复(概率极小),本地生成,没有网络开销,效率高;

缺点:长度较长;没有递增趋势性;可读性差;不易维护;

2-6 方案6:MySQL的ID自动增长

mysql自带自增生成id,oracle可以用序列生成id,但在数据库集群环境下,扩展性不好。

优点:不需要我们自己生成订单号,mysql会自动生成。

缺点:如果订单表数量太大时需要分库分表,此时订单号会重复。如果数据备份后再恢复,订单号会变。

2-7 方案7: 日期+随机数

采用毫秒+随机数。

缺点:仍然有重复的可能。不建议采用此方案。在没有更好的解决方案之前可以使用。

X 参考与推荐文献

[Java EE]小结:生成全局唯一编号的思路的更多相关文章

  1. 常见的生成全局唯一id有哪些?他们各有什么优缺点?

    分布式系统中全局唯一id是我们经常用到的,生成全局id方法由很多,我们选择的时候也比较纠结.每种方式都有各自的使用场景,如果我们熟悉各种方式及优缺点,使用的时候才会更方便.下面我们就一起来看一下常见的 ...

  2. 如何在高并发分布式系统中生成全局唯一Id

    月整理出来,有兴趣的园友可以关注下我的博客. 分享原由,最近公司用到,并且在找最合适的方案,希望大家多参与讨论和提出新方案.我和我的小伙伴们也讨论了这个主题,我受益匪浅啊…… 博文示例: 1.     ...

  3. 如何在高并发分布式系统中生成全局唯一Id(转)

    http://www.cnblogs.com/heyuquan/p/global-guid-identity-maxId.html 又一个多月没冒泡了,其实最近学了些东西,但是没有安排时间整理成博文, ...

  4. (转)如何在高并发分布式系统中生成全局唯一Id

    又一个多月没冒泡了,其实最近学了些东西,但是没有安排时间整理成博文,后续再奉上.最近还写了一个发邮件的组件以及性能测试请看 <NET开发邮件发送功能的全面教程(含邮件组件源码)> ,还弄了 ...

  5. 面试官:如何在分布式场景下生成全局唯一 ID?

    在分布式系统中,有一些场景需要使用全局唯一 ID ,可以和业务场景有关,比如支付流水号,也可以和业务场景无关,比如分库分表后需要有一个全局唯一 ID,或者用作事务版本号.分布式链路追踪等等,好的全局唯 ...

  6. Java订单号生成,唯一订单号(日均千万级别不重复)

    Java订单号生成,唯一订单号 相信大家都可以搜索到很多的订单的生成方式,不懂的直接百度.. 1.订单号需要具备以下几个特点. 1.1 全站唯一性. 1.2 最好可读性. 1.3 随机性,不能重复,同 ...

  7. 高并发分布式系统中生成全局唯一(订单号)Id js返回上一页并刷新、返回上一页、自动刷新页面 父页面操作嵌套iframe子页面的HTML标签元素 .net判断System.Data.DataRow中是否包含某列 .Net使用system.Security.Cryptography.RNGCryptoServiceProvider类与System.Random类生成随机数

    高并发分布式系统中生成全局唯一(订单号)Id   1.GUID数据因毫无规律可言造成索引效率低下,影响了系统的性能,那么通过组合的方式,保留GUID的10个字节,用另6个字节表示GUID生成的时间(D ...

  8. 生成全局唯一ID

    在实际业务处理中,有时需要生成全局唯一ID来区别同类型的不同事物,介绍一下几种方式及其C++实现 //获取全局唯一ID //server_id为服务的id,因当同一个服务部署在多个服务器上时,需要区别 ...

  9. 游戏服务器生成全局唯一ID的几种方法

    在服务器系统开发时,为了适应数据大并发的请求,我们往往需要对数据进行异步存储,特别是在做分布式系统时,这个时候就不能等待插入数据库返回了取自动id了,而是需要在插入数据库之前生成一个全局的唯一id,使 ...

  10. 高并发分布式系统中生成全局唯一Id汇总

    数据在分片时,典型的是分库分表,就有一个全局ID生成的问题.单纯的生成全局ID并不是什么难题,但是生成的ID通常要满足分片的一些要求:   1 不能有单点故障.   2 以时间为序,或者ID里包含时间 ...

随机推荐

  1. operations使用研究

    简介 operations支持在peer或者orderer运行过程中,提供基于restful接口的运维服务.包括健康检查.日志level管理.指标metrics接口等.首先利用test-network ...

  2. js中函数(方法)注释

    原文链接:https://blog.csdn.net/tianxintiandisheng/article/details/103764074 实例 /** * @function 处理表格的行 * ...

  3. 据库连接中useSSL

    在进行数据库连接时: jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/db?useSSL=false&am ...

  4. 在Win7的64位系统中安装最新版Eclipse或STS

    Eclipse版本为:eclipse-jee-2021-12-R-win32-x86_64 STS版本为:sts-4.13.1.RELEASE 这两个版本中都自带了17.0.1的jre插件,默认会使用 ...

  5. WIn环境基于vs编辑器,Qt应用申请管理员权限的解决方法(转载)

    原文章地址:https://blog.csdn.net/weixin_38416696/article/details/103879377 基本按照那个文章的操作.基本可以了. 唯一问题就是那个vs的 ...

  6. OpenJudge2811:熄灯问题(枚举)

    熄灯问题 有一个由按钮组成的矩阵,其中每行有6个按钮,共5行.每个按钮的位置上有一盏灯.当按下一个按钮后,该按钮以及周围位置(上边.下边.左边.右边)的灯都会改变一次.即,如果灯原来是点亮的,就会被熄 ...

  7. mybatis-plus获取对象的某一个属性list--->List<String>

    获取List<String> name new QueryWrapper<对象>().eq("**","**")).stream().m ...

  8. PHP 文件和文件夹操作

    文件夹操作 创建文件夹 mkdir(名称,权限,递归创建):创建文件 例如: #创建文件夹 mkdir('./aa') # 创建 aa 文件夹 mkdir('./aa/bb') # 在 aa 目录下创 ...

  9. 如何在 Apinto 实现 HTTP 与gRPC 的协议转换 (上)

    什么是 gRPC 像gRPC是由google开发的一个高性能.通用的开源 RPC 框架,主要面向移动应用开发且基于HTTP/2协议标准而设计,同时支持大多数流行的编程语言. gRPC基于 HTTP/2 ...

  10. 2021 技术展望丨AV1 在 RTC 应用实践中的现状与展望

    线上会议.在线教育.电商直播等多个场景的兴起,也使得实时互动技术从幕后走到台前,得到了更多人的关注.编解码.网络传输.计算机视觉等 RTE 相关的一系列技术也正焕发出更强的生命力.2021 年,在深度 ...