详解redis中的锁以及使用场景

分布式锁

什么是分布式锁?

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。

为什么要使用分布式锁?

​ 为了保证共享资源的数据一致性。

什么场景下使用分布式锁?

​ 数据重要且要保证一致性

如何实现分布式锁?

主要介绍使用redis来实现分布式锁

redis事务

redis事务介绍:

​ 1.redis事务可以一次执行多个命令,本质是一组命令的集合。

​ 2.一个事务中的所有命令都会序列化,按顺序串行化的执行而不会被其他命令插入

​ **作用:**一个队列中,一次性、顺序性、排他性的执行一系列命令

multi指令的使用

1. 下面指令演示了一个完整的事物过程,所有指令在exec前不执行,而是缓存在服务器的一个事物队列中

2. 服务器一旦收到exec指令才开始执行事物队列,执行完毕后一次性返回所有结果

3. 因为redis是单线程的,所以不必担心自己在执行队列是被打断,可以保证这样的“原子性”

注:redis事物在遇到指令失败后,后面的指令会继续执行

# Multi 命令用于标记一个事务块的开始事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由 EXEC 命令原子性( atomic )地执行
> multi(开始一个redis事物)
incr books
incr books
> exec (执行事物)
> discard (丢弃事物)

注:mysql的rollback与redis的discard的区别

mysql回滚为sql全部成功才执行,一条sql失败则全部失败,执行rollback后所有语句造成的影响消失

redis的discard只是结束本次事务,正确命令造成的影响仍然还在.

1)redis如果在一个事务中的命令出现错误,那么所有的命令都不会执行;
2)redis如果在一个事务中出现运行错误,那么正确的命令会被执行。

watch 指令作用

实质:WATCH 只会在数据被其他客户端抢先修改了的情况下通知执行命令的这个客户端(通过 WatchError 异常)但不会阻止其他客户端对数据的修改

1. watch其实就是redis提供的一种乐观锁,可以解决并发修改问题

2. watch会在事物开始前盯住一个或多个关键变量,当服务器收到exec指令要顺序执行缓存中的事物队列时,redis会检查关键变量自watch后是否被修改

3. WATCH 只会在数据被其他客户端抢先修改了的情况下通知执行命令的这个客户端(通过 WatchError 异常)但不会阻止其他客户端对数据的修改

watch+multi实现乐观锁

setnx指令(redis的分布式锁)

1、分布式锁

分布式锁本质是占一个坑,当别的进程也要来占坑时发现已经被占,就会放弃或者稍后重试
占坑一般使用 setnx(set if not exists)指令,只允许一个客户端占坑
先来先占,用完了在调用del指令释放坑

> setnx lock:codehole true
.... do something critical ....
> del lock:codehole

​但是这样有一个问题,如果逻辑执行到中间出现异常,可能导致del指令没有被调用,这样就会陷入死锁,锁永远无法释放
为了解决死锁问题,我们拿到锁时可以加上一个expire过期时间,这样即使出现异常,当到达过期时间也会自动释放锁

> setnx lock:codehole true
> expire lock:codehole 5
.... do something critical ....
> del lock:codehole

这样又有一个问题,setnx和expire是两条指令而不是原子指令,如果两条指令之间进程挂掉依然会出现死锁
为了治理上面乱象,在redis 2.8中加入了set指令的扩展参数,使setnx和expire指令可以一起执行

> set lock:codehole true ex 5 nx
''' do something '''
> del lock:codehole

redis解决超卖问题

1、使用reids的 watch + multi 指令实现

#! /usr/bin/env python
# -*- coding: utf-8 -*-
import redis
def sale(rs):
  while True:
    with rs.pipeline() as p:
      try:
        p.watch('apple')          # 监听key值为apple的数据数量改变
        count = int(rs.get('apple'))
        print('拿取到了苹果的数量: %d' % count)
        p.multi()             # 事务开始
        if count> 0 :           # 如果此时还有库存
          p.set('apple', count - 1)
          p.execute()          # 执行事务
        p.unwatch()
        break               # 当库存成功减一或没有库存时跳出执行循环
      except Exception as e:         # 当出现watch监听值出现修改时,WatchError异常抛出
        print('[Error]: %s' % e)
        continue              # 继续尝试执行

rs = redis.Redis(host='127.0.0.1', port=6379)   # 连接redis
rs.set('apple',1000)                # # 首先在redis中设置某商品apple 对应数量value值为1000
sale(rs)

1)原理

1. 当用户购买时,通过 WATCH 监听用户库存,如果库存在watch监听后发生改变,就会捕获异常而放弃对库存减一操作

2. 如果库存没有监听到变化并且数量大于1,则库存数量减一,并执行任务

2)弊端

1. Redis 在尝试完成一个事务的时候,可能会因为事务的失败而重复尝试重新执行

2. 保证商品的库存量正确是一件很重要的事情,但是单纯的使用 WATCH 这样的机制对服务器压力过大

2、使用reids的 watch + multi + setnx 指令实现

1)为什么要自己构建锁

然有类似的 SETNX 命令可以实现 Redis 中的锁的功能,但他锁提供的机制并不完整

. 并且setnx也不具备分布式锁的一些高级特性,还是得通过我们手动构建

2)创建一个redis锁

在 Redis 中,可以通过使用 SETNX 命令来构建锁:rs.setnx(lock_name, uuid值)

. 而锁要做的事情就是将一个随机生成的 128 位 UUID 设置位键的值,防止该锁被其他进程获取

3)释放锁

锁的删除操作很简单,只需要将对应锁的 key 值获取到的 uuid 结果进行判断验证

. 符合条件(判断uuid值)通过 delete 在 redis 中删除即可,pipe.delete(lockname)

3. 此外当其他用户持有同名锁时,由于 uuid 的不同,经过验证后不会错误释放掉别人的锁

4)解决锁无法释放问题

1. 在之前的锁中,还出现这样的问题,比如某个进程持有锁之后突然程序崩溃,那么会导致锁无法释放

2. 而其他进程无法持有锁继续工作,为了解决这样的问题,可以在获取锁的时候加上锁的超时功能

import redis
import uuid
import time

# 1.初始化连接函数
def get_conn(host="127.0.0.1",port=6379):
  rs = redis.Redis(host=host, port=port)
  return rs

# 2. 构建redis锁
def acquire_lock(rs, lock_name, expire_time=10):
  '''
  rs: 连接对象
  lock_name: 锁标识
  acquire_time: 过期超时时间
  return -> False 获锁失败 or True 获锁成功
  '''
  identifier = str(uuid.uuid4())
  end = time.time() + expire_time
  while time.time() < end:
    # 当获取锁的行为超过有效时间,则退出循环,本次取锁失败,返回False
    if rs.setnx(lock_name, identifier): # 尝试取得锁
      return identifier
    # time.sleep(.001)
    return False

# 3. 释放锁
def release_lock(rs, lockname, identifier):
  '''
  rs: 连接对象
  lockname: 锁标识
  identifier: 锁的value值,用来校验
  '''

  if rs.get(lockname).decode() == identifier: # 防止其他进程同名锁被误删
    rs.delete(lockname)
    return True      # 删除锁
  else:
    return False      # 删除失败

#有过期时间的锁
def acquire_expire_lock(rs, lock_name, expire_time=10, locked_time=10):
  '''
  rs: 连接对象
  lock_name: 锁标识
  acquire_time: 过期超时时间
  locked_time: 锁的有效时间
  return -> False 获锁失败 or True 获锁成功
  '''
  identifier = str(uuid.uuid4())
  end = time.time() + expire_time
  while time.time() < end:
    # 当获取锁的行为超过有效时间,则退出循环,本次取锁失败,返回False
    if rs.setnx(lock_name, identifier): # 尝试取得锁
      # print('锁已设置: %s' % identifier)
      rs.expire(lock_name, locked_time)
      return identifier
    time.sleep(.001)
  return False

'''在业务函数中使用上面的锁'''
def sale(rs):
  start = time.time()      # 程序启动时间
  with rs.pipeline() as p:
    '''
    通过管道方式进行连接
    多条命令执行结束,一次性获取结果
    '''

    while 1:
      lock = acquire_lock(rs, 'lock')
      if not lock: # 持锁失败
        continue

      #开始监测"lock"
      p.watch("lock")
      try:
        #开启事务
        p.multi()
        count = int(rs.get('apple')) # 取量
        p.set('apple', count-1)   # 减量
        # time.sleep(5)

        #提交事务
        p.execute()
        print('当前库存量: %s' % count)
        #成功则跳出循环
        break
      except:
        #事务失败对应处理
        print("修改数据失败")

      #无论成功与否最终都需要释放锁
      finally:

        res = release_lock(rs, 'lock', lock)
        #释放锁成功,
        if res:
          print("删除锁成功")
        #释放锁失败,强制删除
        else:
          print("删除锁失败,强制删除锁")
          res = rs.delete('lock')
          print(res)

    print('[time]: %.2f' % (time.time() - start))

rs = redis.Redis(host='127.0.0.1', port=6379)   # 连接redis
# rs.set('apple',1000)               # # 首先在redis中设置某商品apple 对应数量value值为1000
sale(rs)

优化锁无法释放的问题,为锁添加过期时间

def acquire_expire_lock(rs, lock_name, expire_time=10, locked_time=10):
  '''
  rs: 连接对象
  lock_name: 锁标识
  acquire_time: 过期超时时间
  locked_time: 锁的有效时间
  return -> False 获锁失败 or True 获锁成功
  '''
  identifier = str(uuid.uuid4())
  end = time.time() + expire_time
  while time.time() < end:
    # 当获取锁的行为超过有效时间,则退出循环,本次取锁失败,返回False
    if rs.setnx(lock_name, identifier): # 尝试取得锁
      # print('锁已设置: %s' % identifier)
      rs.expire(lock_name, locked_time)
      return identifier
    time.sleep(.001)
  return False

关于redis中的锁

Watch:监测一个key。如果这个key的value改变,那个接下来的事务操作全部失效

multi: 开启一个事务。

Setnx: 跟set一样都往redis添加一个key。不一定的地方在于:set的时候如果这个值存在,就是修改操作。不存在就是添加操作。setnx:存在的时候不能再次添加,不存在的时候才能添加。

到此这篇关于详解redis中的锁以及使用场景的文章就介绍到这了,更多相关redis 锁内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 浅谈Redis分布式锁的正确实现方式

    前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁. 可靠性 首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件: 1.互斥性.在任意时刻,只有一个客户端能持有锁. 2.不会发生死锁.即使有一个

  • Redis实现分布式锁的几种方法总结

    Redis实现分布式锁的几种方法总结 分布式锁是控制分布式系统之间同步访问共享资源的一种方式.在分布式系统中,常常需要协调他们的动作.如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁. 我们来假设一个最简单的秒杀场景:数据库里有一张表,column分别是商品ID,和商品ID对应的库存量,秒杀成功就将此商品库存量-1.现在假设有1000个线程来秒杀两件商品,500个线程秒杀第一个商品,

  • SpringBoot集成Redisson实现分布式锁的方法示例

    上篇 <SpringBoot 集成 redis 分布式锁优化>对死锁的问题进行了优化,今天介绍的是 redis 官方推荐使用的 Redisson ,Redisson 架设在 redis 基础上的 Java 驻内存数据网格(In-Memory Data Grid),基于NIO的 Netty 框架上,利用了 redis 键值数据库.功能非常强大,解决了很多分布式架构中的问题. Github的wiki地址: https://github.com/redisson/redisson/wiki 官方文档

  • Redis Template实现分布式锁的实例代码

    前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁. 可靠性 首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件: 1.互斥性.在任意时刻,只有一个客户端能持有锁. 2.不会发生死锁.即使有一个

  • redis中使用java脚本实现分布式锁

    redis被大量用在分布式的环境中,自然而然分布式环境下的锁如何解决,立马成为一个问题.例如我们当前的手游项目,服务器端是按业务模块划分服务器的,有应用服,战斗服等,但是这两个vm都有可能同时改变玩家的属性,这如果在同一个vm下面,就很容易加锁,但如果在分布式环境下就没那么容易了,当然利用redis现有的功能也有解决办法,比如redis的脚本. redis在2.6以后的版本中增加了Lua脚本的功能,可以通过eval命令,直接在RedisServer环境中执行Lua脚本,并且可以在Lua脚本中调用

  • 基于Redis实现分布式锁以及任务队列

    一.前言 双十一刚过不久,大家都知道在天猫.京东.苏宁等等电商网站上有很多秒杀活动,例如在某一个时刻抢购一个原价1999现在秒杀价只要999的手机时,会迎来一个用户请求的高峰期,可能会有几十万几百万的并发量,来抢这个手机,在高并发的情形下会对数据库服务器或者是文件服务器应用服务器造成巨大的压力,严重时说不定就宕机了,另一个问题是,秒杀的东西都是有量的,例如一款手机只有10台的量秒杀,那么,在高并发的情况下,成千上万条数据更新数据库(例如10台的量被人抢一台就会在数据集某些记录下 减1),那次这个

  • 如何利用Redis锁解决高并发问题详解

    redis技术的使用: redis真的是一个很好的技术,它可以很好的在一定程度上解决网站一瞬间的并发量,例如商品抢购秒杀等活动... redis之所以能解决高并发的原因是它可以直接访问内存,而以往我们用的是数据库(硬盘),提高了访问效率,解决了数据库服务器压力. 为什么redis的地位越来越高,我们为何不选择memcache,这是因为memcache只能存储字符串,而redis存储类型很丰富(例如有字符串.LIST.SET等),memcache每个值最大只能存储1M,存储资源非常有限,十分消耗内

  • 详解Java如何实现基于Redis的分布式锁

    前言 单JVM内同步好办, 直接用JDK提供的锁就可以了,但是跨进程同步靠这个肯定是不可能的,这种情况下肯定要借助第三方,我这里实现用Redis,当然还有很多其他的实现方式.其实基于Redis实现的原理还算比较简单的,在看代码之前建议大家先去看看原理,看懂了之后看代码应该就容易理解了. 我这里不实现JDK的java.util.concurrent.locks.Lock接口,而是自定义一个,因为JDK的有个newCondition方法我这里暂时没实现.这个Lock提供了5个lock方法的变体,可以

  • Python实现的redis分布式锁功能示例

    本文实例讲述了Python实现的redis分布式锁功能.分享给大家供大家参考,具体如下: #!/usr/bin/env python # coding=utf-8 import time import redis class RedisLock(object): def __init__(self, key): self.rdcon = redis.Redis(host='', port=6379, password="", db=1) self._lock = 0 self.lock

  • redis实现加锁的几种方法示例详解

    前言 本文主要给大家介绍了关于redis实现加锁的几种方法,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 1. redis加锁分类 redis能用的的加锁命令分表是INCR.SETNX.SET 2. 第一种锁命令INCR 这种加锁的思路是, key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作进行加一. 然后其它用户在执行 INCR 操作进行加一时,如果返回的数大于 1 ,说明这个锁正在被使用当中. 1. 客户端A请求服务器获取key的值为1表示

随机推荐