1. Redis事务机制
1. 与MySQL等关系数据库相同,Redis中也有事务机制,Redis的事务实质上是命令的集合,但Redis中的事务机制不保证事务的原子性,这与关系型数据库中的事务不同,在一个事务中要么所有命令都被执行,要么所有事物都不执行。
Redis中的事务是可以视为一个队列,即我们可以通过MULTI开始一个事务,这相当于我们声明了一个命令队列。接下来,我们向Redis中提交的每条命令,都会被排入这个命令队列。当我们输入EXEC命令时,将触发当前事务,这相当于我们从命令队列中取出命令并执行,所以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方式