> ## Content Index
> Fetch the complete content index at: https://blog.hacking.pub/llms.txt
> Use this file to discover other available public pages before exploring further.

# 数据库并发锁
- URL: https://blog.hacking.pub/2017/04/23/shu-ju-ku-bing-fa-suo/
- Published: 2017-04-23T07:51:29.000Z
- Updated: 2017-04-25T06:52:57.000Z
- Author: chainhelen
- Tags: 数据库, lock, 并发, 多线程, java

#### 问题

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

1. 如果查到三条信息一致的记录，那么就update原来的那条记录 count + 1
2. 如果没有查到这样的记录，那么insert一条 count = 1 的数据

问题就是这个insert；不然只有update的话，按照原来我写的文章 [mysql数据一致性闲谈](https://blog.hacking.pub/2016/12/26/mysqlshu-ju-yi-zhi-xing-xian-tan/)，搞一个悲观锁就ok了

#### 问题场景

当一个新的用户，连续两次快速点击同一个商品的购物车按钮（前端没有做同步，允许并发），后台的ssh结构会导致两个线程都进入了上述`情况2`中

#### 简化问题

因为我对java不是很熟，我们来把原有问题简化成如下问题

```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也是多线程不安全的，所以在同步的时候，需要再次读  
也就是`读后写，写需要同步，但是写之前还需要读一次`