Redis事务简介
概述
事务是一个业务,也可以看成是一个逻辑工作单元,是为了保证业务的完整,数据的正确而推出的一种控制机制,原则上来讲,事务必须要满足ACID四个特性(原子性,一致性,隔离性,持久性),在多个事务
在并发执行,为更好保证事务的四个特性的实现,通常会对事务加锁,Redis为了性能,采用了乐观锁方式进行事务控制,它使用watch命令监视给定的key,当exec(提交事务)的时候,如果监视的key从调用watch后发生过变化,则整个事务会失败。也可以调用watch多次监视多个key。注意watch的key是对整个连接有效的,如果连接断开,监视和事务都会被自动清除。当然exec,discard,unwatch命令都会清除连接中的所有监视。
Redis 的事务处理比较简单。只能保证 client 发起的事务中的命令可以连续的执行,而且不会插入其它的 client 命令,当一个 client 在连接中发出 multi
命令时,这个连接就进入一个事务的上下文,该连接后续的命令不会执行,而是存放到一个队列中,当执行 exec
命令时,redis 会顺序的执行队列中的所有命令。
> multi
> set name a
> set name b
> exec
> get name
操作截图:
需要注意的是,redis 对于事务的处理方式比较特殊,它不会在事务过程中出错时恢复到之前的状态,这在实际应用中导致我们不能依赖 redis 的事务来保证数据一致性。
基本指令
redis进行事务控制时,通常是基于如下指令进行实现,例如:
- multi 开启事务
- exec 提交事务
- discard 取消事务
- watch 监控,如果监控的值发生变化,则提交事务时会失败
- unwatch 去掉监控
Redis保证一个事务中的所有命令要么都执行,要么都不执行(原子性)。如果在发送EXEC命令前客户端断线了,则Redis会清空事务队列,事务中的所有命令都不会执行。而一旦客户端发送了EXEC命令,所有的命令就都会被执行,即使此后客户端断线也没关系,因为Redis中已经记录了所有要执行的命令。
Redis事务控制实践
exec提交事务
例如:模拟转账,tony 500,jack 200,tony转给jack100。过程如下:
127.0.0.1:6379> set tony 500
OK
127.0.0.1:6379> set jack 200
OK
127.0.0.1:6379> mget tony jack
1) "500"
2) "200"
127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379(TX)> decrby tony 100 #所有指令操作会进入到队列
QUEUED
127.0.0.1:6379(TX)> incrby jack 100
QUEUED
127.0.0.1:6379(TX)> mget tony jack
QUEUED
127.0.0.1:6379(TX)> exec #提交事务
1) (integer) 400
2) (integer) 300
3) 1) "400"
2) "300"
127.0.0.1:6379> mget tony jack
1) "400"
2) "300"
127.0.0.1:6379>
discard取消事务
注意redis事务太简单,没有回滚,而只有取消。
127.0.0.1:6379> mget tony jack
1) "400"
2) "300"
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incrby jack 100
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> get jack
"300"
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI
当出现错误指令时,事务也会自动取消。
127.0.0.1:6379> mget tony jack
1) "400"
2) "300"
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby jack 100
QUEUED
127.0.0.1:6379(TX)> abcd
(error) ERR unknown command `abcd`, with args beginning with:
127.0.0.1:6379(TX)> get jack
QUEUED
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get jack
"300"
127.0.0.1:6379>
秒杀抢票事务处理
基于一个秒杀,抢购案例,演示redis乐观锁方式,例如
第一步:打开客户端1,执行如下操作
127.0.0.1:6379> set ticket 1
OK
127.0.0.1:6379> set money 0
OK
127.0.0.1:6379> watch ticket #乐观锁,对值进行观察,改变则事务失败
OK
127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379> decr ticket
QUEUED
127.0.0.1:6379> incrby money 100
QUEUED
第二步:打开客户端2,执行如下操作,演示还没等客户端1提交事务,此时客户端2把票买到了。
127.0.0.1:6379> get ticket
"1"
127.0.0.1:6379> decr ticket
(integer) 0
第三步,回到客户端1:提交事务,检查ticket的值
127.0.0.1:6379> exec
(nil) #执行事务,失败
127.0.0.1:6379> get ticket
“0”
127.0.0.1:6379> unwatch #取消监控
Jedis 客户端事务操作
基于Jedis进行事务测试,代码如下:
package com.jt;
import org.junit.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
public class JedisTransactionTests {
@Test
public void testTransaction(){
Jedis jedis=new Jedis("192.168.126.130",6379);
jedis.auth("123456");
jedis.set("tony","300");
jedis.set("jack","500");
//实现操作,tony转账100给jack
//开启事务
Transaction multi = jedis.multi();
//执行业务操作
try {
multi.decrBy("tony", 100);
multi.incrBy("jack", 100);
int n=100/0;//模拟异常
//提交事务
multi.exec();
}catch(Exception e) {
//出现异常取消事务
multi.discard();
}
String tonyMoney=jedis.get("tony");
String jackMoney=jedis.get("jack");
System.out.println("tonyMoney="+tonyMoney);
System.out.println("jackMoney="+jackMoney);
jedis.close();
}
}
Jedis 客户端秒杀操作实践
package com.jt.demos;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Response;
import redis.clients.jedis.Transaction;
import java.util.List;
/**
* redis秒杀练习:
* 模拟两个线程都去抢购同一张票(考虑乐关锁)
*/
public class SecondKillDemo02 {
public static void secKill(){
Jedis jedis=new Jedis("192.168.126.130",6379);
jedis.auth("123456");
jedis.watch("ticket","money");
String ticket = jedis.get("ticket");
if(ticket==null||Integer.valueOf(ticket)==0)
throw new RuntimeException("已无库存");
Transaction multi = jedis.multi();
try {
multi.decr("ticket");
multi.incrBy("money", 100);
List<Object> exec = multi.exec();
System.out.println(exec);
}catch (Exception e){
e.printStackTrace();
multi.discard();
}finally {
jedis.unwatch();
jedis.close();
}
}
public static void main(String[] args) {
Jedis jedis=new Jedis("192.168.126.130",6379);
jedis.auth("123456");
jedis.set("ticket","1");
jedis.set("money","0");
Thread t1=new Thread(()->{
secKill();
});
Thread t2=new Thread(()->{
secKill();
});
t1.start();
t2.start();
}
}
总结(Summary)
本小节重点讲解了redis中的事务,以及事务控制指令,控制机制,乐观锁实现。
本文由 liyunfei 创作,采用 知识共享署名4.0
国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Sep 21,2022