meta_query check for meta value in key which holds an array of values

Revised Answer

( PS: I actually wanted to revise this answer long ago, but eventually I kept forgetting to do so. 🙂 )

So in this revised answer, the main point is the first one below, but I hope the rest also help you:

  1. What you’re trying to do is not going to be (easily) possible with the way the IDs are stored, which is serialized such as a:3:{i:0;i:1;i:1;i:10;i:2;i:11;} (for array( 1, 10, 11 )).

    What if the current user ID is 10 and in the meta value, there’s an i:10;i:9, i.e. array item keyed 10 with the value (a user ID) of 9 (i.e. $some_variable[10] = 9), but the ID 10 is not actually in the list?

  2. So because of the above, you might better off create a custom database table and store the user ID and post ID in their own column.

    And if you want, you can check a simplified example/demo on DB Fiddle.

  3. Or you could instead store each user ID in individual post meta named post_is_read. That way, filtering the posts would be easy — but the post meta table would end up being really huge with many, many rows…

    // Saving the meta:
    add_post_meta( 1, 'post_is_read', 10 );
    add_post_meta( 1, 'post_is_read', 11 );
    
    // Then when filtering the posts:
    $user_id = 10;
    $args = array(
        'meta_key'     => 'post_is_read',
        'meta_value'   => $user_id,
        'meta_compare' => '!=',
    );
    $query = new WP_Query( $args );
    
  4. Or you may use a serialized value, but store the user name/login and not the ID:

    // Store usernames and not IDs.
    $usernames = array( 'foo', 'user-bar', 'etc_etc' );
    
    update_post_meta( 123, 'post_is_read', $usernames );
    

    And then you can use the NOT LIKE comparison like so:

    $username="user-bar";
    $args = array(
        'meta_key'     => 'post_is_read',
        'meta_value'   => '"' . $username . '"', // search for exactly "<username>" (including the quotes)
        'meta_compare' => 'NOT LIKE',
    );
    $query = new WP_Query( $args );
    

Original Answer

Please check the revisions, but basically in my original answer, I was saying if you’d rather use the user IDs, then you could store them as a comma-separated list like 1,10,11 and then use the NOT REGEXP comparison:

// Saving the meta:
update_post_meta( 1, 'post_is_read', '1,10,11' );

// Then when filtering the posts:
$user_id = 10;
$args = array(
    'meta_key'     => 'post_is_read',
    'meta_value'   => "(^|,)$user_id(,|$)",
    'meta_compare' => 'NOT REGEXP',
);
$query = new WP_Query( $args );

And that did (and still does) work (with WordPress 5.6.1), except that it needs an extra parsing when retrieving the meta value, e.g.:

$ids = get_post_meta( 1, 'post_is_read', true );
$ids = wp_parse_id_list( $ids ); // convert to array