1.分布式并发问题

提交订单:商品超卖问题

13、分布式锁 - 图1

2.如何解决分布式并发问题呢

使⽤redis实现分布式锁

13、分布式锁 - 图2

3.使用Redis实现分布式锁-代码实现

  1. @Transactional
  2. public Map<String,String> addOrder(String cids,Orders order) throws SQLException {
  3. logger.info("add order begin...");
  4. Map<String, String> map = null;
  5. //1.校验库存:根据cids查询当前订单中关联的购物⻋记录详情(包括库存)
  6. String[] arr = cids.split(",");
  7. List<Integer> cidsList = new ArrayList<>();
  8. for (int i = 0; i < arr.length; i++) {
  9. cidsList.add(Integer.parseInt(arr[i]));
  10. }
  11. //根据⽤户在购物⻋列表中选择的购物⻋记录的id 查询到对应的购物⻋记录
  12. List<ShoppingCartVO> list = shoppingCartMapper.selectShopcartByCids(cidsList);
  13. //从购物⻋信息中获取到要购买的 skuId(商品ID) 以skuId为key写到redis中: 1 2 3
  14. boolean isLock = true;
  15. String[] skuIds = new String[list.size()]; //记录已经锁定的商品的ID
  16. for (int i = 0; i <list.size() ; i++) {
  17. String skuId = list.get(i).getSkuId(); //订单中可能包含多个商品,每个skuId表示⼀个商品
  18. Boolean ifAbsent = stringRedisTemplate.boundValueOps(skuId).setIfAbsent("fmmall");
  19. if(ifAbsent){
  20. skuIds[i] = skuId;
  21. }
  22. isLock = isLock && ifAbsent;
  23. }
  24. //如果isLock为true,表示“加锁”成功
  25. if(isLock){
  26. try{
  27. //1.⽐较库存: 当第⼀次查询购物⻋记录之后,在加锁成功之前,可能被其他的并发线程修改库存
  28. List<ShoppingCartVO> list = shoppingCartMapper.selectShopcartByCids(cidsList);
  29. boolean f = true;
  30. String untitled = "";
  31. for (ShoppingCartVO sc : list) {
  32. if (Integer.parseInt(sc.getCartNum()) > sc.getSkuStock()) {
  33. f = false;
  34. }
  35. untitled = untitled + sc.getProductName() + ",";
  36. }
  37. if (f) {
  38. //2.添加订单
  39. //3.保存快照
  40. //4.修改库存
  41. //5.删除购物⻋
  42. map = new HashMap<>();
  43. logger.info("add order finished...");
  44. map.put("orderId", orderId);
  45. map.put("productNames", untitled);
  46. }
  47. }catch(Exception e){
  48. e.printStackTrance();
  49. }finally{
  50. //释放锁
  51. for (int m = 0; m < skuIds.length ; m++) {
  52. String skuId = skuIds[m];
  53. if(skuId!=null && !"".equals(skuId)){
  54. stringRedisTemplate.delete(skuId);
  55. }
  56. }
  57. }
  58. return map;
  59. }else{
  60. //表示加锁失败,订单添加失败
  61. // 当加锁失败时,有可能对部分商品已经锁定,要释放锁定的部分商品
  62. for (int i = 0; i < skuIds.length ;
  63. if(skuId!=null && !"".equals(skuId)){
  64. stringRedisTemplate.delete(skuId);
  65. }
  66. }
  67. return null;
  68. }
  69. }

问题:

1.如果订单中部分商品加锁成功,但是某⼀个加锁失败,导致最终加锁状态失败——需要对已经锁定的部分商品释
放锁

2.在成功加锁之前,我们根据购物车记录的id查询了购物车记录(包含商品库存),能够直接使用这个库存进行库
存校验?
——不能,因为在查询之后加锁之前可能被并发的线程修改了库存;因此在进行库存比较之前需要重新查询库存。

3.当当前线程加锁成功之后,执行添加订单的过程中,如果当前线程出现异常导致无法释放锁,这个问题又该如何
解决呢?

4.解决因线程异常导致无法释放锁的问题

解决⽅案:在对商品进⾏加锁时,设置过期时间,这样⼀来及时线程出现故障⽆法释放锁,在过期时间结束
时也会⾃动“释放锁”

13、分布式锁 - 图3

问题:当给锁设置了过期时间之后,如果当前线程t1因为特殊原因,在锁过期前没有完成业务执⾏,将会释放锁,
同时其他线程(t2)就可以成功加锁了,当t2加锁成功之后, t1执⾏结束释放锁就会释放t2的锁,就会导致t2在⽆锁
状态下执⾏业务。

5.解决因t1过期释放t2锁的问题

  • 在加锁的时候,为每个商品设置唯⼀的value
    13、分布式锁 - 图4
  • 在释放锁的时候,先获取当前商品在redis中对应的value,如果获取的值与当前value相同,则释放锁
    13、分布式锁 - 图5

问题:当释放锁的时候,在查询并判断“这个锁是当前线程加的锁”成功之后,正要进⾏删除时锁过期了,并且被其
他线程成功加锁,⼀样会导致当前线程删除其他线程的锁。

  • Redis的操作都是原⼦性的
  • 要解决如上问题,必须保证查询操作和删除操作的原⼦性——使⽤lua脚本

使⽤lua脚本

  • 在resources⽬录下创建unlock.lua,编辑脚本:

    1. if redis.call("get",KEYS[1]) == ARGV[1] then
    2. return redis.call("del",KEYS[1])
    3. else
    4. return 0
    5. end
  • 配置Bean加载lua脚本

    1. @Bean
    2. public DefaultRedisScript<List> defaultRedisScript(){
    3. DefaultRedisScript<List> defaultRedisScript = new DefaultRedisScript<>();
    4. defaultRedisScript.setResultType(List.class);
    5. defaultRedisScript.setScriptSource(new ResourceScriptSource(new
    6. ClassPathResource("unlock.lua")));
    7. return defaultRedisScript;
    8. }
  • 通过执⾏lua脚本解锁
    ```java @AutoWired private DefaultRedisScript defaultRedisScript;

//执⾏lua脚本 List keys = new ArrayList<>(); keys.add(skuId); List rs = stringRedisTemplate.execute(defaultRedisScript,keys , values.get(skuId)); System.out.println(rs.get(0));

  1. <a name="40ee3d3b"></a>
  2. ## 6.看门狗机制
  3. ![](https://s2.loli.net/2021/12/31/yVm8jQlC1iOgE2t.png#crop=0&crop=0&crop=1&crop=1&id=N7SDs&originHeight=199&originWidth=1254&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
  4. 看门狗线程:⽤于给当前key延长过期时间,保证业务线程正常执⾏的过程中,锁不会过期
  5. <a name="77c2eba0"></a>
  6. ## 7.分布式锁框架-Redisson
  7. > 基于Redis+看门狗机制的分布式锁框架
  8. <a name="3f1b6e1a"></a>
  9. ### 7.1 Redisson介绍
  10. Redisson在基于NIO的Netty框架上,充分的利⽤了Redis键值数据库提供的⼀系列优势,在Java实⽤⼯具包中常⽤接<br />⼝的基础上,为使⽤者提供了⼀系列具有分布式特性的常⽤⼯具类。使得原本作为协调单机多线程并发程序的⼯具<br />包获得了协调分布式多机多线程并发系统的能⼒,⼤⼤降低了设计和研发⼤规模分布式系统的难度。同时结合各富<br />特⾊的分布式服务,更进⼀步简化了分布式环境中程序相互之间的协作
  11. <a name="b05f9e51"></a>
  12. ### 7.2 在SpringBoot应用中使用Redisson
  13. - 添加依赖
  14. ```xml
  15. <dependency>
  16. <groupId>org.redisson</groupId>
  17. <artifactId>redisson</artifactId>
  18. <version>3.12.0</version>
  19. </dependency
  • 配置yml

    1. redisson:
    2. addr:
    3. singleAddr:
    4. host: redis://47.96.11.185:6370
    5. password: 12345678
    6. database: 0
  • 配置RedissonClient

    1. @Configuration
    2. public class RedissonConfig {
    3. @Value("${redisson.addr.singleAddr.host}")
    4. private String host;
    5. @Value("${redisson.addr.singleAddr.password}")
    6. private String password;
    7. @Value("${redisson.addr.singleAddr.database}")
    8. private int database;
    9. @Bean
    10. public RedissonClient redissonClient(){
    11. Config config = new Config();
    12. config.useSingleServer().setAddress(host).setPassword(password).setDatabase(database);
    13. return Redisson.create(config);
    14. }
    15. }
  • 在秒杀业务实现中注⼊RedissonClient对象

7.3 Redisson工作原理

  • “看门狗” | Redisson⼯作原理图 | | —- | | 13、分布式锁 - 图6 |

7.4 Redisson使用扩展

7.4.1 Redisson单机连接

  • application.yml

    1. redisson:
    2. addr:
    3. singleAddr:
    4. host: redis://47.96.11.185:6370
    5. password: 12345678
    6. database: 0
  • RedissonConfig

    1. @Configuration
    2. public class RedissonConfig {
    3. @Value("${redisson.addr.singleAddr.host}")
    4. private String host;
    5. @Value("${redisson.addr.singleAddr.password}")
    6. private String password;
    7. @Value("${redisson.addr.singleAddr.database}")
    8. private int database;
    9. @Bean
    10. public RedissonClient redissonClient(){
    11. Config config = new Config();
    12. config.useSingleServer().setAddress(host).setPassword(password).setDatabase(database);
    13. return Redisson.create(config);
    14. }
    15. }

7.4.2 Redisson集群连接

  • application.yml

    1. redisson:
    2. addr:
    3. cluster:
    4. hosts: redis://47.96.11.185:6370,...,redis://47.96.11.185:6373
    5. password: 12345678
  • RedissonConfig——RedissonClient对象

    1. @Configuration
    2. public class RedissonConfig {
    3. @Value("${redisson.addr.cluster.hosts}")
    4. private String hosts;
    5. @Value("${redisson.addr.cluster.password}")
    6. private String password;
    7. /**
    8. * 集群模式
    9. * @return
    10. */
    11. @Bean
    12. public RedissonClient redissonClient(){
    13. Config config = new Config();
    14. config.useClusterServers().addNodeAddress(hosts.split("[,]"))
    15. .setPassword(password)
    16. .setScanInterval(2000)
    17. .setMasterConnectionPoolSize(10000)
    18. .setSlaveConnectionPoolSize(10000);
    19. return Redisson.create(config);
    20. }
    21. }

7.4.3 Redisson主从连接

  • application.yml

    1. redisson:
    2. addr:
    3. masterAndSlave:
    4. masterhost: redis://47.96.11.185:6370
    5. slavehosts: redis://47.96.11.185:6371,redis://47.96.11.185:6372
    6. password: 12345678
    7. database: 0
  • RedissonConfig —- RedissonClient

    1. @Configuration
    2. public class RedissonConfig3 {
    3. @Value("${redisson.addr.masterAndSlave.masterhost}")
    4. private String masterhost;
    5. @Value("${redisson.addr.masterAndSlave.slavehosts}")
    6. private String slavehosts;
    7. @Value("${redisson.addr.masterAndSlave.password}")
    8. private String password;
    9. @Value("${redisson.addr.masterAndSlave.database}")
    10. private int database;
    11. /**
    12. * 主从模式
    13. * @return
    14. */
    15. @Bean
    16. public RedissonClient redissonClient(){
    17. Config config = new Config();
    18. config.useMasterSlaveServers()
    19. .setMasterAddress(masterhost)
    20. .addSlaveAddress(slavehosts.split("[,]"))
    21. .setPassword(password)
    22. .setDatabase(database)
    23. .setMasterConnectionPoolSize(10000)
    24. .setSlaveConnectionPoolSize(10000);
    25. return Redisson.create(config);
    26. }
    27. }

7.5 分布式锁总结

7.5.1 分布式锁特点

1、互斥性 和我们本地锁⼀样互斥性是最基本,但是分布式锁需要保证在不同节点的不同线程的互斥。

2、可重⼊性 同⼀个节点上的同⼀个线程如果获取了锁之后那么也可以再次获取这个锁。

3、锁超时 和本地锁⼀样支持锁超时,加锁成功之后设置超时时间,以防止线程故障导致不释放锁,防止死锁。

4、高效,高可用 加锁和解锁需要⾼效,同时也需要保证高可用防止分布式锁失效,可以增加降级。

redission是基于redis的, redis的故障就会导致redission锁的故障,因此redission⽀持单节点redis、 reids主从、 reids集群

5、支持阻塞和非阻塞 和 ReentrantLock ⼀样⽀持 lock 和 trylock 以及 tryLock(long timeOut)。

7.5.2 锁的分类

1、乐观锁与悲观锁

  • 乐观锁
  • 悲观锁

2、可重⼊锁和非可重⼊锁

  • 可重⼊锁:当在⼀个线程中第⼀次成功获取锁之后,在此线程中就可以再次获取
  • ⾮可重⼊锁

3、公平锁和⾮公平锁

  • 公平锁:按照线程的先后顺序获取锁
  • ⾮公平锁:多个线程随机获取锁

4、阻塞锁和⾮阻塞锁

  • 阻塞锁:不断尝试获取锁,直到获取到锁为⽌
  • ⾮阻塞锁:如果获取不到锁就放弃,但可以⽀持在⼀定时间段内的重试
    ——在⼀段时间内如果没有获取到锁就放弃

7.5.3 Redission的使用

1、获取锁——公平锁和非公平锁

  1. //获取公平锁
  2. RLock lock = redissonClient.getFairLock(skuId);
  3. //获取⾮公平锁
  4. RLock lock = redissonClient.getLock(skuId);

2、加锁——阻塞锁和⾮阻塞锁

  1. //阻塞锁(如果加锁成功之后,超时时间为30s;加锁成功开启看⻔狗,剩5s延⻓过期时间)
  2. lock.lock();
  3. //阻塞锁(如果加锁成功之后,设置⾃定义20s的超时时间)
  4. lock.lock(20,TimeUnit.SECONDS);
  5. //⾮阻塞锁(设置等待时间为3s;如果加锁成功默认超时间为30s)
  6. boolean b = lock.tryLock(3,TimeUnit.SECONDS);
  7. //⾮阻塞锁(设置等待时间为3s;如果加锁成功设置⾃定义超时间为20s)
  8. boolean b = lock.tryLock(3,20,TimeUnit.SECONDS);

3、释放锁

  1. lock.unlock();

4、应⽤示例

  1. //公平⾮阻塞锁
  2. RLock lock = redissonClient.getFairLock(skuId);
  3. boolean b = lock.tryLock(3,20,TimeUnit.SECONDS);

8.分布式锁释放锁代码优化

  • 伪代码

    1. HashMap map = null;
    2. 加锁
    3. try{
    4. if(isLock){
    5. 校验库存
    6. if(库存充⾜){
    7. 保存订单
    8. 保存快照
    9. 修改库存
    10. 删除购物⻋
    11. map = new HashMap();
    12. ...
    13. }
    14. }
    15. }catch(Exception e){
    16. e.printStackTrace();
    17. }finally{
    18. 释放锁
    19. }
    20. return map;
  • Java代码实现
    ```java /**

  • 保存订单业务 */ @Transactional public Map addOrder(String cids, Orders order) throws SQLException { logger.info(“add order begin…”); Map map = null;

    //1.校验库存:根据cids查询当前订单中关联的购物⻋记录详情(包括库存) String[] arr = cids.split(“,”); List cidsList = new ArrayList<>(); for (int i = 0; i < arr.length; i++) {

    1. cidsList.add(Integer.parseInt(arr[i]));

    }

    //根据⽤户在购物⻋列表中选择的购物⻋记录的id 查询到对应的购物⻋记录 List list = shoppingCartMapper.selectShopcartByCids(cidsList);

    //加锁 boolean isLock = true; String[] skuIds = new String[list.size()]; Map locks = new HashMap<>(); //⽤于存放当前订单的锁 for (int i = 0; i < list.size(); i++) {

    1. String skuId = list.get(i).getSkuId();
    2. boolean b = false;
    3. try {
    4. RLock lock = redissonClient.getLock(skuId);
    5. b = lock.tryLock(10, 3, TimeUnit.SECONDS);
    6. if (b) {
    7. skuIds[i] = skuId;
    8. locks.put(skuId, lock);
    9. }
    10. } catch (InterruptedException e) {
    11. e.printStackTrace();
    12. }
    13. isLock = isLock & b;

    } //如果isLock为true,表示“加锁”成功 try {

    1. if (isLock){
    2. //1.检验库存
    3. boolean f = true;
    4. String untitled = "";
    5. list = shoppingCartMapper.selectShopcartByCids(cidsList);
    6. for (ShoppingCartVO sc : list) {
    7. if (Integer.parseInt(sc.getCartNum()) > sc.getSkuStock()) {
    8. f = false;
    9. }
    10. untitled = untitled + sc.getProductName() + ",";
    11. }
    12. if (f) {
    13. //如果库存充⾜,则进⾏下订单操作
    14. logger.info("product stock is OK...");
    15. //2.保存订单
    16. order.setUntitled(untitled);
    17. order.setCreateTime(new Date());
    18. order.setStatus("1");
    19. //⽣成订单编号
    20. String orderId = UUID.randomUUID().toString().replace("-", "");
    21. order.setOrderId(orderId);
    22. int i = ordersMapper.insert(order);
    23. //3.⽣成商品快照
    24. for (ShoppingCartVO sc : list) {
    25. int cnum = Integer.parseInt(sc.getCartNum());
    26. String itemId = System.currentTimeMillis() + "" + (new Random().nextInt(89999) + 10000);
    27. OrderItem orderItem = new OrderItem(itemId, orderId, sc.getProductId(), sc.getProductName(), sc.getProductImg(), sc.getSkuId(), sc.getSkuName(), new BigDecimal(sc.getSellPrice()), cnum, new BigDecimal(sc.getSellPrice() * cnum), new Date(), new Date(), 0);
    28. orderItemMapper.insert(orderItem);
    29. //增加商品销量
    30. }
    31. //4.扣减库存:根据套餐ID修改套餐库存量
    32. for (ShoppingCartVO sc : list) {
    33. String skuId = sc.getSkuId();
    34. int newStock = sc.getSkuStock() - Integer.parseInt(sc.getCartNum());
    35. ProductSku productSku = new ProductSku();
    36. productSku.setSkuId(skuId);
    37. productSku.setStock(newStock);
    38. productSkuMapper.updateByPrimaryKeySelective(productSku);
    39. }
    40. //5.删除购物⻋:当购物⻋中的记录购买成功之后,购物⻋中对应做删除操作
    41. for (int cid : cidsList) {
    42. shoppingCartMapper.deleteByPrimaryKey(cid);
    43. }
    44. map = new HashMap<>();
    45. logger.info("add order finished...");
    46. map.put("orderId", orderId);
    47. map.put("productNames", untitled);
    48. }
    49. }

    }catch (Exception e){

    1. e.printStackTrace();

    }finally {

    1. //释放锁
    2. for (int i = 0; i < skuIds.length; i++) {
    3. String skuId = skuIds[i];
    4. if (skuId != null && !"".equals(skuId)) {
    5. locks.get(skuId).unlock();
    6. System.out.println("-----------------------unlock");
    7. }
    8. }

    } return map; } ```