0%

redis---事务

使用

为了保证原子性, redis提供了简单的事务. 但不支持事务回滚

1
2
3
multi 开始事务, 之后输入命令, 会放入队列而不是真正的执行
discard 停止事务
exec 执行事务, 真正执行命令

细节

  1. 在事务中, 如果有一条命令执行错误, 其他的命令也会正常执行
    事务案例
    即redis的事务不满足原子性, 但满足隔离性(redis为单线程, 一个事务内的指定可以得到严格的串行化执行)
  2. 使用discard停止事务后, 之前提交的命令不会执行

    流水线vs事务

    流水线: 将多个命令打包, 然后一并发送到服务器
    事务: 将多个命令打包, 然后让服务器一并执行

    客户端使用

    在客户端中使用事务, 很多库的实现方式是用pipline将命令打包发送到服务器, 并在服务端用事务执行. 这样就能实现多个命令的打包发送, 以及打包执行.

watch命令

watch命令可以对数据库的key进行监视, 如果在exec之前, 这些键的值发生了变化, 那么该事务执行失败.

取消watch

  1. unwatch命令
  2. exec命令执行事务后, 监视自动取消
  3. discard终止事务, 监视自动取消

使用watch实现乐观锁

用被监视的key当做锁, 如果键值发生改变, 事务执行失败;

分布式锁是一种悲观锁

案例

假设我们对redis中的数据进行倍数操作; 如果是加减操作, 可以直接用incrby, 不会产生并发问题. 但是redis并没有提供倍数操作的指令, 我们只能先取值, 运算, 再存入. 这就会产生并发问题. 解决方法可以用watch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package redis;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

import java.util.List;

/**
* @Author wenxuan.hao
* @create 2020-02-16 00:31
*/
public class Watch {

public static void main(String[] args) {
Jedis jedis = new Jedis();
String userId = "abc";
String key = keyFor(userId);
jedis.setnx(key, String.valueOf(5));
System.out.println(doubleAccount(jedis, userId));
jedis.close();
}

public static int doubleAccount(Jedis jedis, String userId) {
String key = keyFor(userId);
// 若加锁失败, 自旋
while (true) {
jedis.watch(key);
int value = Integer.parseInt(jedis.get(key));
value *= 2; // 加倍

// 事务
Transaction tx = jedis.multi();
tx.set(key, String.valueOf(value));
List<Object> res = tx.exec();
// 因为对key进行了watch, 如果key没有改变, 更新成功, 跳出循环
if (res != null) {
break; // 成功了
}
}
return Integer.parseInt(jedis.get(key)); // 重新获取余额
}

public static String keyFor(String userId) {
return String.format("account_%s", userId);
}
}