zoukankan      html  css  js  c++  java
  • [Java EE]小结:生成全局唯一编号的思路

    并发是一个让人很头疼的问题,通常会在服务端数据库端做处理,保证在并发下数据的准确性。
    为此,简要讨论一下,如何通过解决全局生成唯一编号的并发问题。

    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 参考与推荐文献

  • 相关阅读:
    centos 编码问题 编码转换 cd到对应目录 执行 中文解压
    centos 编码问题 编码转换 cd到对应目录 执行 中文解压
    centos 编码问题 编码转换 cd到对应目录 执行 中文解压
    Android MVP 十分钟入门!
    Android MVP 十分钟入门!
    Android MVP 十分钟入门!
    Android MVP 十分钟入门!
    mysql备份及恢复
    mysql备份及恢复
    mysql备份及恢复
  • 原文地址:https://www.cnblogs.com/johnnyzen/p/13915432.html
Copyright © 2011-2022 走看看