一、场景
Redis 分布式锁主要解决数据不一致性和并发性问题,通过确保同一时间只有一个进程或线程可以对资源进行修改,避免了竞态条件和资源争用,从而保证系统的一致性和稳定性。根据具体的应用场景,可以选择不同的锁实现方式来解决这些问题。
例如一个典型的商品购买。Redis中的商品缓存数量为100,第一个请求购买后,还为更新缓存,第二个请求就已经过来,导致的超卖问题。
import time
import redis
from tornado import ioloop
from tornado.web import RequestHandler, Application
REDIS_HOST = 'localhost'
REDIS_PORT = 6379
REDIS_DB = 0
REDIS_PASSWORD = '123456'
class MainHandler(RequestHandler):
def initialize(self, redis_conn):
self.redis_conn = redis_conn
async def get(self):
hash_name = 'mysite'
if self.redis_conn.exists(hash_name):
redis_data = self.redis_conn.hget(hash_name, 'num')
if redis_data:
self.write(redis_data)
time.sleep(3)#模拟耗时
print("缓存修改")
self.redis_conn.hdel(hash_name, 'num')
self.redis_conn.hset(hash_name, 'num', str(int(redis_data) - 1))
return
else:
self.write("缓存未命中")
else:
self.write("哈希表不存在")
if __name__ == "__main__":
try:
redis_conn = redis.StrictRedis(
host=REDIS_HOST,
port=REDIS_PORT,
db=REDIS_DB,
decode_responses=True,
password=REDIS_PASSWORD
)
print("Redis 连接成功!")
except redis.exceptions.ConnectionError as e:
print(f"Redis连接失败: {e}")
exit(1)
app = Application([
(r"/?", MainHandler,dict(redis_conn=redis_conn)),
],debug=True)
app.listen(7777)
print(f"服务启动,端口为7777")
ioloop.IOLoop.current().start()
这是第一个服务,如果另一个服务没有这里的耗时操作,那么就会出现问题,例如商品数据只有1,被购买后没有更新,导致其他服务仍然可以购买。
二、解决方案
def acquire_lock(self, lock_key, expiration=10):
'''
获取锁
'''
lock_value = str(uuid.uuid4())
success = self.redis_conn.set(lock_key, lock_value, nx=True, ex=expiration)
return success, lock_value
def release_lock(self, lock_key, lock_value):
'''
释放锁
'''
script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
self.redis_conn.eval(script, 1, lock_key, lock_value)
async def get(self):
success, lock_value = self.acquire_lock(lock_key='lock_key')
if not success:
self.write("请求失败: 锁不可用")
return
try:
print("请求成功")
hash_name = 'mysite'
if self.redis_conn.exists(hash_name):
redis_data = self.redis_conn.hget(hash_name, 'num')
if redis_data:
self.write(redis_data)
self.redis_conn.hdel(hash_name, 'num')
self.redis_conn.hset(hash_name, 'num', str(int(redis_data) - 1))
else:
self.write("缓存未命中")
else:
self.write("哈希表不存在")
finally:
self.release_lock(lock_key='lock_key', lock_value=lock_value)
通过SETNX + EXPIRE锁机制避免了数据不一致性的问题,这样在多个客户端同时访问Redis数据库时,就可以避免数据不一致性的问题。解决超卖问题。
不知道大家有没有发现上面代码的Value值,是没有使用的。可以这里有一个更好的解决方案,就是将当前的时间和锁的生命周期时长存储在Value中,这样其他客户端可以通过获取Value的值判断锁是否过期,避免没有设置锁的生命周期问题,获取其他异常状态导致设置锁生命周期失败。相当于二次检验,更加保险。
def acquire_lock(self, lock_key, expiration=10):
'''
获取锁
'''
expiration_time = datetime.now() + timedelta(seconds=expiration)#获取锁的过期时间
lock_value = expiration_time.isoformat()
success = self.redis_conn.set(lock_key, lock_value, nx=True, ex=expiration)
if success:
return True, lock_value
else:
lock_value = self.redis_conn.get(lock_key)#二次判断
if lock_value:
stored_expiration_time = datetime.fromisoformat(lock_value)
if datetime.now() > stored_expiration_time:
self.release_lock(lock_key, lock_value)
return self.acquire_lock(lock_key, expiration)
else:
return False, None
else:
return False, None
三、Redlock 算法
假设你运营一个大型电商平台,该平台采用微服务架构,每个微服务都有自己的Redis实例。多个用户可能会同时尝试购买同一件商品,因此需要一种机制来确保库存数据的一致性,防止超卖问题。
在高并发的情况下,多台服务器同时处理订单请求。为了确保数据一致性,你需要使用分布式锁。挑战在于,当某些服务宕机或网络出现问题时,可能会导致锁无法正确释放,进而导致数据不一致的问题。
Redlock算法通过在多个独立的Redis实例上获取锁,确保在任何时刻只有一个客户端能够成功获取锁并进行操作,从而避免超卖和数据不一致的问题。
具体流程为:
记录当前的时间–>在多个Redsi实例上获取锁–>计算获取锁的总时间
–>如果大部分锁获取成功并且总时间小于锁的过期时间,则认为获取锁成功
–>获取相关数据–>释放锁
下载redlock-py模块,当然也可以自己简单的实现。
pip install redlock-py
import redis
from tornado import ioloop
from tornado.web import RequestHandler, Application
from redlock import Redlock
class MainHandler(RequestHandler):
def initialize(self,redis_nodes):
self.redis_conn = redis.StrictRedis(host='localhost', port=6379, db=0)
self.dlm = Redlock(redis_nodes)
async def get(self):
lock_key = "lock_key"
expiration = 10
lock = self.dlm.lock(lock_key, expiration * 1000)
if lock:
try:
hash_name = 'mysite'
if self.redis_conn.exists(hash_name):
redis_data = self.redis_conn.hget(hash_name, 'num')
if redis_data:
self.write(redis_data)
self.redis_conn.hdel(hash_name, 'num')
self.redis_conn.hset(hash_name, 'num', str(int(redis_data) - 1))
else:
self.write("缓存未命中")
else:
self.write("哈希表不存在")
finally:
self.dlm.unlock(lock)
else:
self.write("请求失败:无法获取锁")
if __name__ == "__main__":
redis_nodes = [
{"host": "localhost", "port": 6379},
{"host": "localhost", "port": 6380},
{"host": "localhost", "port": 6381},
{"host": "localhost", "port": 6382},
{"host": "localhost", "port": 6383}
]
app = Application([
(r"/?", MainHandler,dict(redis_nodes=redis_nodes)),
],debug=True)
app.listen(7777)
print(f"服务启动,端口为7777")
ioloop.IOLoop.current().start()
四、总结
Redis 分布式锁通过一种简单的机制来确保多个进程或服务不会同时访问同一资源。基本原理是用一个唯一的标识符(比如 UUID)在 Redis 中设置一个锁。如果设置成功,表示锁被获取,其他进程就不能再获取这个锁,直到它被释放。Redis 支持通过 SETNX
和 EXPIRE
命令来实现锁,这样可以确保锁在一定时间后自动过期,防止锁被永久占用。对于更复杂的需求,如需要多个 Redis 实例的高可用性,可以使用 Redlock 算法,它通过在多个 Redis 实例上获取锁来提高系统的容错能力。