分布式中的常见问题

全局ID生成器

 

 代码实现:

@Component
public class RedisIDGenerator {

    //获取2022-01-01 00:00:00的时间戳为1640995200L
    private static final long BEGIN_TIMESTAMP = 1640995200L;

    private static final int COUNT_BITS = 32;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     *  符号位            时间戳(31 bit)                         序列号(32bit)
     *  0     -   00000000 00000000 00000000 0000000  -  00000000 00000000 00000000 00000000
     */
    public long nextId(String keyPrefix) {
        //1.生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIMESTAMP;

        //2.生产序列号
        //2.1获取当天的日期,请求到天
        // 一个key是有数量上限的,所以添加上当天的日期,就减少了超过上限的概率
        //yyyy:MM:dd 因为Redis可以根据:有层级关系,则可以统计每天,每月和每年的数量
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        //2.2自增长
        Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        //3.拼接返回
        return timestamp << COUNT_BITS | count;
    }

    public static void main(String[] args) {
        LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
        System.out.println(time.toEpochSecond(ZoneOffset.UTC));
    }
}

超卖问题

 

 

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

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

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

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

一人一单

单机

public interface SinglePay {
    void buyOrder(Long userId);
    String createOrder(Long userId);
}

@Service
public class SinglePayImpl implements SinglePay {

    public void buyOrder(Long userId) {
        //.....其他操作
        //synchronized放在这里保护整个事务在同步块内
        synchronized (userId.toString().intern()) {
            //因为createOrder方法存在事务,如果直接this.createOrder的调用则用的spring中的SinglePayUtil对象
            //不是代理对象则事务将不会生效,则不能直接调用createOrder方法,
            //   createOrder(userId);
            // 需要拿到代理对象,调用代理对象createOrder方法,事务才会生效
            SinglePay proxy = (SinglePay) AopContext.currentProxy();
            proxy.createOrder(userId);
        }
    }

    //每人只能下一单
    //synchronized不放在方法上所有的用户公用这个类的同一个锁,影响性能
    //则用在userId上
    @Transactional
    public String createOrder(Long userId) {
        /**
         *   Long是一个对象,但相同值得Long每一次对象都是不同的,
         *   所以要对值加锁,但是toString的底层是new String所以还是对象加锁
         *   所以要调用String的intern方法,再常量池中找值相同的引用,则所有都一样
         *   但是synchronized放在这里也有问题, Transaction的事务是在方法结束时才会提交,
         *   在并发的情况下也会存在事务还没提交,锁已经释放,存在可能同一个ID进入同步块的情况,所以要放在整个方法的外面
         *    保证事务提交后,才能再次进入此方法
         */
//        synchronized (userId.toString().intern()){
        //查询数据库order中userId是否存在记录,存在则不让购买
        int count = queryOrderCount(userId);
        if (count > 0) {
            //已买过购买失败
            return "fail";
        }

        boolean success = order_count(userId);
        if (!success) {
            return "库存不足";
        }

        saveOrder(userId);
        return "成功";
//        }
    }

    private void saveOrder(Long userId) {
        //模拟提交订单插入数据库
        //insert into order values(userId.....);
    }

    private boolean order_count(Long userId) {
        //修改库存
        //update order_count set count = count -1 where userId =? and count >0;
        return true;
    }

    private int queryOrderCount(Long userId) {
        //模拟查询数据库
        //select count(*) from order where userId = ?
        return 0;
    }
}

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

  • 引入依赖
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>
  • 启动类上启动Aspectj

//exposeProxy默认是false,不暴露代理对象,需要设置为true,code中才能获取代理对象
@EnableAspectJAutoProxy(exposeProxy = true)

 分布式

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

 分布式锁

 

 

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

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

@Component
public class SimpleRedisLock implements ILock {
    private static final String KEY_PREFIX = "lock:";
    //不带横线的uuid
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    @Override
    public boolean tryLock(String key, long timeoutSec) {
        //获取当前线程的ID作为和 UUID 拼接作为value
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(
                KEY_PREFIX + key, threadId, timeoutSec, TimeUnit.SECONDS);
        //可能存在拆箱问题
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock(String key) {
        String threadId = stringRedisTemplate.opsForValue().get(KEY_PREFIX + key);
        if (StrUtil.isEmpty(threadId)){
            return;
        }
        String currentThreadId =  ID_PREFIX + Thread.currentThread().getId();
        if (currentThreadId.equals(threadId)){
            stringRedisTemplate.delete(KEY_PREFIX + key);
        }
    }
}

/**
 * 实现分布式锁
 */
@Service
public class DistributePayImpl implements SinglePay {

    @Autowired
    SimpleRedisLock simpleRedisLock;

    public void buyOrder(Long userId) {
        String lockKey = "order:" + userId;
        boolean isLock = simpleRedisLock.tryLock(lockKey, 10);
        if (!isLock) {
            //获取锁失败,返回错误或重试
            //"一人只能下一单"
            return;
        }
        try {
            SinglePay proxy = (SinglePay) AopContext.currentProxy();
            proxy.createOrder(userId);
        } finally {
            simpleRedisLock.unlock(lockKey);
        }
    }

    @Transactional
    public String createOrder(Long userId) {
        //查询数据库order中userId是否存在记录,存在则不让购买
        int count = queryOrderCount(userId);
        if (count > 0) {
            //已买过购买失败
            return "fail";
        }

        boolean success = order_count(userId);
        if (!success) {
            return "库存不足";
        }

        saveOrder(userId);
        return "成功";
//        }
    }

    private void saveOrder(Long userId) {
        //模拟提交订单插入数据库
        //insert into order values(userId.....);
    }

    private boolean order_count(Long userId) {
        //修改库存
        //update order_count set count = count -1 where userId =? and count >0;
        return true;
    }

    private int queryOrderCount(Long userId) {
        //模拟查询数据库
        //select count(*) from order where userId = ?
        return 0;
    }
}

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

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

 

 

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

 代码实现:

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

    //接受lua脚本
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;

    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    /**
     * 基于Lua脚本解锁
     */
    @Override
    public void unlock(String key) {
        //使用execute执行脚本指令
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + key),
                ID_PREFIX + Thread.currentThread().getId());
    }

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值