• 分布式中的常见问题


    全局ID生成器

     

     代码实现:

    1. @Component
    2. public class RedisIDGenerator {
    3. //获取2022-01-01 00:00:00的时间戳为1640995200L
    4. private static final long BEGIN_TIMESTAMP = 1640995200L;
    5. private static final int COUNT_BITS = 32;
    6. @Autowired
    7. private StringRedisTemplate stringRedisTemplate;
    8. /**
    9. * 符号位 时间戳(31 bit) 序列号(32bit)
    10. * 0 - 00000000 00000000 00000000 0000000 - 00000000 00000000 00000000 00000000
    11. */
    12. public long nextId(String keyPrefix) {
    13. //1.生成时间戳
    14. LocalDateTime now = LocalDateTime.now();
    15. long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
    16. long timestamp = nowSecond - BEGIN_TIMESTAMP;
    17. //2.生产序列号
    18. //2.1获取当天的日期,请求到天
    19. // 一个key是有数量上限的,所以添加上当天的日期,就减少了超过上限的概率
    20. //yyyy:MM:dd 因为Redis可以根据:有层级关系,则可以统计每天,每月和每年的数量
    21. String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
    22. //2.2自增长
    23. Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
    24. //3.拼接返回
    25. return timestamp << COUNT_BITS | count;
    26. }
    27. public static void main(String[] args) {
    28. LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
    29. System.out.println(time.toEpochSecond(ZoneOffset.UTC));
    30. }
    31. }

    超卖问题

     

     

     使用乐观锁解决超卖问题:

    假设sql: update order set num = num -1 where num = ?; 

    这个存在一个问题,多个人来同时购买时,会出现库存依旧还有,但是很多购买失败的问题,则修改sql

    update order set num = num -1 where num >0

    一人一单

    单机

    1. public interface SinglePay {
    2. void buyOrder(Long userId);
    3. String createOrder(Long userId);
    4. }
    5. @Service
    6. public class SinglePayImpl implements SinglePay {
    7. public void buyOrder(Long userId) {
    8. //.....其他操作
    9. //synchronized放在这里保护整个事务在同步块内
    10. synchronized (userId.toString().intern()) {
    11. //因为createOrder方法存在事务,如果直接this.createOrder的调用则用的spring中的SinglePayUtil对象
    12. //不是代理对象则事务将不会生效,则不能直接调用createOrder方法,
    13. // createOrder(userId);
    14. // 需要拿到代理对象,调用代理对象createOrder方法,事务才会生效
    15. SinglePay proxy = (SinglePay) AopContext.currentProxy();
    16. proxy.createOrder(userId);
    17. }
    18. }
    19. //每人只能下一单
    20. //synchronized不放在方法上所有的用户公用这个类的同一个锁,影响性能
    21. //则用在userId上
    22. @Transactional
    23. public String createOrder(Long userId) {
    24. /**
    25. * Long是一个对象,但相同值得Long每一次对象都是不同的,
    26. * 所以要对值加锁,但是toString的底层是new String所以还是对象加锁
    27. * 所以要调用String的intern方法,再常量池中找值相同的引用,则所有都一样
    28. * 但是synchronized放在这里也有问题, Transaction的事务是在方法结束时才会提交,
    29. * 在并发的情况下也会存在事务还没提交,锁已经释放,存在可能同一个ID进入同步块的情况,所以要放在整个方法的外面
    30. * 保证事务提交后,才能再次进入此方法
    31. */
    32. // synchronized (userId.toString().intern()){
    33. //查询数据库order中userId是否存在记录,存在则不让购买
    34. int count = queryOrderCount(userId);
    35. if (count > 0) {
    36. //已买过购买失败
    37. return "fail";
    38. }
    39. boolean success = order_count(userId);
    40. if (!success) {
    41. return "库存不足";
    42. }
    43. saveOrder(userId);
    44. return "成功";
    45. // }
    46. }
    47. private void saveOrder(Long userId) {
    48. //模拟提交订单插入数据库
    49. //insert into order values(userId.....);
    50. }
    51. private boolean order_count(Long userId) {
    52. //修改库存
    53. //update order_count set count = count -1 where userId =? and count >0;
    54. return true;
    55. }
    56. private int queryOrderCount(Long userId) {
    57. //模拟查询数据库
    58. //select count(*) from order where userId = ?
    59. return 0;
    60. }
    61. }

    Note:使用代理对象需要配置

    • 引入依赖
    1. <dependency>
    2. <groupId>org.aspectjgroupId>
    3. <artifactId>aspectjweaverartifactId>
    4. dependency>
    1. //exposeProxy默认是false,不暴露代理对象,需要设置为true,code中才能获取代理对象
    2. @EnableAspectJAutoProxy(exposeProxy = true)

     分布式

    单机的情况下虽然利用锁解决了一人一单的问题, 但是在分布式的情况下,锁只能保证在自己jvm中不会同一个userId进入代码块,多个jvm中就不能保证

     分布式锁

     

     

     问题1: 业务时间过长导致锁超时自动释放,其他线程获取到锁,进行操作时,前一个线程完成业务删除了锁,导致多个线程同时操作需要同步的业务。

     解决方案:Redis加锁时,value为当前线程ID,则删除时判断锁的value是否是自己的线程id,如果不是则不删除。但是多个jvm生成的线程ID也可能相同,所以可以在线程ID和UUID进行拼接,减少重复的概率。

    1. @Component
    2. public class SimpleRedisLock implements ILock {
    3. private static final String KEY_PREFIX = "lock:";
    4. //不带横线的uuid
    5. private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
    6. @Autowired
    7. private StringRedisTemplate stringRedisTemplate;
    8. @Override
    9. public boolean tryLock(String key, long timeoutSec) {
    10. //获取当前线程的ID作为和 UUID 拼接作为value
    11. String threadId = ID_PREFIX + Thread.currentThread().getId();
    12. //获取锁
    13. Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(
    14. KEY_PREFIX + key, threadId, timeoutSec, TimeUnit.SECONDS);
    15. //可能存在拆箱问题
    16. return Boolean.TRUE.equals(success);
    17. }
    18. @Override
    19. public void unlock(String key) {
    20. String threadId = stringRedisTemplate.opsForValue().get(KEY_PREFIX + key);
    21. if (StrUtil.isEmpty(threadId)){
    22. return;
    23. }
    24. String currentThreadId = ID_PREFIX + Thread.currentThread().getId();
    25. if (currentThreadId.equals(threadId)){
    26. stringRedisTemplate.delete(KEY_PREFIX + key);
    27. }
    28. }
    29. }
    30. /**
    31. * 实现分布式锁
    32. */
    33. @Service
    34. public class DistributePayImpl implements SinglePay {
    35. @Autowired
    36. SimpleRedisLock simpleRedisLock;
    37. public void buyOrder(Long userId) {
    38. String lockKey = "order:" + userId;
    39. boolean isLock = simpleRedisLock.tryLock(lockKey, 10);
    40. if (!isLock) {
    41. //获取锁失败,返回错误或重试
    42. //"一人只能下一单"
    43. return;
    44. }
    45. try {
    46. SinglePay proxy = (SinglePay) AopContext.currentProxy();
    47. proxy.createOrder(userId);
    48. } finally {
    49. simpleRedisLock.unlock(lockKey);
    50. }
    51. }
    52. @Transactional
    53. public String createOrder(Long userId) {
    54. //查询数据库order中userId是否存在记录,存在则不让购买
    55. int count = queryOrderCount(userId);
    56. if (count > 0) {
    57. //已买过购买失败
    58. return "fail";
    59. }
    60. boolean success = order_count(userId);
    61. if (!success) {
    62. return "库存不足";
    63. }
    64. saveOrder(userId);
    65. return "成功";
    66. // }
    67. }
    68. private void saveOrder(Long userId) {
    69. //模拟提交订单插入数据库
    70. //insert into order values(userId.....);
    71. }
    72. private boolean order_count(Long userId) {
    73. //修改库存
    74. //update order_count set count = count -1 where userId =? and count >0;
    75. return true;
    76. }
    77. private int queryOrderCount(Long userId) {
    78. //模拟查询数据库
    79. //select count(*) from order where userId = ?
    80. return 0;
    81. }
    82. }

     问题2:当第一个线程,业务操作完成后,并且判断了Redis锁的value是自己的线程ID,准备进行删除时,遇到了阻塞(eg:垃圾回收)导致超时自动释放,并且有另一个线程已经获取到锁,并且开始执行自己的业务时,第一个线程阻塞完成了继续删除锁的操作,则删除了另一线程的锁

     解决方案需要获取锁,判断是否为自己的锁 删除锁具有原子性。可以使用Lua脚本

     

     

    1. --获取锁中的线程标示 get key
    2. local id = redis.call('get',KEYS[1])
    3. --比较线程标示和锁中的标示是否一致
    4. if(id == ARGV[1]) then
    5. --释放锁
    6. return redis.call('del',KEYS[1])
    7. end
    8. return 0

     代码实现:

    代码中创建Lua文件,idea中需要安装EmmyLua插件 

    1. //接受lua脚本
    2. private static final DefaultRedisScript UNLOCK_SCRIPT;
    3. static {
    4. UNLOCK_SCRIPT = new DefaultRedisScript<>();
    5. UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
    6. UNLOCK_SCRIPT.setResultType(Long.class);
    7. }
    8. /**
    9. * 基于Lua脚本解锁
    10. */
    11. @Override
    12. public void unlock(String key) {
    13. //使用execute执行脚本指令
    14. stringRedisTemplate.execute(
    15. UNLOCK_SCRIPT,
    16. Collections.singletonList(KEY_PREFIX + key),
    17. ID_PREFIX + Thread.currentThread().getId());
    18. }

     分布式锁依然存在的问题:

     使用Redisson替换自己的实现的分布式锁

  • 相关阅读:
    uni-app rich-text组件富文本图片展示不全问题
    H桥级联型五电平三相逆变器Simulink仿真模型
    Linux基本指令集合
    CPU GPU TPU NPU 的一些概念 和 使用
    web3.js:使用eth包
    Linux安装Kafka单机版本
    解析Redis缓存穿透、击穿和雪崩问题及解决方案
    踩坑笔记: 基于 rust-analyzer 在 vscode 中进行 rust 开发配置问题
    区块链技术的应用场景和优势
    java面试官如何面试别人
  • 原文地址:https://blog.csdn.net/qq_33753147/article/details/126414735