Redis事务介绍
Redis 的事务是通过 MULTI 、 EXEC 、 DISCARD 和 WATCH 这四个命令来完成的。
Redis 的单个命令都是原子性的,所以这里需要确保事务性的对象是命令集合。
Redis 将命令集合序列化并确保处于同一事务的命令集合连续且不被打断的执行。
Redis 不支持回滚操作。
在一个事物中,处理命令集不会被干扰。Redis是单进程单线程,所以不会出现线程并发。
事务命令
MULTI:用于标记事务块的开始。Redis会将后续的命令逐个放入队列中,然后使用EXEC命令原子化地执行这个命令序列。
EXEC:在一个事务中执行所有先前放入队列的命令,然后恢复正常的连接状态
DISCARD:清除所有先前在一个事务中放入队列的命令,然后恢复正常的连接状态。
WATCH:当某个[事务需要按条件执行]时,就要使用这个命令将给定的[键设置为受监控]的状态。注意事项:使用该命令可以实现 Redis 的乐观锁。(后面实现)
UNWATCH:清除所有先前为一个事务监控的键
事务失败处理
- Redis 语法错误:整个事务的命令在队列里都清除
- Redis 运行错误:在队列里正确的命令可以执行 (弱事务性)
弱事务性 :
- 在队列里正确的命令可以执行 (非原子操作)
- 不支持回滚
Redis 不支持事务回滚(为什么呢)
- 大多数事务失败是因为语法错误或者类型错误,这两种错误,在开发阶段都是可以预见的
- Redis 为了性能方面就忽略了事务回滚。 (回滚记录历史版本)
Redis事务使用场景----Redis乐观锁
乐观锁基于CAS(Compare And Swap)思想(比较并替换),是不具有互斥性,不会产生锁等待而消耗资源,但是需要反复的重试,但也是因为重试的机制,能比较快的响应。因此我们可以利用redis来实现乐观锁。
具体思路如下:
- 利用redis的watch功能,监控这个redisKey的状态值
- 获取redisKey的值
- 创建redis事务
- 给这个key的值+1
- 然后去执行这个事务,如果key的值被修改过则回滚,key不加1
package com.wang.redis.lock;
import java.util.List;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
public class Lock {
public void watch() {
Jedis jedis = new Jedis("127.0.0.1", 6378);
try {
String watchKeys = "watchKeys";
//初始值 value=1
jedis.set(watchKeys, "1");
//监听key为watchKeys的值
jedis.watch(watchKeys);
//开启事务
Transaction tx = jedis.multi();
//watchKeys自增加一
tx.incr(watchKeys);
//执行事务,如果其他线程对watchKeys中的value进行修改,则该事务将不会执行
//通过redis事务以及watch命令实现乐观锁
List<Object> exec = tx.exec();
if (exec == null) {
System.out.println("事务未执行");
} else {
System.out.println("事务成功执行,watchKeys的value成功修改");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
jedis.close();
}
}
}
Redis乐观锁实现秒杀
package com.wang.redis.lock;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
public class Second {
public static void main(String[] arg) {
String redisKey = "second";
ExecutorService executorService = Executors.newFixedThreadPool(20);
try {
Jedis jedis = new Jedis("127.0.0.1", 6378);
// 初始值
jedis.set(redisKey, "0");
jedis.close();
} catch (Exception e) {
e.printStackTrace();
}
for (int i = 0; i < 1000; i++) {
executorService.execute(() -> {
Jedis jedis1 = new Jedis("127.0.0.1", 6378);
try {
jedis1.watch(redisKey);
String redisValue = jedis1.get(redisKey);
int valInteger = Integer.valueOf(redisValue);
String userInfo = UUID.randomUUID().toString();
// 没有秒完
if (valInteger < 20) {
Transaction tx = jedis1.multi();
tx.incr(redisKey);
List list = tx.exec();
// 秒成功 失败返回空list而不是空
if (list != null && list.size() > 0) {
System.out.println("用户:" + userInfo + ",秒杀成功!当前成功人数:" + (valInteger + 1));
} // 版本变化,被别人抢了。
else {
System.out.println("用户:" + userInfo + ",秒杀失败");
}
} // 秒完了
else {
System.out.println("已经有20人秒杀成功,秒杀结束");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
jedis1.close();
}
});
}
executorService.shutdown();
}
}