分布式锁
1.分布式并发问题
提交订单: 商品超卖问题
2.如何解决分布式并发问题呢 ?
使⽤redis实现分布式锁
在进行添加之前就向redis中添加一个 key为 skuId 的数据 完成操作之后就删除 数据 实现锁的机制
-
1.如果订单中部分商品加锁成功,但是某⼀个加锁失败,导致最终加锁状态失败——需要对已经锁定的部分商品释放锁
-
2.在成功加锁之前,我们根据购物⻋记录的id查询了购物⻋记录(包含商品库存),能够直接使⽤这个库存进⾏库存校验?
- ——不能,因为在查询之后加锁之前可能被并发的线程修改了库存;因此在进⾏库存⽐较之前需要重新查询库存。
代码如下:
@Override
@Transactional()
public Map<String, String> addOrder(String cids, Orders order) throws SQLException {
HashMap<String, String> map = new HashMap<>();
//根据cids查询当前订单中关联的购物车详情 和库存
String[] arr = cids.split(",");
List<Integer> cidsList = new ArrayList<>();
for (String s : arr) {
cidsList.add(Integer.parseInt(s));
}
List<ShoppingCartVO> list = shoppingCartMapper.selectShopcartByCids(cidsList);
//从购物车信息中获取到要购买的skuId 以id为key写道redis中
boolean isLock = true;
//记录已经存入redis中的id
String[] skuIds = new String[list.size()];
for (int i = 0; i < list.size(); i++) {
String skuId = list.get(i).getSkuId();
//设置时间是防止在加完锁之后 线程出现故障 导致锁不能释放
Boolean ifAbsent = stringRedisTemplate.boundValueOps(skuId).setIfAbsent("随便", 10, TimeUnit.SECONDS);
if (ifAbsent) {
skuIds[i] = skuId;
values.put(skuId, value);
}
isLock = isLock && ifAbsent;
}
//加锁
if (isLock) {
//校验库存
//保存订单
//扣减库存
//删除购物车 购买成功之后 购物车自动删除
//释放锁
for (String skuId : skuIds) {
if (skuId != null && !"".equals(skuId)) {
stringRedisTemplate.delete(skuId);
}
}
} else {
//释放锁
for (String skuId : skuIds) {
if (skuId != null && !"".equals(skuId)) {
stringRedisTemplate.delete(skuId);
}
}
}
return null;
}
问题:
当当前线程加锁成功之后,执⾏添加订单的过程中,如果当前线程出现异常导致⽆法释放锁,这个问题⼜该如何解决呢?
3.解决因线程异常导致⽆法释放锁的问题
在对商品进⾏加锁时,设置过期时间,这样⼀来及时线程出现故障⽆法释放锁,在过期时间结束时也会⾃动“释放锁”
Boolean ifAbsent = stringRedisTemplate.boundValueOps(skuId).setIfAbsent(value, 10, TimeUnit.SECONDS);
问题:
当给锁设置了过期时间之后,如果当前线程t1因为特殊原因,在锁过期前没有完成业务执⾏,将会释放锁,同时其他线程(t2)就可以成功加锁了,当t2加锁成功之后,t1执⾏结束释放锁就会释放t2的锁,就会导致t2在⽆锁状态下执⾏业务。
4.解决因t1过期释放t2锁的问题
-
在加锁的时候,为每个商品设置唯⼀的value
-
在释放锁的时候,先获取当前商品在redis中对应的value,如果获取的值与当前value相同,则释放锁
问题:
当释放锁的时候,在查询并判断“这个锁是当前线程加的锁”成功之后,正要进⾏删除时锁过期了,并且被其他线程成功加锁,⼀样会导致当前线程删除其他线程的锁。
- Redis的操作都是原⼦性的
- 要解决如上问题,必须保证查询操作和删除操作的原⼦性——使⽤lua脚本
使⽤lua脚本
-
在resources⽬录下创建unlock.lua,编辑脚本:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
-
配置Bean加载lua脚本
@Bean public DefaultRedisScript<List> defaultRedisScript(){ DefaultRedisScript<List> defaultRedisScript = new DefaultRedisScript<>(); defaultRedisScript.setResultType(List.class); defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("unlock.lua"))); return defaultRedisScript; }
-
通过执⾏lua脚本解锁
@AutoWired private DefaultRedisScript defaultRedisScript; //执⾏lua脚本 List<String> keys = new ArrayList<>(); keys.add(skuId); List rs = stringRedisTemplate.execute(defaultRedisScript,keys , values.get(skuId)); System.out.println(rs.get(0));