diff --git a/sql/rpl_parallel.cc b/sql/rpl_parallel.cc
index bad68b45871..0b2002300d0 100644
--- a/sql/rpl_parallel.cc
+++ b/sql/rpl_parallel.cc
@@ -326,6 +326,20 @@ register_wait_for_prior_event_group_commit(rpl_group_info *rgi,
 }
 
 
+static void
+register_wait_for_conflicting_event_group(rpl_group_info *rgi,
+                                          rpl_parallel_entry *entry)
+{
+  mysql_mutex_assert_owner(&entry->LOCK_parallel_entry);
+  if (rgi->conflicting_sub_id > entry->last_committed_sub_id)
+  {
+    wait_for_commit *waitee= &rgi->conflicting_rgi->commit_orderer;
+    if (waitee)
+      rgi->commit_orderer.register_wait_for_prior_commit(waitee);
+  }
+}
+
+
 /*
   Wait for prior transactions in the default domain_id to complete before
   starting to replicate in an alternate domain_id configured to replicate
@@ -988,6 +1002,7 @@ retry_event_group(rpl_group_info *rgi, rpl_parallel_thread *rpt,
   statistic_increment(slave_retried_transactions, LOCK_status);
   mysql_mutex_unlock(&rli->data_lock);
 
+  bool need_register_for_prior= false;
   for (;;)
   {
     mysql_mutex_lock(&entry->LOCK_parallel_entry);
@@ -997,7 +1012,17 @@ retry_event_group(rpl_group_info *rgi, rpl_parallel_thread *rpt,
 #endif
         rgi->gtid_sub_id < entry->stop_on_error_sub_id)
     {
-      register_wait_for_prior_event_group_commit(rgi, entry);
+      if (rli->mi->parallel_mode >= SLAVE_PARALLEL_AGGRESSIVE &&
+          rgi->conflicting_sub_id != 0 &&
+          rgi->conflicting_sub_id < rgi->wait_commit_sub_id &&
+          rgi->speculation == rpl_group_info::SPECULATE_OPTIMISTIC &&
+          retries + 2 < slave_trans_retries)
+      {
+        register_wait_for_conflicting_event_group(rgi, entry);
+        need_register_for_prior= true;
+      }
+      else
+        register_wait_for_prior_event_group_commit(rgi, entry);
     }
     else
     {
@@ -1017,12 +1042,13 @@ retry_event_group(rpl_group_info *rgi, rpl_parallel_thread *rpt,
       Let us wait for all prior transactions to complete before trying again.
       This way, we avoid repeatedly conflicting with and getting deadlock
       killed by the same earlier transaction.
+
+      In aggressive mode, we try to identify the transaction we conflicted
+      with (if within the same domain_id), and only wait until that one
+      committed.
     */
     if (!(err= thd->wait_for_prior_commit()))
-    {
-      rgi->speculation = rpl_group_info::SPECULATE_WAIT;
       break;
-    }
 
     convert_kill_to_deadlock_error(rgi);
     if (!has_temporary_error(thd))
@@ -1044,6 +1070,17 @@ retry_event_group(rpl_group_info *rgi, rpl_parallel_thread *rpt,
     });
   }
 
+  if (need_register_for_prior)
+  {
+    mysql_mutex_lock(&entry->LOCK_parallel_entry);
+    register_wait_for_prior_event_group_commit(rgi, entry);
+    mysql_mutex_unlock(&entry->LOCK_parallel_entry);
+    rgi->conflicting_sub_id= 0;
+    rgi->conflicting_rgi= NULL;
+  }
+  else
+    rgi->speculation = rpl_group_info::SPECULATE_WAIT;
+
   /*
     Let us clear any lingering deadlock kill one more time, here after
     wait_for_prior_commit() has completed. This should rule out any
diff --git a/sql/rpl_rli.cc b/sql/rpl_rli.cc
index 52eb255161c..7cb478e93a4 100644
--- a/sql/rpl_rli.cc
+++ b/sql/rpl_rli.cc
@@ -2140,6 +2140,8 @@ rpl_group_info::reinit(Relay_log_info *rli)
   last_event_start_time= 0;
   gtid_sub_id= 0;
   commit_id= 0;
+  conflicting_sub_id.store(0, std::memory_order_relaxed);
+  conflicting_rgi= NULL;
   gtid_pending= false;
   worker_error= 0;
   row_stmt_start_timestamp= 0;
diff --git a/sql/rpl_rli.h b/sql/rpl_rli.h
index b09cb8857f7..4551428782a 100644
--- a/sql/rpl_rli.h
+++ b/sql/rpl_rli.h
@@ -668,6 +668,17 @@ struct rpl_group_info
   uint64 gtid_sub_id;
   rpl_gtid current_gtid;
   uint64 commit_id;
+  /*
+    When non-zero and deadlock killed, this is the sub_id of the conflicting
+    transaction.
+
+    In aggressive mode, this is used to wait for the specific conflicting
+    transaction to commit before retrying (as opposed to all prior commits
+    in optimistic); this allows more parallelism at the cost of potentially
+    more conflicts.
+  */
+  std::atomic<uint64> conflicting_sub_id;
+  rpl_group_info *conflicting_rgi;
   /*
     This is used to keep transaction commit order.
     We will signal this when we commit, and can register it to wait for the
diff --git a/sql/sql_class.cc b/sql/sql_class.cc
index 8cc864fe10b..34466cc260c 100644
--- a/sql/sql_class.cc
+++ b/sql/sql_class.cc
@@ -5145,9 +5145,24 @@ thd_rpl_deadlock_check(MYSQL_THD thd, MYSQL_THD other_thd)
       order on the GTID sub_id, and rollback the later transaction to allow the
       earlier transaction to commit first.
     */
-    if (!rgi->gtid_sub_id || !other_rgi->gtid_sub_id ||
-        rgi->gtid_sub_id > other_rgi->gtid_sub_id)
+    uint64 sub_id= rgi->gtid_sub_id;
+    uint64 other_sub_id= other_rgi->gtid_sub_id;
+    if (!sub_id || !other_sub_id || sub_id > other_sub_id)
       return 0;
+    /*
+      Store the blocked transaction's sub_id and rgi into the blocking
+      transaction. This allows the blocking transaction to attempt to retry as
+      soon as the blocked transaction has committed (in aggressive mode), and
+      not wait for _all_ prior transactions to commit.
+
+      Only store the first conflicting sub_id, to ensure consistency with the
+      conflicting_rgi.
+    */
+    uint64 expected_sub_id= 0;
+    if (other_rgi->conflicting_sub_id.compare_exchange_strong(
+          expected_sub_id, sub_id,
+          std::memory_order_relaxed, std::memory_order_relaxed))
+      other_rgi->conflicting_rgi= rgi;
   }
   else
   {
