You have a problem in your solution design. This duplicate creation is a very common pattern you run into when setting up e.g. Double Opt In processes. It will be beyond the scope of this answer to give you the full path, but let me outline the faultlines and ways around the issue. You'll still have to do your own research about the specifics mentioned, and there might be ramifications beyond simple coding, as there is not a simple "fix" to this.
With that said.
Scenario 1 is not surprising and nothing can be changed here, it's how Contacts are created when sending to a Sendable Data Extension using Email Studio Send Flow.
The problem, and the solution, has to be in the way the Triggered send (scenario 2) is set up. Here, there is something you can do.
3 is the expected logical consequence of 1 and 2 - when both 1 and 2 happen, you create duplicates.
Why? The system compares against existing Subscriber Keys on All Subscribers what the scenarios put in: A CRM Subscriber Key in scenario 1 (finds an existing record where SubscriberKey matches, does not create a duplicate), and Email in scenario 2 >> (finds no existing SubscriberKey that matches, creates duplicate).
With Scenario 1 out of our reach, you have to address scenario 2.
A triggered send can be freely defined with regards to what it uses as Subscriber Key. It will ALWAYS match on subscriber key though, you cannot tell it to "match on email instead" if you have subscriberKey implemented (as anyone with CRM connected does).
In your implementation, Email was used as Subscriber Key for the triggered send.
This is the root cause and this is what you have to tackle either by getting the proper CRM ID - duplicate checked - before the triggered send is executed (synchronously) or some time after the sendout, but then clean up the duplicate (asynchronously).
Synchronous approach: before executing the triggered send, perform a duplicate check on the email field on the SFSC Contact Object using AMPScript RetrieveSingleSalesforceObject() and if necessary, create a new Contact using CreateSalesforceObject(). Then use the resulting ID of the retrieve or the create for the Triggered Send.
You simply get the correct ID before you match with All Subscribers.
However, there are some pretty big downsides to this - less so, if you know that all your sends go out to already existing Contacts, but still:
These AMPScript roundtrips take a few seconds.
You create Contacts in CRM without validating anything, potentially directly from the web.
This also means: you will also create these contacts in SFSC if the subsequent email sendout fails (auto suppression configuration blocks it, list detective blocks it...). These are plain trash and will probably have to be cleaned (= cross cloud deletion, in two interconnected systems). Which leads to:
You are involving two systems (SFSC and SFMC) in real time for something pretty simple - an email send. More systems = more things that can fail or that you have to worry about.
Asynchronous approach: Allow the duplicate temporarily, but make it easily recognizable, and auto-delete it after a certain period.
Before executing the triggered send, change the Subscriber Key from Email to a temporary ID that you create randomly, but that contains something you can re-identify as belonging to your process across all addresses that come in. I usually use a prefixed GUID, created like so in AMPscript:
SET @myId = Concat("myprocess_",GUID())
Then, configure the triggered send to use @myId instead of Email.
Now, you still have a duplicate once the Triggered Send goes off:
All Subscribers List
Subscriberkey / Email
myprocess_1234-56-78-90 (prefixed GUID) / firstname.lastname@example.org
003XXXXXXXXXXXX (CRM ID) / email@example.com
But: you can now easily spot the problematic records, as they all start with
myprocess_, and at any later time find all the relevant records and process them, plus inject them into a Contact Deletion process via SQL. Contact Deletion: Trying to automate contact deletion with SSJS
Of course you can also isolate them by this pattern from other processes if you cannot perform a proper cleanup in time.
At any rate, the SQL:
WHERE subscriberKey LIKE 'myprocess_%'
AND /* some date based logic after which period you want to delete them */
finds all the duplicates thusly created in one go for asynchronous processing.
Now of course, Contact Deletion will delete them, losing their sendout statistics etc, and regardless of whether you have them as another Contact or not. You could backup the necessary data, alter the SQL to only select them if email occurs more than once, if they match some other criteria ... it depends on your expectation and processes here.
So with the asynchronous approach you'll have temporary, clearly marked duplicates. They will exist only in SFMC at first, making the follow up processing easier.
You'd still have to implement a duplicate matching logic and it will be similarly complicated (The AMPscript methods mentioned are pretty finicky, and usage of SFSC Leads makes this even more complicated, same for Person Accounts to a lesser degree.)
You'd still have to get new records into your CRM to give them a proper ID using AMPScript or the API (or even Journey Builder), but:
in some scenarios, now you are out of the real-time problematic and that opens the door also for using SQL
you can keep your CRM cleaner, as you can e.g. respect when emails are validated negatively by SFMC (and don't get sent), and make those never reach CRM.
You will need a contact deletion in both scenarios, but where the synchronous approach creates "random trash" that is hard to tell apart from legit data (think unvalidated emails) in two systems, the asynchronous process creates "clearly marked trash" with the temporary IDs only in one system, so the handling gets way easier in the asynchronous approach if you manage contact deletion and potential data backups correctly.