1.分布式并发问题
提交订单:商品超卖问题
2.如何解决分布式并发问题呢 ?
使⽤redis实现分布式锁
3.使⽤Redis实现分布式锁-代码实现
@Transactional
public Map<String,String> addOrders(Orders orders, String cartId) {
logger.info("增加订单");
// 查询购物车列表
ArrayList<Integer> integers = new ArrayList<>();
String[] split = cartId.split(",");
for (String s : split) {
integers.add(Integer.parseInt(s));
}
// 根据用户在购物车列表中选择的购物车记录id查询到对应的购物记录
List<ShoppingCartVo> shoppingCartVos = shoppingCartMapper.listShoppingCartByCartId(integers);
boolean isTrue = true;
// 设置数组,为后面释放锁做准备 数组里是被锁住的id ["1","2","null"]
String[] skuIds = new String[shoppingCartVos.size()];
// 存储redis中的内容为后面验证是否是该线程的锁做准备
HashMap<String, String> values = new HashMap<>();
for (int i = 0; i < shoppingCartVos.size(); i++) {
ShoppingCartVo shoppingCartVo = shoppingCartVos.get(i);
String skuId = shoppingCartVo.getSkuId();
String value = UUID.randomUUID().toString();
try {
Boolean sku = stringRedisTemplate.boundValueOps(skuId).setIfAbsent(value,10, TimeUnit.MINUTES);
if (sku){
skuIds[i] = skuId;
values.put(skuId,value);
}
isTrue = isTrue && sku;
} catch (Exception e) {
e.printStackTrace();
}
}
//相当于加锁
if (isTrue){
try {
// 比较库存,当第一次查询购物车记录之后,在加索成功之前,可能被其他的并发线程修改库存
shoppingCartVos = shoppingCartMapper.listShoppingCartByCartId(integers);
boolean isEnough = true;
// 查询库存是否充足
String untitled = "";
for (ShoppingCartVo shoppingCartVo : shoppingCartVos) {
if (Integer.parseInt(shoppingCartVo.getCartNum()) > shoppingCartVo.getStock()) {
isEnough = false;
break;
}
untitled = untitled + shoppingCartVo.getProductName();
}
// 库存充足,进行 添加订单,保存快照,修改库存,删除购物车
if (isEnough) {
// 生成订单id
// 保存订单
String orderId = UUID.randomUUID().toString().replace("-", "");
orders.setUntitled(untitled);
orders.setOrderId(orderId);
orders.setStatus("1");
orders.setCreateTime(new Date());
int insert = ordersMapper.insert(orders);
if (insert > 0) {
// 生成订单快照
for (ShoppingCartVo sc : shoppingCartVos) {
OrderItem orderItem = new OrderItem();
String itemId = System.currentTimeMillis() + "" + (new Random().nextInt(8999) + 1000);
orderItem.setItemId(itemId);
orderItem.setOrderId(orderId);
orderItem.setProductId(sc.getProductId());
orderItem.setProductImg(sc.getUrl());
orderItem.setSkuId(sc.getSkuId());
orderItem.setSkuName(sc.getSkuName());
orderItem.setProductPrice(sc.getProductPrice());
orderItem.setProductName(sc.getProductName());
int cnum = Integer.parseInt(sc.getCartNum());
orderItem.setBuyCounts(cnum);
orderItem.setTotalAmount(BigDecimal.valueOf(sc.getSellPrice() * cnum));
orderItem.setBasketDate(new Date());
orderItem.setBuyTime(new Date());
orderItem.setIsComment(0);
orderItemMapper.insert(orderItem);
}
// 更新库存
for (ShoppingCartVo sc : shoppingCartVos) {
String skuId = sc.getSkuId();
int newStoken = sc.getStock() - Integer.parseInt(sc.getCartNum());
ProductSku productSku = new ProductSku();
productSku.setSkuId(skuId);
productSku.setStock(newStoken);
int k = productSkuMapper.updateByPrimaryKeySelective(productSku);
}
// 删除购物车
for (Integer integer : integers) {
shoppingCartMapper.deleteByPrimaryKey(integer);
}
HashMap<String, String> map = new HashMap<>();
map.put("orderId", orderId);
map.put("productNames", untitled);
System.out.println("map = " + map.get("productNames"));
System.out.println("map = " + map.get("orderId"));
return map;
}
return null;
} else {
return null;
}
} catch (NumberFormatException e) {
e.printStackTrace();
} finally {
//释放锁
for (int i = 0; i < skuIds.length; i++) {
String skuId = skuIds[i];
if (skuIds != null && !"".equals(skuIds)){
// 释放锁之前,先查询当前的value
String s = stringRedisTemplate.boundValueOps(skuId).get();
// 如果从redis中获取 的值与生成的value是一至的,则表示此锁是当前线程的锁
if (s != null && s.equals(values.get(skuId))){
stringRedisTemplate.delete(skuId);
}
}
}
}
}else {
// 表示加锁失败,天大订单失败
// 当加锁失败时,有可能部分商品已经加锁,要释放锁定的部分商品
for (int i = 0; i < skuIds.length; i++) {
String skuId = skuIds[i];
if (skuIds != null && !"".equals(skuIds)){
// 释放锁之前,先查询当前的value
String s = stringRedisTemplate.boundValueOps(skuId).get();
// 如果从redis中获取 的值与生成的value是一至的,则表示此锁是当前线程的锁
if (s != null && s.equals(values.get(skuId))){
stringRedisTemplate.delete(skuId);
}
}
}
}
return null;
}
问题:
1.如果订单中部分商品加锁成功,但是某⼀个加锁失败,导致最终加锁状态失败——需要对
已经锁定的部分商品释放锁
2.在成功加锁之前,我们根据购物⻋记录的id查询了购物⻋记录(包含商品库存),能够直接
使⽤这个库存进⾏库存校验?
——不能,因为在查询之后加锁之前可能被并发的线程修改了库存;因此在进⾏库存⽐较之
前需要重新查询库存。
3.当当前线程加锁成功之后,执⾏添加订单的过程中,如果当前线程出现异常导致⽆法释放
锁,这个问题⼜该如何解决呢?
4.解决因线程异常导致⽆法释放锁的问题
解决⽅案:在对商品进⾏加锁时,设置过期时间,这样⼀来及时线程出现故障⽆法释放
锁,在过期时间结束时也会⾃动“释放锁”
问题:当给锁设置了过期时间之后,如果当前线程t1因为特殊原因,在锁过期前没有完成业
务执⾏,将会释放锁,同时其他线程(t2)就可以成功加锁了,当t2加锁成功之后,t1执⾏结
束释放锁就会释放t2的锁,就会导致t2在⽆锁状态下执⾏业务。
5.解决因t1过期释放t2锁的问题
在加锁的时候,为每个商品设置唯⼀的value
在释放锁的时候,先获取当前商品在redis中对应的value,如果获取的值与当前value相
同,则释放锁
5.看⻔狗机制
看⻔⼝线程:⽤于给当前key延⻓过期时间,保证业务线程正常执⾏的过程中,锁不会过期。