Search code examples
pythonredistransactionspython-multithreadingsortedset

Transaction on a redis sorted set doesn't appear atomic


I'm encountering some strange behavior in a pipelined/transaction in Redis that makes me question that the code is actually being executed in a transaction:

class RedisThread:

    KEY = "threading_test"

    def __init__(self, id:int):
        self._redis = Redis(host="localhost", port=6379, db=0)
        self._id    = id
        self._redis.delete(self.KEY)

    def __call__(self, *args, **kwargs):
        results = []
        for i in range(3):
            # Transaction
            pipe = self._redis.pipeline(transaction=True)

            # ZADD current time as score
            pipe.zadd(self.KEY, {f"{self._id}-{i}": time.time()})

            # ZRANK
            pipe.zrank(self.KEY, f"{self._id}-{i}")

            # Commit and get result of ZRANK
            results.append(str(pipe.execute()[1]))

        print(", ".join(results))


    threads = [
        threading.Thread(target=RedisThread(1)),
        threading.Thread(target=RedisThread(2)),
        threading.Thread(target=RedisThread(3)),
        threading.Thread(target=RedisThread(4)),
        threading.Thread(target=RedisThread(5)),
    ]

    for t in threads:
        t.start()

    for t in threads:
        t.join()

When I run this code, this is the result:

1, 5, 9
3, 6, 10
0, 3, 13
4, 11, 12
1, 4, 13

Notice that there are duplicate values between threads. Since I'm doing a ZRANK, and the values I'm adding (via ZADD) to the set are time based (and thus always increasing in values), I should not be seeing any duplicates, yet there are duplicates...


Solution

  • Redis transaction is atomic. The problem is that you use time() as score.

    Since the programs runs fast, so time() might return the same result for multiple calls. In other words, you are setting members in sorted set with the same score.

    If members have the same score, they are sorted by lexicographical order.

    Now, let's see why you got duplicate values:

    thread1: ZADD key same-time 1-0
    thread1: ZRANK key 1-0 ----------> 0
    
    thread2: ZADD key same-time 2-0
    thread2: ZRANK key 2-0 ----------> 1
    
    thread1: ZADD key same-time 1-1
    thread1: ZRANK key 1-1 ----------> 1
    

    The second transaction of thread1 executed after the first transaction of thread2, and 1-1 is less than 2-0 in lexicographical order. So when the second transaction of thread1 executed, 1-1 will be inserted before 2-0, and that's why they both get the rank: 1.