数据库并发锁

问题

之前让同事修改需求,实现商品列表页上的每个购物车图标按钮,点击能够局部刷新用户的购物车
购物车数据库是这样设计的,每条数据主要跟用户id、商家id、商品id有关;另外还有一个字段是count,也就是说用户对某个商品的购物车数据就是该count

  1. 如果查到三条信息一致的记录,那么就update原来的那条记录 count + 1
  2. 如果没有查到这样的记录,那么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也是多线程不安全的,所以在同步的时候,需要再次读
也就是读后写,写需要同步,但是写之前还需要读一次