redis应该中如何解决多写的竞争问题
最近项目中需要使用到redis进行数据缓存及读写操作。遇到了一些问题并总结到这里。
考虑到redis没有像db中的sql语句,update val = val + 10 where ...,无法使用这种方式进行对数据的更新。
假如有某个key = "price", value值为10,现在想把value值进行+10操作。正常逻辑下,就是先把数据key为price的值读回来,加上10,再把值给设置回去。如果只有一个连接的情况下,这种方式没有问题,可以工作得很好,但如果有两个连接时,两个连接同时想对还price进行+10操作,就可能会出现问题了。
例如:两个连接同时对price进行写操作,同时加10,最终结果我们知道,应该为30才是正确。
考虑到一种情况:
T1时刻,连接1将price读出,目标设置的数据为10+10 = 20。
T2时刻,连接2也将数据读出,也是为10,目标设置为20。
T3时刻,连接1将price设置为20。
T4时刻,连接2也将price设置为20,则最终结果是一个错误值20。
如何解决?
方案一:可以使用独占锁的方式,类似操作系统的mutex机制。(网上有例子,http://blog.csdn.net/black_ox/article/details/48972085 不过实现相对复杂,成本较高)
方案二:使用乐观锁的方式进行解决(成本较低,非阻塞,性能较高)
如何用乐观锁方式进行解决?
本质上是假设不会进行冲突,使用redis的命令watch进行构造条件。伪代码如下:
watch price
get price $price
$price = $price + 10
multi
set price $price
exec
解释一下:
watch这里表示监控该key值,后面的事务是有条件的执行,如果从watch的exec语句执行时,watch的key对应的value值被修改了,则事务不会执行。
同样考虑刚刚的场景,
T1时刻,连接1对price进行watch,读出price值为10,目标计算为20;
T2时刻,连接2对price进行watch,读出price值为10,目标计算为20;
T3时刻,连接2将目标值为20写到redis中,执行事务,事务返回成功。
T4时刻,连接1也对price进行写操作,执行事务时,由于之前已经watch了price,price在T1至T4之间已经被修改过了,所以事务执行失败。
综上,该乐观锁机制可以简单明了的解决了写冲突的问题。
如果同时进行有多个请求进行写操作,例如同一时刻有100个请求过来,那么只会有一个最终成功,其余99个全部会失败,效率不高。
而且从业务层面,有些是不可接受的场景。例如:大家同时去抢一个红包,如果背后也是用乐观锁的机制去处理,那每个请求后都只有一个人成功打开红包,这对业务是不可忍受的。
在这种情况下,如果想让总体效率最大化,可以采用排队的机制进行。
将所有需要对同一个key的请求进行入队操作,然后用一个消费者线程从队头依次读出请求,并对相应的key进行操作。
这样对于同一个key的所有请求就都是顺序访问,正常逻辑下则不会有写失败的情况下产生 。从而最大化写逻辑的总体效率。
https://blog.csdn.net/dreamvyps/article/details/60964130
我的疑问:为什么不用lua脚本去执行呢?
方案三
直接上代码,写一个线程
public static class IncTest implements Runnable{
private JedisPool pool;
private CountDownLatch cdl;
public IncTest(JedisPool pool,CountDownLatch cdl) {
this.pool = pool;
this.cdl = cdl;
}
@Override
public void run() {
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
Jedis j = pool.getResource();
try {
// ===============使用lua脚本的方式==============
j.eval("local count = redis.call('get', 'test') return redis.call('set', 'test', count+1)")
// ===============使用lua脚本的方式结束==============
//==============错误的并发方式======================
// String test = j.get("test");
// System.out.println("old Data:" + test);
// Integer newtest = Integer.valueOf(test) + 1;
// System.out.println("new Data:" + newtest);
//
// j.set("test", newtest.toString());
// ===========错误的并发方式结束=============
} finally {
pool.returnResource(j);
}
}
}
main方法模拟并发
public static void main(String[] args) {
JedisPool pool = new JedisPool("192.168.113.132", 6379);
CountDownLatch cdl = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
new Thread(new IncTest(pool, cdl)).start();
cdl.countDown();
}
test开始值为1 ,执行100次应该变成101。使用lua方式可以获得正确结果。另一种方式结果错误。