From the top of my head, here is something completely untested:
(function( $ ) {
    // Perform a cached ajax request
    // keyFn is a function that takes ajax options
    // and returns a suitable cache key
    jQuery.cachedAjax = function( keyFn ) {
        // Cache for jqXHR objects
        var cache = {};
        // Actual function to perform cached ajax calls
        return function( url, options ) {
            // If url is an object, simulate pre-1.5 signature
            if ( typeof url === "object" ) {
                options = url || {};
                url = undefined;
            // else, add the url into the options  
            } else if ( url ) {
                options = $.extend( {}, options || {} );
                options.url = url + "";
            }
            // Get the cache key
            var key = keyFn( options );
            // If not cached yet, cache it
            if ( !cache[ key ] ) {
                cache[ key ] = $.ajax( options );
            } else {
                // If already cached, ensure success, error
                // and complete callbacks are properly attached
                for( var cbType in { success: 1, error: 1, complete: 1 } ) {
                    cache[ key ][ cbType ]( options[ cbType ] );
                }
            }
            // Return the jqXHR for this key
            return cache[ key ];
        };
    };
})( jQuery ):
// Create a method that caches by url    
jQuery.ajaxCachedByURL = jQuery.cachedAjax(function( options ) {
    return options.url;
};
// Use the method just like ajax      
jQuery.cachedAjax( url, options ).then( successCallback, errorCallback );
The idea is to store the jqXHR in the cache, not the value. As soon as the request is initiated once then it doesn't matter if it has already finished or is running: fact is further calls to the cached ajax method will return the same jqXHR so concurrency is handled transparently.