首页 > 数据库 >MySQL并行复制死锁源码解析

MySQL并行复制死锁源码解析

时间:2024-01-18 18:01:23浏览次数:39  
标签:slave 并行复制 cc 死锁 源码 cond mysql NULL event

最近一个MySQL 5.7.21备库告警当天的备份失败,登录上去看的时候发现前一天的备份任务还没有结束,通过查看日志发现无法备份成功的原因是一直无法获取FTWRL锁,登录MySQL查看会话状态发现其中几个复制worker一致处于异常状态,下发STOP SLAVE命令时命令也一直被卡住,当时的会话状态如下:

> show processlist;
+---------+-----------------+----------------------+------+---------+----------+---------------------------------------------+-----------------------------+
| Id      | User            | Host                 | db   | Command | Time     | State                                       | Info                        |
+---------+-----------------+----------------------+------+---------+----------+---------------------------------------------+-----------------------------+
|       1 | event_scheduler | localhost            | NULL | Daemon  | 47179666 | Waiting on empty queue                      | NULL                        |
|     101 | system user     |                      | NULL | Connect | 47085737 | Waiting for master to send event            | NULL                        |
|     102 | system user     |                      | NULL | Connect |   112325 | Waiting for dependent transaction to commit | NULL                        |
|     103 | system user     |                      | NULL | Connect |   116446 | Waiting for commit lock                     | NULL                        |
|     104 | system user     |                      | NULL | Connect |   116434 | Waiting for preceding transaction to commit | NULL                        |
|     105 | system user     |                      | NULL | Connect |   590564 | Waiting for an event from Coordinator       | NULL                        |
|     106 | system user     |                      | NULL | Connect |  2902104 | Waiting for an event from Coordinator       | NULL                        |
|     107 | system user     |                      | NULL | Connect |  7057083 | Waiting for an event from Coordinator       | NULL                        |
|     108 | system user     |                      | NULL | Connect | 10948598 | Waiting for an event from Coordinator       | NULL                        |
|     109 | system user     |                      | NULL | Connect | 47085737 | Waiting for an event from Coordinator       | NULL                        |
|     110 | system user     |                      | NULL | Connect | 47085737 | Waiting for an event from Coordinator       | NULL                        |
|     111 | system user     |                      | NULL | Connect | 47085737 | Waiting for an event from Coordinator       | NULL                        |
|     112 | system user     |                      | NULL | Connect | 47085737 | Waiting for an event from Coordinator       | NULL                        |
|     113 | system user     |                      | NULL | Connect | 47085737 | Waiting for an event from Coordinator       | NULL                        |
|     114 | system user     |                      | NULL | Connect | 47085737 | Waiting for an event from Coordinator       | NULL                        |
|     115 | system user     |                      | NULL | Connect | 47085737 | Waiting for an event from Coordinator       | NULL                        |
|     116 | system user     |                      | NULL | Connect | 47085737 | Waiting for an event from Coordinator       | NULL                        |
|     117 | system user     |                      | NULL | Connect | 47085737 | Waiting for an event from Coordinator       | NULL                        |
|     118 | system user     |                      | NULL | Connect | 47085737 | Waiting for an event from Coordinator       | NULL                        |
|     119 | system user     |                      | NULL | Connect | 47085737 | Waiting for an event from Coordinator       | NULL                        |
|     120 | system user     |                      | NULL | Connect | 47085737 | Waiting for an event from Coordinator       | NULL                        |
|     121 | system user     |                      | NULL | Connect | 47085737 | Waiting for an event from Coordinator       | NULL                        |
|     122 | system user     |                      | NULL | Connect | 47085737 | Waiting for an event from Coordinator       | NULL                        |
| 4409099 | backupxxx       | xxx.xxx.xxx.xx:48300 | NULL | Query   |   112222 | Waiting for commit lock                     | FLUSH TABLES WITH READ LOCK |
+---------+-----------------+----------------------+------+---------+----------+---------------------------------------------+-----------------------------+
根据当时的会话状态我们可以得知是发生了元数据锁等待,不过由于这个实例并没有开启元数据锁跟踪,所以并不能通过metadata_locks表来确认元数据锁的详细情况。不过有经验的DBA通过会话状态及故障现象应该能够猜测出来是发生了并行复制死锁,所以当时为了保留现场并快速恢复复制,就打了一个pstack便于后面详细分析问题,并通过kill掉当时的备份任务和会话来解除死锁状态,使复制能够正常运行下去。 接下来我们来分析一下当时的堆栈,还原一下当时MySQL的状态及问题发生的原因。 将堆栈归类,并将可能对我们分析问题有用的线程堆栈提取整理后如下:
#1. Waiting for dependent transaction to commit
pthread_cond_wait,native_cond_wait(thr_cond.h:140),my_cond_wait(thr_cond.h:140),inline_mysql_cond_wait(thr_cond.h:140),Mts_submode_logical_clock::wait_for_last_committed_trx(thr_cond.h:140),
Mts_submode_logical_clock::schedule_next_event(rpl_mts_submode.cc:657),schedule_next_event(log_event.cc:2768),Log_event::get_slave_worker(log_event.cc:2909),Log_event::apply_event(log_event.cc:3468),
apply_event_and_update_pos(rpl_slave.cc:4713),exec_relay_log_event(rpl_slave.cc:5240),handle_slave_sql(rpl_slave.cc:5240),pfs_spawn_thread(pfs.cc:2190),start_thread(libpthread.so.0),clone(libc.so.6) #2. 持有意向全局读锁(IX) Waiting for preceding transaction to commit pthread_cond_wait,native_cond_wait(thr_cond.h:140),my_cond_wait(thr_cond.h:140),inline_mysql_cond_wait(thr_cond.h:140),Commit_order_manager::wait_for_its_turn(thr_cond.h:140),
MYSQL_BIN_LOG::ordered_commit(binlog.cc:9245),MYSQL_BIN_LOG::commit(binlog.cc:8529),ha_commit_trans(handler.cc:1796),trans_commit(transaction.cc:239),Xid_log_event::do_commit(log_event.cc:7016),
Xid_apply_log_event::do_apply_event_worker(log_event.cc:7111),slave_worker_exec_job_group(rpl_rli_pdb.cc:2594),handle_slave_worker(rpl_slave.cc:6161),pfs_spawn_thread(pfs.cc:2190),
start_thread(libpthread.so.0),clone(libc.so.6) #3. STOP SLAVE 等待worker完成当前任务并退出 pthread_cond_timedwait,native_cond_timedwait(thr_cond.h:129),my_cond_timedwait(thr_cond.h:129),inline_mysql_cond_timedwait(thr_cond.h:129),terminate_slave_thread(thr_cond.h:129),
terminate_slave_thread(rpl_slave.cc:1665),terminate_slave_threads(rpl_slave.cc:1665),stop_slave(rpl_slave.cc:10172),stop_slave(rpl_slave.cc:618),stop_slave_cmd(rpl_slave.cc:759),
mysql_execute_command(sql_parse.cc:3438),mysql_parse(sql_parse.cc:5582),dispatch_command(sql_parse.cc:1458),do_command(sql_parse.cc:999),handle_connection(connection_handler_per_thread.cc:300),
pfs_spawn_thread(pfs.cc:2190),start_thread(libpthread.so.0),clone(libc.so.6) #4. 等待获取意向全局读锁(IX) Waiting for commit lock pthread_cond_timedwait,native_cond_timedwait(thr_cond.h:129),my_cond_timedwait(thr_cond.h:129),inline_mysql_cond_timedwait(thr_cond.h:129),MDL_wait::timed_wait(thr_cond.h:129),
MDL_context::acquire_lock(mdl.cc:3695),ha_commit_trans(handler.cc:1761),trans_commit(transaction.cc:239),Xid_log_event::do_commit(log_event.cc:7016),
Xid_apply_log_event::do_apply_event_worker(log_event.cc:7111),slave_worker_exec_job_group(rpl_rli_pdb.cc:2594),handle_slave_worker(rpl_slave.cc:6161),
pfs_spawn_thread(pfs.cc:2190),start_thread(libpthread.so.0),clone(libc.so.6) #5. 备份线程 等待获取全局读锁(X) Waiting for commit lock pthread_cond_timedwait,native_cond_timedwait(thr_cond.h:129),my_cond_timedwait(thr_cond.h:129),inline_mysql_cond_timedwait(thr_cond.h:129),MDL_wait::timed_wait(thr_cond.h:129),
MDL_context::acquire_lock(mdl.cc:3695),Global_read_lock::make_global_read_lock_block_commit(lock.cc:1196),reload_acl_and_cache(sql_reload.cc:234),mysql_execute_command(sql_parse.cc:4170),
mysql_parse(sql_parse.cc:5582),dispatch_command(sql_parse.cc:1458),do_command(sql_parse.cc:999),handle_connection(connection_handler_per_thread.cc:300),pfs_spawn_thread(pfs.cc:2190),
start_thread(libpthread.so.0),clone(libc.so.6)
我们给这5个线程编了号并大致说明了一下线程的状态,接下来我们详细分析一下每个线程的状态。 先看第一个线程#1所处的函数为Mts_submode_logical_clock::wait_for_last_committed_trx:
    thd->ENTER_COND(&rli->logical_clock_cond, &rli->mts_gaq_LOCK,
                    &stage_worker_waiting_for_commit_parent, &old_stage);
    do
    {
      mysql_cond_wait(&rli->logical_clock_cond, &rli->mts_gaq_LOCK);
    }
    while ((!rli->info_thd->killed && !is_error) &&
           !clock_leq(last_committed_arg, estimate_lwm_timestamp()));
其所处的stage为“Waiting for dependent transaction to commit”。目前其停留在do...while循环中,其正常需要满足两个条件才能跳出循环,第一:last_committed <= lwm,即满足事务满足并行回放的条件;第二:接收到logical_clock_cond条件变量,logical_clock_cond条件变量为worker回放完事务后会判断当前是否还有事务等待,如果存在则发送logical_clock_cond条件变量唤醒等待的coordinator线程。 线程#2所处的函数为Commit_order_manager::wait_for_its_turn,该函数被函数MYSQL_BIN_LOG::ordered_commit调用,其逻辑处于两阶段提交的FLSH STAGE之前,用于判断从库提交事务时是否需要按照和主库一样的顺序进行提交(slave_preserve_commit_order)。线程所处的位置为:
    while (queue_front() != worker->id)
    {
      if (unlikely(worker->found_order_commit_deadlock()))
      {
        mysql_mutex_unlock(&m_mutex);
        thd->EXIT_COND(&old_stage);
        DBUG_RETURN(true);
      }
      mysql_cond_wait(cond, &m_mutex);
    }
其也处在一个while循环中,其跳出循环的条件为只有当前worker是worker队列的头部时才可以。worker应用事务时,不仅会把事务分配给worker,还会将worker注册到工作队列中,其入队的堆栈如下:
#0  Commit_order_manager::queue_push (this=0x7fb1a8007f30, index=0) at /soft/mysql-5.7.21/sql/rpl_slave_commit_order_manager.h:123
#1  0x000000000183ba79 in Commit_order_manager::register_trx (this=0x7fb1a8007f30, worker=0x7fb1a8022a60) at /soft/mysql-5.7.21/sql/rpl_slave_commit_order_manager.cc:49
#2  0x000000000183adb9 in Mts_submode_logical_clock::get_least_occupied_worker (this=0x7fb1a8007ee0, rli=0xb340c70, ws=0xb343fa8, ev=0x7fb1a8691ce0)
    at /soft/mysql-5.7.21/sql/rpl_mts_submode.cc:916
#3  0x00000000017978b9 in Log_event::get_slave_worker (this=0x7fb1a8691ce0, rli=0xb340c70) at /soft/mysql-5.7.21/sql/log_event.cc:2954
#4  0x0000000001798f77 in Log_event::apply_event (this=0x7fb1a8691ce0, rli=0xb340c70) at /soft/mysql-5.7.21/sql/log_event.cc:3468
#5  0x000000000180b5a4 in apply_event_and_update_pos (ptr_ev=0x7fb255aca8c0, thd=0x7fb1a8000950, rli=0xb340c70) at /soft/mysql-5.7.21/sql/rpl_slave.cc:4713
#6  0x000000000180cc3c in exec_relay_log_event (thd=0x7fb1a8000950, rli=0xb340c70) at /soft/mysql-5.7.21/sql/rpl_slave.cc:5240
#7  0x00000000018131e3 in handle_slave_sql (arg=0xb2a31b0) at /soft/mysql-5.7.21/sql/rpl_slave.cc:7377
#8  0x0000000001ce0555 in pfs_spawn_thread (arg=0x7fb1b00ddab0) at /soft/mysql-5.7.21/storage/perfschema/pfs.cc:2190
#9  0x00007fb2f7745ea5 in start_thread () from /lib64/libpthread.so.0
#10 0x00007fb2f660bb0d in clone () from /lib64/libc.so.6
其出队的堆栈如下:
#0  Commit_order_manager::queue_pop (this=0x7fb1a8007f30) at /soft/mysql-5.7.21/sql/rpl_slave_commit_order_manager.h:111
#1  0x000000000183bee9 in Commit_order_manager::unregister_trx (this=0x7fb1a8007f30, worker=0x7fb1a8022a60) at /soft/mysql-5.7.21/sql/rpl_slave_commit_order_manager.cc:126
#2  0x00000000017cd0a4 in Stage_manager::enroll_for (this=0x2d1c418 <mysql_bin_log+4568>, stage=Stage_manager::FLUSH_STAGE, thd=0x7fb1ac000950, stage_mutex=0x0)
    at /soft/mysql-5.7.21/sql/binlog.cc:2000
#3  0x00000000017dd652 in MYSQL_BIN_LOG::change_stage (this=0x2d1b240 <mysql_bin_log>, thd=0x7fb1ac000950, stage=Stage_manager::FLUSH_STAGE, queue=0x7fb1ac000950, leave_mutex=0x0,
    enter_mutex=0x2d1b248 <mysql_bin_log+8>) at /soft/mysql-5.7.21/sql/binlog.cc:8854
#4  0x00000000017de2f6 in MYSQL_BIN_LOG::ordered_commit (this=0x2d1b240 <mysql_bin_log>, thd=0x7fb1ac000950, all=false, skip_commit=false) at /soft/mysql-5.7.21/sql/binlog.cc:9251
#5  0x00000000017dcbbe in MYSQL_BIN_LOG::commit (this=0x2d1b240 <mysql_bin_log>, thd=0x7fb1ac000950, all=false) at /soft/mysql-5.7.21/sql/binlog.cc:8529
#6  0x0000000001632b78 in trans_commit_stmt (thd=0x7fb1ac000950) at /soft/mysql-5.7.21/sql/transaction.cc:463
#7  0x000000000153895a in mysql_execute_command (thd=0x7fb1ac000950, first_level=true) at /soft/mysql-5.7.21/sql/sql_parse.cc:5004
#8  0x0000000001539f91 in mysql_parse (thd=0x7fb1ac000950, parser_state=0x7fb255a88a80) at /soft/mysql-5.7.21/sql/sql_parse.cc:5582
#9  0x000000000179cf62 in Query_log_event::do_apply_event (this=0x7fb1a8691ce0, rli=0x7fb1a8022a60, query_arg=0x7fb1a869f2d7 "create table t(id int)", q_len_arg=22)
    at /soft/mysql-5.7.21/sql/log_event.cc:4711
#10 0x000000000179be51 in Query_log_event::do_apply_event (this=0x7fb1a8691ce0, rli=0x7fb1a8022a60) at /soft/mysql-5.7.21/sql/log_event.cc:4431
#11 0x00000000017bf204 in Log_event::do_apply_event_worker (this=0x7fb1a8691ce0, w=0x7fb1a8022a60) at /soft/mysql-5.7.21/sql/log_event.cc:758
#12 0x0000000001834bee in Slave_worker::slave_worker_exec_event (this=0x7fb1a8022a60, ev=0x7fb1a8691ce0) at /soft/mysql-5.7.21/sql/rpl_rli_pdb.cc:1833
#13 0x0000000001836bb8 in slave_worker_exec_job_group (worker=0x7fb1a8022a60, rli=0xb340c70) at /soft/mysql-5.7.21/sql/rpl_rli_pdb.cc:2594
#14 0x000000000180f9aa in handle_slave_worker (arg=0x7fb1a8022a60) at /soft/mysql-5.7.21/sql/rpl_slave.cc:6161
#15 0x0000000001ce0555 in pfs_spawn_thread (arg=0x7fb1a8027000) at /soft/mysql-5.7.21/storage/perfschema/pfs.cc:2190
#16 0x00007fb2f7745ea5 in start_thread () from /lib64/libpthread.so.0
即事务在ordered_commit中当在切换到FLUSH STAGE的过程中会进行出队,因为此时已经可以通过FLSUH STAGE的&LOCK_LOG互斥量来保证该事务组一定会比后续的事务组先提交。 线程#3是我们下发STOP SLAVE命令的线程,其处于terminate_slave_thread函数中,其正在退出SQL线程:
  while (*slave_running)                        // Should always be true
  {
    DBUG_PRINT("loop", ("killing slave thread"));

    mysql_mutex_lock(&thd->LOCK_thd_data);
    /*
      Error codes from pthread_kill are:
      EINVAL: invalid signal number (can't happen)
      ESRCH: thread already killed (can happen, should be ignored)
    */
#ifndef _WIN32
    int err MY_ATTRIBUTE((unused))= pthread_kill(thd->real_id, SIGUSR1);
    DBUG_ASSERT(err != EINVAL);
#endif
    thd->awake(THD::NOT_KILLED);
    mysql_mutex_unlock(&thd->LOCK_thd_data);

    /*
      There is a small chance that slave thread might miss the first
      alarm. To protect againts it, resend the signal until it reacts
    */
    struct timespec abstime;
    set_timespec(&abstime,2);
#ifndef DBUG_OFF
    int error=
#endif
      mysql_cond_timedwait(term_cond, term_lock, &abstime);    // 在这等待
    if ((*stop_wait_timeout) >= 2)
      (*stop_wait_timeout)= (*stop_wait_timeout) - 2;
    else if (*slave_running)
    {
      if (need_lock_term)
        mysql_mutex_unlock(term_lock);
      DBUG_RETURN (1);
    }
    DBUG_ASSERT(error == ETIMEDOUT || error == 0);
  }

  DBUG_ASSERT(*slave_running == 0);

  if (need_lock_term)
    mysql_mutex_unlock(term_lock);
  DBUG_RETURN(0);
}
其也是处于一个while循环中,循环判断接收线程退出条件变量term_cond,退出条件变量的触发只有当worker线程发现了mi->abort_slave = true退出标记并完成当前的event_group之后才会退出(详细分析可见https://www.cnblogs.com/wagaga/p/17286379.html),所以如果当前worker线程存在未完成的event,用户的STOP SLAVE命令就无法执行完成。 线程#4正处于ha_commit_trans函数中,正在等待获取commit lock:
if (rw_trans && !ignore_global_read_lock)
    {
      /*
        Acquire a metadata lock which will ensure that COMMIT is blocked
        by an active FLUSH TABLES WITH READ LOCK (and vice versa:
        COMMIT in progress blocks FTWRL).

        We allow the owner of FTWRL to COMMIT; we assume that it knows
        what it does.
      */
      MDL_REQUEST_INIT(&mdl_request,
                       MDL_key::COMMIT, "", "", MDL_INTENTION_EXCLUSIVE,
                       MDL_EXPLICIT);

      DBUG_PRINT("debug", ("Acquire MDL commit lock"));
      if (thd->mdl_context.acquire_lock(&mdl_request,
                                        thd->variables.lock_wait_timeout))
      {
        ha_rollback_trans(thd, all);
        DBUG_RETURN(1);
      }
      release_mdl= true;

      DEBUG_SYNC(thd, "ha_commit_trans_after_acquire_commit_lock");
}
即事务在提交之前需要先获取意向独占元数据锁(commit lock),如果当前实例存在全局读锁,则事务就会被阻塞,不能提交。 此时让我们再回头看线程#2,发现其堆栈中间也有ha_commit_trans函数,说明其已经获取到了意向独占元数据锁。 最后看线程#5,其很简单,就是由于xtrabackup备份而触发的需要获取全局读锁,且正处于等待状态。 此时,我们把待分析的线程都分析了一遍,这时候我们就可以理清他们之间的关系了:
  1. 线程#1可能在等待线程#2,也可能在等待线程#4,不过不重要,因为它也只是受害者
  2. 线程#2持有元数据意向独占锁(IX),并等待线程#4的提交
  3. 线程#3等待线程#2和#4完成后停止复制线程,不重要,受害者
  4. 线程#4想要获取意向独占锁(IX),但是却被阻塞,所以在等待线程#5
  5. 线程#5想要获取独占锁(X),但是却被阻塞,所以在等待线程#2
此时死锁就产生了,线程#2由于slave_preserve_commit_order的原因需要等待线程#4,而线程#4等待线程#5,线程#5又等待线程#2。所以我们在最开始kill掉备份会话(线程#5)就可以解决该问题了。

标签:slave,并行复制,cc,死锁,源码,cond,mysql,NULL,event
From: https://www.cnblogs.com/wagaga/p/17973101

相关文章

  • SpringSecurity-认证流程源码级详解
    自定义用户认证逻辑处理用户信息获取逻辑:UserDetailsService处理用户校验逻辑:UserDetails处理密码加密解密:PasswordEncoder认证处理流程以表单认证为例:从发起认证请求到认证过滤器,接着认证成功后,响应从认证过滤器返回的整个过程。SpringSecurity做了什么,设计到了哪些类?他......
  • 【深入挖掘Java技术】「源码原理体系」盲点问题解析之HashMap工作原理全揭秘(下)
    承接上文在阅读了上篇文章《【深入挖掘Java技术】「源码原理体系」盲点问题解析之HashMap工作原理全揭秘(上)》之后,相信您对HashMap的基本原理和基础结构已经有了初步的认识。接下来,我们将进一步深入探索HashMap的源码,揭示其深层次的技术细节。通过这次解析,您将更深入地理解HashMap的......
  • Android RECYCLEVIEWscrollbarDefaultDelayBeforeFade 源码
    AndroidRecyclerViewscrollbarDefaultDelayBeforeFade源码实现简介在Android开发中,RecyclerView是一个常用的控件,用于展示大数据集合的列表或网格。RecyclerView提供了一些自定义的属性和方法,方便开发者根据需求进行定制。其中一个常用的属性是scrollbarDefaultDelayBefo......
  • LinkedHashMao & LinkedHashSet源码阅读
    目录简介模型代码分析成员变量方法补充-LinkedHashSet总结参考链接本人的源码阅读主要聚焦于类的使用场景,一般只在java层面进行分析,没有深入到一些native方法的实现。并且由于知识储备不完整,很可能出现疏漏甚至是谬误,欢迎指出共同学习本文基于corretto-17.0.9源码,参考本文时......
  • 掌上医院预约挂号源码,uni-app+.net公众号、支付宝小程序预约挂号平台
    线上预约挂号系统构建了医院和患者的连接,通过改善患者院内的就医服务流程,以公众号、支付宝小程序为服务入口,为居民提供导诊、预约、支付、报告查询等线上线下一体化的就医服务,缩短患者就诊环节,提高医疗机构服务效率。●与医院HIS系统深度融合,实现诊疗业务及数据线上线下的双向传......
  • SpringSecurity系列,第四章:源码分析
    源码分析SpringSecurity的核心功能即为:认证(Authentication)、授权(Authorization)一、概览1、在SpringSecurity中,用户的认证信息主要由Authentication的实现类来保存,Authentication接口定义如下:【保存用户认证信息】publicinterfaceAuthenticationextendsPrin......
  • HashMap & HashSet源码阅读
    目录简介模型代码分析成员变量方法参考链接本人的源码阅读主要聚焦于类的使用场景,一般只在java层面进行分析,没有深入到一些native方法的实现。并且由于知识储备不完整,很可能出现疏漏甚至是谬误,欢迎指出共同学习本文基于corretto-17.0.9源码,参考本文时请打开相应的源码对照,否则......
  • 【送酒小程序系统源码】/花店送花系统/蛋糕店系统/奶茶店系统源码
    前端uniapp+后端thinkphp+数据库mysql多门店外卖餐饮点餐系统预约点餐匹配附近店铺 堂食外卖带走  菜品管理.根据用户的位置匹配附近饭店 点餐后,可以在线等叫号餐时输入手机号并支付后,可以支持外支持多规格、备注等快捷功能,以吸多多门店管理 数据概览支持微信小程序 ......
  • 尚无忧【无人共享空间 saas 系统源码】无人共享麻将室系统源码共享自习室系统源码,共享
    可saas多开,非常方便,大大降低了上线成本UNIAPP+thinkphp+mysql独立开源!1、定位功能:可定位附近是否有店2、能通过关键字搜索现有的店铺3、个性轮播图展示,系统公告消息提醒4、个性化功能展示,智能排序,距离、价格排序5、现有店铺清单展示,订房可查看房间单价,根据日期、时间端订房,选择时......
  • PDF转图片-itextpdf-java源码
    提供PDF文件转图片的工具类。电子签章过程中存在着在网页上对签署文件进行预览、指定签署位置、文件签署等操作,由于图片在浏览器上的兼容性和友好性优于PDF文件,所以一般在网页上进行电子签章时,会先将PDF文件转换成图片,展示给用户。用户在页面上确定好签署位置,并进行签署时,后端服......