Redis事务与实现分布式锁

1. Redis事务机制

    1. 与MySQL等关系数据库相同,Redis中也有事务机制,Redis的事务实质上是命令的集合,但Redis中的事务机制不保证事务的原子性,这与关系型数据库中的事务不同,在一个事务中要么所有命令都被执行,要么所有事物都不执行。 一个事务从开始到执行会经历以下三个阶段:

  • 开始事务。
  • 命令入队。
  • 执行事务。

在MySQL中使用START TRANSACTION 或 BEGIN开启一个事务,使用COMMIT提交一个事务;而在Redis中使用MULTI 开始一个事务,由 EXEC 命令触发事务, 一并执行事务中的所有命令。和关系型数据库中的事物相比,在redis事务中如果有某一条命令执行失败,其它的命令仍然会被继续执行,也就是Redis中不支持事务的回滚,也就不具备事务的原子性

    2. Redis事务机制的相关指令:

  • MULTI:用于标记事务的开始,其后执行的命令都将被存入命令队列,直到执行EXEC时,这些命令才会被原子执行
  • EXEC:执行命令队列中的所有命令,但如果在一个事务内执行了WATCH命令,那么只有当WATCH所监控的keys没有被修改的前提下,EXEC命令才能执行事务队列中的所有命令,否则EXEC将放弃当前事务中的所有命令。
  • DISCARD:取消执行事务队列中的所有命令,同时再将当前连接的状态恢复为正常状态,即非事务状态。如果WATCH命令被使用,该命令将UNWATCH所有的keys。注意,该指令并不是Redis的回滚指令,Redis中不支持回滚,该指令只是取消事务中的所有指令的执行
  • WATCH  key[key...]:在MULTI命令执行之前,可以指定待监控的keys,在执行EXEC之前,如果被监控的keys发生修改,EXEC将放弃执行该事务队列中的所有指令。WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC或DISCARD命令。该命令可以保证某个key的CAS
  • UNWATCH:取消当前事务中指定监控的keys,如果执行了EXEC或DISCARD命令,则无需再手工执行该命令了,因为在此之后,事务中所有的keys都将自动取消监控

    3. 命令使用示例:

//正常执行
127.0.0.1:6379> redis-cli -h 127.0.0.1 -p 6379    //命令拼接redis服务器
ok
127.0.0.1:6379> get test                                      //获取test的键值
"hello world"
127.0.0.1:6379> multi          //生成事务
ok
127.0.0.1:6379> set test "hello mygod"               //修改指令
QUEUED
127.0.0.1:6379>exec                                           //提交事务
1) OK
127.0.0.1:6379>

 

2. 分布式锁

    1. 产生背景:分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。在单机环境中,Java中其实提供了很多并发处理相关的API,但是这些API在分布式场景中就无能为力了。也就是说单纯的Java Api并不能提供分布式锁的能力。所以针对分布式锁的实现目前有多种方案。

    2. 实现分布式锁的方案:典型的方案有以下几种

  • 基于数据库实现分布式锁 
  • 基于缓存(redis,memcached,tair)实现分布式锁
  •  基于Zookeeper实现分布式锁

    3. 分布式锁的要求:

  • 可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行
  • 这把锁要是一把可重入锁(避免死锁)
  • 这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条)
  • 有高可用的获取锁和释放锁功能
  • 获取锁和释放锁的性能要好

3. 分布式锁的数据库实现方案

    1. 基于数据库表的实现:最简单的方式可能就是直接创建一张锁表,然后通过操作该表中的数据来实现,当我们要锁住某个方法或资源时,我们就在该表中增加一条记录,想要释放锁的时候就删除这条记录。

  • 首先创建一个分布式锁的表,可以把里面存储的看做分布式锁
    CREATE TABLE `methodLock` (
      `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
      `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名',
      `desc` varchar(1024) NOT NULL DEFAULT '备注信息',
      `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
      PRIMARY KEY (`id`),
      UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
    
  • 如果想要对分布式执行的某个方法加锁,就使用这个方法名向表中插入数据
    insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)

    因为我们对method_name做了唯一性约束,这里如果有多个插入请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。

  • 当方法执行完毕之后,想要释放锁的话,需要执行以下Sql删除锁
    delete from methodLock where method_name ='method_name'

     

    2. 产生的问题:

  • 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
  • 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
  • 这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
  • 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。

    3. 解决办法:

  • 数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。
  • 没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
  • 非阻塞的?搞一个while循环,直到insert成功再返回成功。
  • 非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。

4. 基于Redis缓存实现的分布式锁

    1. 相比较于基于数据库实现分布式锁的方案来说,基于缓存来实现在性能方面会表现的更好一点(连接数据库进行读写操作性能耗费比缓存大)。而且很多缓存是可以集群部署的,可以解决单点问题。

    2. Redis中有直接的命令支持,而且Redis的本身命令执行是一个单线程的,这就为分布式锁提供了很好的实现,实现命令如下

  • SETNX key val:当且仅当key不存在时,set才会成功,返回1;若key存在,操作失败,返回0。
  • expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
  • delete key:删除key,即释放锁

    3. Jedis客户端也提供了相应的方法,主要就是setnx(String key,String value)方法,简单实现思想伪代码如下:

String get(String key) {
//首先尝试从redis(或redis集群)中获取key对应的数据  
   String value = redis.get(key); 
//如果为null,则使用redis中的分布式锁
   if (value  == null) {  
//通过setnx方法创建分布式锁
    if (redis.setnx(key_mutex, "1")) {  
        // 设置分布式锁的过期时间,可以避免死锁 
        redis.expire(key_mutex, 3 * 60)  
        value = db.get(key); //从数据库中取得数据 
        redis.set(key, value);//回写到缓存中  
        redis.delete(key_mutex);//释放锁  
    } else {  
        //其他线程休息50毫秒后重试  
        Thread.sleep(50);  
        get(key);  
    }  
  }  
}  

5. 基于Zookeeper实现分布式锁

    1. ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:

(1)创建一个目录mylock; 
(2)线程A想获取锁就在mylock目录下创建临时顺序节点; 
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁; 
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点; 
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。

这里推荐一个Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。

优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。

缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。

 

exit(☀)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值