Redis数据结构:String
超卖and一人一单问题
相关前言问题: 超卖解决: 方案(乐观锁):只需要在扣减库存更新数据库时带上条件 库存>0(CAS法) boolean success = iSeckillVoucherService.update() .setSql("stock = stock - 1") .eq("voucher_id", voucherOrder.getVoucherId()) .gt("stock", 0) .update(); 方案(悲观锁):添加同步锁让线程串行执行。(简单粗暴,性能下降) 一人一单: 单体项目: 一:如果是更新数据:则可以在购买更新前先做判断 int count = query().eq("user_id", userId).eq("voucher_id",voucherId).count(); // 5.2.判断是否存在 if (count > 0) { // 用户已经购买过了 return Result.fail("用户已经购买过一次!"); } 二:加锁 public synchronized Result CreateVoucherOrder(Long voucherId) {} 集群模式: 添加锁监视器(Mysql本身互斥锁实现、Redis中setnx的互斥命令实现、Zookeeper利用节点唯一性和有序性实现)
基于setnx实现分布式锁
存在问题:不可重入 (同一个线程在已经持有锁的情况,再次尝试获取同一把锁时会失败; 假如他在一个方法内,调用另一个方法,那么此时如果是不可重入的,就死锁了)
不可重试 (线程尝试获取锁失败后,不会再次尝试获取,而是直接返回失败结果)
超时释放 (通常需要手动设置锁的过期时间,以避免锁被永久占用;二是如果锁的过期时间设置不合理,可能会导致业务逻辑还未执行完,锁就已经过期释放,从而引发并发问题)
主从一致性:(如果Redis提供了主从集群,当我们向集群写数据时,主机需要异步的将数据同步给从机,而万一在同步过去之前,主机宕机了,就会出现死锁问题。)
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-"; @Override public boolean tryLock(long timeoutSec) { // 获取线程标示(直接使用Thread.currentThread() 只在当前线程的JVM上是唯一的,而如果涉及多个JVM则会导致冲突,存在可能误删问题) String threadId = ID_PREFIX + Thread.currentThread().getId(); // 获取锁 Boolean success = stringRedisTemplate.opsForValue() .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS); return Boolean.TRUE.equals(success); } //释放锁版本一: public void unlock() { // 获取线程标示 String threadId = ID_PREFIX + Thread.currentThread().getId(); // 获取锁中的标示 String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name); // 判断标示是否一致 // !!!这里如果线程1在判断完后,锁正好到期了,那么此时线程2进来获取锁,然而线程1他会接着往后执行,当他卡顿结束后,他直接就会执行删除锁那行代码,把线程2的锁给释放了 // 因此要保证分布式锁的原子性->Lua if(threadId.equals(id)) { // 释放锁 stringRedisTemplate.delete(KEY_PREFIX + name); } } //释放锁版本二: private static final DefaultRedisScript<Long> UNLOCK_SCRIPT; static { UNLOCK_SCRIPT = new DefaultRedisScript<>(); UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua")); UNLOCK_SCRIPT.setResultType(Long.class); } public void unlock() { // 调用lua脚本 stringRedisTemplate.execute( //上面定义好的 Lua 脚本对象。 UNLOCK_SCRIPT, // KEYS 参数传入脚本,也就是锁的键 Collections.singletonList(KEY_PREFIX + name), // ARGV 参数传入脚本,即当前线程的标识 ID_PREFIX + Thread.currentThread().getId()); }-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示 -- 获取锁中的标示,判断是否与当前线程标示一致 if (redis.call('GET', KEYS[1]) == ARGV[1]) then -- 一致,则删除锁 return redis.call('DEL', KEYS[1]) end -- 不一致,则直接返回 return 0
Redission实现分布式锁
可重入:用state变量来记录重入的状态的,比如当前没有人持有这把锁,那么state=0,假如来一个人持有这把锁,那么state++,释放一次就-1 ,直到减少成0 时,表示当前这把锁没有被人持有。
分布式锁-redission锁重试和WatchDog机制:先判断当前这把锁是否存在,如果不存在,插入一把锁,返回null;判断当前这把锁是否是属于当前线程,如果是,则返回null;所以如果返回是null,则代表着当前这哥们已经抢锁完毕。如果是没有传入时间,则此时也会进行抢锁, 而且抢锁时间是默认看门狗时间 。
redission锁的MutiLock原理:使用这把锁不使用主从,每个节点的地位都是一样的, 这把锁加锁的逻辑需要写入到每一个主丛节点上,只有所有的服务器都写入成功,此时才是加锁成功,假设现在某个节点挂了,那么他去获得锁的时候,只要有一个节点拿不到,都不能算是加锁成功,保证了加锁的可靠性。
// 引用 <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.13.6</version> </dependency> // 配置类 @Configuration public class RedissonConfig { @Bean public RedissonClient redissonClient(){ Config config = new Config(); config.useSingleServer().setAddress("redis://localhost:6379").setPassword("123456"); return Redisson.create(config); } } @Resource private RedissonClient redissonClient; @Override public Result seckillVoucher(Long voucherId) { // 1.查询优惠券 SeckillVoucher voucher = seckillVoucherService.getById(voucherId); // 2.判断秒杀是否开始 if (voucher.getBeginTime().isAfter(LocalDateTime.now())) { // 尚未开始 return Result.fail("秒杀尚未开始!"); } // 3.判断秒杀是否已经结束 if (voucher.getEndTime().isBefore(LocalDateTime.now())) { // 尚未开始 return Result.fail("秒杀已经结束!"); } // 4.判断库存是否充足 if (voucher.getStock() < 1) { // 库存不足 return Result.fail("库存不足!"); } //以上局部检查 Long userId = UserHolder.getUser().getId(); //创建锁对象 这个代码不用了,现在要使用分布式锁 //SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate); RLock lock = redissonClient.getLock("lock:order:" + userId); //获取锁对象 boolean isLock = lock.tryLock(); //加锁失败 if (!isLock) { return Result.fail("不允许重复下单"); } try { //获取代理对象(事务)来调用方法 // 拿到IVoucherOrderService接口代理对象进行操作, 防止事务失效 // (Transactional事务是用Spring的代理对象来完成的, 而这里直接调用createVoucherOrder则是使用this.的方式调用,会导致事务失效) IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); } finally { //释放锁 lock.unlock(); } }