问题
之前让同事修改需求,实现商品列表页上的每个购物车图标按钮,点击能够局部刷新用户的购物车
购物车数据库是这样设计的,每条数据主要跟用户id、商家id、商品id有关;另外还有一个字段是count,也就是说用户对某个商品的购物车数据就是该count
- 如果查到三条信息一致的记录,那么就update原来的那条记录 count + 1
- 如果没有查到这样的记录,那么insert一条 count = 1 的数据
问题就是这个insert;不然只有update的话,按照原来我写的文章 mysql数据一致性闲谈,搞一个悲观锁就ok了
问题场景
当一个新的用户,连续两次快速点击同一个商品的购物车按钮(前端没有做同步,允许并发),后台的ssh结构会导致两个线程都进入了上述情况2
中
简化问题
因为我对java不是很熟,我们来把原有问题简化成如下问题
import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by chainhelen on 2017/4/22.
*/
class Cart {
private String userId;
private String productId;
private String sellerId;
private int count;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getProductId() {
return productId;
}
public void setProductId(String productId) {
this.productId = productId;
}
public String getSellerId() {
return sellerId;
}
public void setSellerId(String sellerId) {
this.sellerId = sellerId;
}
public Cart(String userId,String productId,String sellerId) {
setUserId(userId);
setProductId(productId);
setSellerId(sellerId);
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
}
class Simulate implements Runnable {
//请求
private List data;
private List<Cart> requestArr;
public Simulate(List<Cart> requestArr, List data) {
this.requestArr = requestArr;
this.data = data;
}
public void printCartData() {
if(null != data && 0 == data.size()) {
System.out.println("<data.size() == 0>");
} else {
Iterator it = data.iterator();
while (it.hasNext()) {
Cart cart = (Cart) it.next();
System.out.println(cart.getUserId() + ", " +
cart.getProductId() + ", " + cart.getSellerId() +
", count = " + cart.getCount()
);
}
}
}
public int found(Cart a, List<Cart> b) {
for(int i = 0;i < b.size();i++) {
Cart curData = (Cart) b.get(i);
if (a.getUserId() == curData.getUserId() &&
a.getProductId() == curData.getProductId() &&
a.getSellerId() == curData.getSellerId()) {
return i;
}
}
return -1;
}
@Override
public void run() {
try {
try {
Thread.sleep(1);
}catch (Exception e) {
e.printStackTrace();
}
Cart cart = requestArr.get(Integer.parseInt(Thread.currentThread().getName()));
int index = found(cart, data);
//如果在数据库里面找到对应数据,那么update原来数据的count + 1
if (-1 != index) {
Cart curData = (Cart) data.get(index);
int count = curData.getCount();
curData.setCount(count + 1);
} else { //如果没有找到,那么insert一条count = 1
Cart curData = new Cart(cart.getUserId(), cart.getProductId(), cart.getSellerId());
curData.setCount(1);
synchronized (data) {
data.add(curData);
}
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
public class main {
public static void main(String[] args) {
List<Cart> requestArr = new ArrayList<Cart>();
requestArr.add(new Cart("userId001", "productId501", "sellerId000"));
requestArr.add(new Cart("userId001", "productId501", "sellerId000"));
requestArr.add(new Cart("userId002", "productId502", "sellerId000"));
requestArr.add(new Cart("userId002", "productId502", "sellerId000"));
requestArr.add(new Cart("userId001", "productId601", "sellerId000"));
requestArr.add(new Cart("userId002", "productId602", "sellerId000"));
requestArr.add(new Cart("userId001", "productId501", "sellerId000"));
requestArr.add(new Cart("userId002", "productId502", "sellerId000"));
requestArr.add(new Cart("userId001", "productId701", "sellerId000"));
requestArr.add(new Cart("userId002", "productId702", "sellerId000"));
List data = new ArrayList();
//requestArr, data
Simulate simulate = new Simulate(requestArr, data);
for(int i = 0;i < requestArr.size();i++) {
(new Thread(simulate, i+"")).start();
}
try {
Thread.currentThread().sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
simulate.printCartData();
}
}
上面主要看两个数据data代表数据库里面的数据,requestArr代表并发的请求
模拟细节,这个地方有个坑,就是java的List.add不是并发安全,或者不可重入的
导致我多线程给List data.add 里面塞了5个非空数据,也就是有个中间态 情况是
data.size() == 5
,但是data[0] == null
采用同步的方式add,解决该问题
synchronized (data) {
data.add(curData);
}
模拟数据
userId002, productId502, sellerId000, count = 2
userId002, productId502, sellerId000, count = 1
userId001, productId601, sellerId000, count = 1
userId002, productId602, sellerId000, count = 1
userId001, productId501, sellerId000, count = 3
userId002, productId702, sellerId000, count = 1
userId001, productId701, sellerId000, count = 1
上面代码跑出来结果如上(每次随机),可以看到前两条数据是由问题的,我们期望的是前两条数据应该是合并一条,而count == 3的
解决方案
如果把三个条件,用户id、商品id、卖家id,都当成锁的条件,所有购物车请求都是排队同步的是非常低效的
所以可以排队同一个用户的所有请求
引入可重入锁,再用一个hashmap,key是userId,而value是对应的锁
在Simulate类中引入单例,
//同步锁
private static HashMap<String, Object> lockMap = new HashMap<String, Object>();
锁的逻辑如下
Object lock = lockMap.get(cart.getUserId());
if(null == lock) {
synchronized (lockMap) {
lock = lockMap.get(cart.getUserId());
if (null == lock) {
lock = new Object();
lockMap.put(cart.getUserId(), lock);
}
}
}
synchronized (lock) {
int index = found(cart, data);
if (-1 != index) {
Cart curData = (Cart) data.get(index);
int count = curData.getCount();
curData.setCount(count + 1);
} else {
Cart curData = new Cart(cart.getUserId(), cart.getProductId(), cart.getSellerId());
curData.setCount(1);
synchronized (data) {
data.add(curData);
}
}
}
唯一注意的是hashmap.put也是多线程不安全的,所以在同步的时候,需要再次读
也就是读后写,写需要同步,但是写之前还需要读一次