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();
}
}
1402

被折叠的 条评论
为什么被折叠?



