运营得不错的P2P平台经常遇到的一个问题是,在标的开始投标的时候出现抢标的情况,有点像电商平台的秒杀,并发量很大,标的总额经常会超标,比如借款人只要100W,但是会员投标金额累加起来会超过100W,这时候就很尴尬了。解决这个问题有很多种方法,今天我们来探讨一下基于Redis的乐观锁的概念下的解决方案。
关于Redis事务的详细介绍,这里就不再赘述了,网上有很多。Redis的事务处理机制中,有一个命令叫“watch”,该命令用于对一个key的监视,在事务处理之前,先把key值监视起来,在事务提交的时候,如果这个key的值被修改过,则事务执行失败。利用该特性,可以实现乐观锁的业务。unwatch命令取消对key的监视,multi命令开始一个事务,exec提交事务,事务提交后,将会关闭所有key的监视。
具体的投标动作在线程中进行,在线程里,我们用线程序号来初始化一个可投金额,后面按投资2元起,逐个扣减,直到扣完为止。
关于Redis事务的详细介绍,这里就不再赘述了,网上有很多。Redis的事务处理机制中,有一个命令叫“watch”,该命令用于对一个key的监视,在事务处理之前,先把key值监视起来,在事务提交的时候,如果这个key的值被修改过,则事务执行失败。利用该特性,可以实现乐观锁的业务。unwatch命令取消对key的监视,multi命令开始一个事务,exec提交事务,事务提交后,将会关闭所有key的监视。
在投标业务中,我们先检查一下标的剩余可投金额,如果足够,那就继续投资,如果不够,就投资失败。我们用100个线程来模拟投标动作,代码如下:
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 40, 3,
TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(40),
new ThreadPoolExecutor.DiscardOldestPolicy());
for (int i = 1; i < 50; i++) {
threadPool.execute(new Thread1(i));
}
具体的投标动作在线程中进行,在线程里,我们用线程序号来初始化一个可投金额,后面按投资2元起,逐个扣减,直到扣完为止。
package com.fire8.redis.test;
import java.util.List;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
/**
* 这个thread 用来并发往redis里增加数据
* @author lixg
*/
public class Thread1 implements Runnable {
private int index=1;
public Thread1(int i)
{
index = i;
}
@Override
public void run() {
Jedis jedis = new Jedis("127.0.0.1");
String value = String.valueOf(index);//获取线程序号,用来初始化标的可投余额
System.out.println("thread ["+index+"]init "+value);
if(!jedis.exists("item.1") && index >9)//查看缓存里有没有
{
long v = jedis.hsetnx("item.1", "amount", value);//没有则放进去
System.err.println("thread ["+index+"]set "+v);//如果1,表示放成功了,0表示没放成功; 检查看是哪个线程放进去的
}
String string = jedis.hget("item.1", "amount");
System.out.println("thread ["+index+"]get:"+string);//其他线程取出来看看是多少
while (true) {
jedis.watch("item.1");//监视这个缓存
string = jedis.hget("item.1", "amount");
System.out.println("thread ["+index+"]watch:"+string);//取出值来计算
long amount = Long.valueOf(string);
if(amount-2<0)//如果可投金额不足,就返回
{
jedis.unwatch();//取消监视
System. err.println( "thread ["+index+"] 标的可投金额不足");
//jedis.lpush("fail", String.format("%02d", index));//记录到失败列表
jedis.zadd("fail", index, String.format("%02d", index));
break;
}
Transaction transaction = jedis.multi();//如果可投金额足够,则开启事务
transaction.hincrBy("item.1", "amount", -2);//扣减剩余金额
List<Object> result = transaction.exec();//执行事务
if (result == null || result.isEmpty()) {
System. err.println( "thread ["+index+"]事务执行失败,说明剩余可投金额被别人修改了");//事务执行失败,说明剩余可投金额被别人修改了
//如果没成功,则再来一次
continue;
}
//能跑到这里,说明事务执行成功,剩余金额扣减了
for (Object rt : result) {
long aa = (Long)rt;
System.err.println("-----------------------------thread ["+index+"]update success ,amount:"+rt.toString());//打印剩作数量
jedis.zadd("success", aa, aa+":"+String.format("%02d", index));//将成功的,记录到一个有序列表里
//jedis.lpush("success", String.format("%02d", index) +":"+rt.toString());
}
break;
}
jedis.disconnect();
jedis.close();
}
}
thread [47]init 47
thread [48]init 48
thread [47]get:43
thread [47]watch:43
thread [48]get:43
thread [49]init 49
thread [48]watch:43
thread [49]get:43
thread [49]watch:43
thread [43]事务执行失败,说明剩余可投金额被别人修改了
thread [49]事务执行失败,说明剩余可投金额被别人修改了
thread [47]事务执行失败,说明剩余可投金额被别人修改了
thread [1]事务执行失败,说明剩余可投金额被别人修改了
thread [44]事务执行失败,说明剩余可投金额被别人修改了
thread [46]事务执行失败,说明剩余可投金额被别人修改了
thread [48]事务执行失败,说明剩余可投金额被别人修改了
-----------------------------thread [42]update success ,amount:41
thread [43]watch:41
thread [45]事务执行失败,说明剩余可投金额被别人修改了
thread [47]watch:41
thread [1]watch:41
thread [44]watch:41
thread [49]watch:41
thread [46]watch:41
-----------------------------thread [43]update success ,amount:39
thread [47]事务执行失败,说明剩余可投金额被别人修改了
success count:21----fails count:28
代码的注释已经详细说明了整个业务逻辑,我在这里抛个砖,欢迎大家拍砖
关于P2P平台设计相关的总结,后续我会再发表一些上来讨论