项目涉及到一个订单重复提交的问题,用一个token验证来解决,
客户端订单页面请求一个token,此token由服务端生成,并加入缓存,客户端提交订单时将token一并传入,服务端验证token,下单时将token置为无效,以此来防止重复提交,因为每个token只有一次真正入库的机会
验证token的过程有两个最主要的程序:
读token,写token,这两个语句必须处在同步中
业务类:(假定为多线程单例)
public class ResourceService {
public SpResult buyResource(Map<String,String> map) {
String cacheToken = map.get("cacheToken");
SpResult sr = new SpResult();
if(cacheToken == null || "".equals(cacheToken)) {
sr.setHeaderMessage("您的token无效,请刷新页面。");
} else {
synchronized (this) {
Long cacheNow = HandleCache.getCache(cacheToken);
if(cacheNow == null) {
System.out.println("您已报过名,我们将在24小时之内联系您,请耐心等待。");
} else {
Long now = System.currentTimeMillis();
Long minus = now - cacheNow;
if(minus > HandleCache.expireTime) {
System.out.println("您的token已过期,请刷新页面。");
} else {
// resourceAccessor.buyResource(map);
System.out.println("报名成功,我们将在24小时之内联系您。");
HandleCache.destroyCache(cacheToken);
}
}
}
}
sr.setHeaderCode("200");
sr.setBody(null);
return sr;
}
}
token缓存类:
public class HandleCache {
protected static Map<String,Long> cache = new ConcurrentHashMap<>();
public static Long expireTime = new Long(1000*60*5);
public static void setCache(String token) {
Long now = System.currentTimeMillis();
cache.put(token, now);
}
public static Long getCache(String token) {
Long temp = cache.get(token);
return temp;
}
public static void destroyCache(String token) {
cache.remove(token);
}
public static String makeToken() {
String base = "abcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 20; i++) {
int number = random.nextInt(base.length());
sb.append(base.charAt(number));
}
return sb.toString();
}
public static void main(String[] args) {
String cacheToken = HandleCache.makeToken();
HandleCache.setCache(cacheToken);
Map<String,String> map = new HashMap<>();
map.put("phone", "13333333333");
map.put("targetId","0");
map.put("cacheToken",cacheToken);
long startMili=System.currentTimeMillis();
for(int i = 0; i < 10; i++){
MyThread thread = new MyThread(map);
thread.start();
}
long endMili=System.currentTimeMillis();
System.out.println("总耗时为:"+(endMili-startMili)+"毫秒");
}
}
线程类:
class MyThread extends Thread{
private static ResourceService resourceService = new ResourceService();
private static Map<String,String> map;
public MyThread(Map<String,String> _map) {
map = _map;
}
public void run() {
resourceService.buyResource(map);
}
}
总耗时为:55毫秒
报名成功,我们将在24小时之内联系您。
您已报过名,我们将在24小时之内联系您,请耐心等待。
您已报过名,我们将在24小时之内联系您,请耐心等待。
您已报过名,我们将在24小时之内联系您,请耐心等待。
您已报过名,我们将在24小时之内联系您,请耐心等待。
您已报过名,我们将在24小时之内联系您,请耐心等待。
您已报过名,我们将在24小时之内联系您,请耐心等待。
您已报过名,我们将在24小时之内联系您,请耐心等待。
您已报过名,我们将在24小时之内联系您,请耐心等待。
您已报过名,我们将在24小时之内联系您,请耐心等待。
十个线程抢入库,仅有第一次真正入库。这里采用存放token的缓存对象cache和验证过程getCache+destroyCache 双重加锁,耗时55ms
如果拆掉getCache+destroyCache的过程锁,
public SpResult buyResource(Map<String,String> map) {
String cacheToken = map.get("cacheToken");
SpResult sr = new SpResult();
if(cacheToken == null || "".equals(cacheToken)) {
sr.setHeaderMessage("您的token无效,请刷新页面。");
} else {
// synchronized (this) {
Long cacheNow = HandleCache.getCache(cacheToken);
if(cacheNow == null) {
System.out.println("您已报过名,我们将在24小时之内联系您,请耐心等待。");
// sr.setHeaderMessage("您已报过名,我们将在24小时之内联系您,请耐心等待。");
} else {
Long now = System.currentTimeMillis();
Long minus = now - cacheNow;
if(minus > HandleCache.expireTime) {
System.out.println("您的token已过期,请刷新页面。");
// sr.setHeaderMessage("您的token已过期,请刷新页面。");
} else {
// resourceAccessor.buyResource(map);
System.out.println("报名成功,我们将在24小时之内联系您。");
HandleCache.destroyCache(cacheToken);
// sr.setHeaderMessage("报名成功,我们将在24小时之内联系您。");
}
}
// }
}
sr.setHeaderCode("200");
sr.setBody(null);
return sr;
}
总耗时为:46毫秒
报名成功,我们将在24小时之内联系您。
报名成功,我们将在24小时之内联系您。
报名成功,我们将在24小时之内联系您。
报名成功,我们将在24小时之内联系您。
报名成功,我们将在24小时之内联系您。
报名成功,我们将在24小时之内联系您。
报名成功,我们将在24小时之内联系您。
报名成功,我们将在24小时之内联系您。
报名成功,我们将在24小时之内联系您。
报名成功,我们将在24小时之内联系您。
十个全抢到了,经过反复运行,也存在4个抢到,6个没抢到这样的情况,处于随机状态
3.12补充,全程加锁的模式,效率不高,下面参考单例模式,进行双重判断,第一次判断后加锁:
public SpResult buyResource(Map<String,String> map) {
String cacheToken = map.get("cacheToken");
SpResult sr = new SpResult();
if(cacheToken == null || "".equals(cacheToken)) {
sr.setHeaderMessage("您的token无效,请刷新页面。");
} else {
if(HandleCache.getCache(cacheToken) != null) {
synchronized (this) {
Long cacheNow = HandleCache.getCache(cacheToken);
if(cacheNow != null) {
Long now = System.currentTimeMillis();
Long minus = now - cacheNow;
if (minus > HandleCache.expireTime) {
System.out.println("您的token已过期,请刷新页面。");
sr.setHeaderMessage("您的token已过期,请刷新页面。");
} else {
// resourceAccessor.buyResource(map);
System.out.println("报名成功,我们将在24小时之内联系您。");
HandleCache.destroyCache(cacheToken);
sr.setHeaderMessage("报名成功,我们将在24小时之内联系您。");
}
} else {
System.out.println("您已报过名lock,我们将在24小时之内联系您,请耐心等待。");
sr.setHeaderMessage("您已报过名lock,我们将在24小时之内联系您,请耐心等待。");
}
}
} else {
System.out.println("您已报过名nlock,我们将在24小时之内联系您,请耐心等待。");
sr.setHeaderMessage("您已报过名nlock,我们将在24小时之内联系您,请耐心等待。");
}
}
sr.setHeaderCode("200");
sr.setBody(null);
return sr;
}
让一部分的线程不需要阻塞便可以进行下去,输出如下:
报名成功,我们将在24小时之内联系您。
您已报过名nlock,我们将在24小时之内联系您,请耐心等待。
您已报过名lock,我们将在24小时之内联系您,请耐心等待。
您已报过名lock,我们将在24小时之内联系您,请耐心等待。
您已报过名nlock,我们将在24小时之内联系您,请耐心等待。
您已报过名nlock,我们将在24小时之内联系您,请耐心等待。
您已报过名nlock,我们将在24小时之内联系您,请耐心等待。
您已报过名nlock,我们将在24小时之内联系您,请耐心等待。
您已报过名nlock,我们将在24小时之内联系您,请耐心等待。
总耗时为:14毫秒
您已报过名nlock,我们将在24小时之内联系您,请耐心等待。
Process finished with exit code 0
可以看到,有7个线程未阻塞,直接判断到null,证明已经处理掉了,2个线程判断到非null,进尔再加锁处理读写