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

  • 相关阅读:
    background-size ie8及以下不兼容的解决方案
    前端
    JavaScript ES(6-11)
    前端工程化
    前端安全漏洞与防范
    Vue源码思维导图
    项目流程总结
    typescript版数据结构与算法库
    tsconfig.json各项配置注解
    Sql server动态加载存储过程--分页
  • 原文地址:https://www.cnblogs.com/johnnyzen/p/13915432.html
Copyright © 2011-2022 走看看