1. 什么是Redis分布式锁?
分布式锁,顾名思义,就是分布式系统中使用的锁,在单体应用中我们使用synchronized、ReentrantLock来解决线程时间的共享资源的访问问题,而在分布式系统中,资源贡献问题已经由线程之间的竞争演变到了进程之间的竞争,分布式锁就是接近分布式系统中多进程之间的共享资源的访问问题。
2. Redis分布式锁的作用
- 互斥性:在同一时间只允许一个进程或线程获取锁。
- 防死锁:锁具有超时机制,避免因为进程或线程意外中断导致锁一直被占用而无法释放。
- 高可用性:Redis 分布式锁可以使用 Redis 集群或者 Redis Sentinel 进行部署,保证了高可用性和可扩展性。
- 高性能:使用 Redis 自带的原子操作实现锁的获取和释放,具有高性能和高并发性。
3. 场景案例
秒杀场景是一个非常经典且需要使用分布式锁的应用场景。在秒杀过程中,多个用户会在同一时间尝试购买有限数量的商品,确保商品库存的正确更新和避免超卖至关重要。通过 Redis 的分布式锁,可以实现对商品库存的安全访问。
秒杀场景的实现
假设有一个商品限时秒杀活动,商品数量有限,多个用户同时尝试购买该商品。我们需要确保在同一时刻只有一个用户能够访问和修改商品的库存信息,以防止超卖。
4. 代码实现
4.1 未加任何锁
4.1.1 数据准备
这里使用的MySql数据库模拟我们要秒杀的商品数量为200。
4.1.2 未加锁的业务逻辑代码
@Service
public class StockService {@Autowiredprivate StockDao stockDao;public String decrement(Integer productid) {// 根据id查询商品的库存int num = stockDao.findById(productid);if (num > 0) {// 修改库存stockDao.update(productid);System.out.println("商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个");return "商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个";} else {System.out.println("商品编号为:" + productid + "的商品库存不足。");return "商品编号为:" + productid + "的商品库存不足。";}}
}
上述是根据我们的业务进行的一个简单的实现,在这个实现里,未对代码进行加锁。如果在并发请求的时候,这段代码将会出现很经典的超卖问题。
4.1.3 接口压测,模拟并发情况
让我们来压测一下接口,看一下对应的效果
我们发送了100个请求在1s的时间里,让我们来一起看看结果
4.1.4 压测结果
4.1.5 压测问题
- 我们发现,100个请求进来之后,如果是正常的情况下,是应该减少20个库存,剩下的应该是没有抢到
- 可以根据结果看,我们的100个请求获得了43个商品。同一个商品卖给了多个用户。列如 195号商品同时卖给了多个人。
- 那么我们该如何去解决这个问题呢?
5. 单机情况下JVM级别加锁
首先我们来看一下,如果是单机(项目只部署在一台机器上),使用 synchronized 进行jvm级别加锁,解决上述问题。
5.1 代码加锁
@Service
public class StockService {@Autowiredprivate StockDao stockDao;private final Object lock = new Object();public String decrement(Integer productid) {synchronized (lock) {// 根据id查询商品的库存int num = stockDao.findById(productid);if (num > 0) {// 修改库存stockDao.update(productid);System.out.println("商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个");return "商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个";} else {System.out.println("商品编号为:" + productid + "的商品库存不足。");return "商品编号为:" + productid + "的商品库存不足。";}}}
}
使用 synchronized 关键字,将我们的业务逻辑进行包裹即可。
5.2 查看结果
从结果上来看,通过synchronized 可以在jvm级别上进行上锁。但是我们实际的生产环境中,很少有部署单机服务的。如果我们部署了多个服务,那么通过synchronized 是肯定无法影响另一条机器上的请求的。
6. 集群模式
上面使用syn和lock虽然解决了并发问题,但是我们未来项目部署时可能要部署集群模式。
开启多服务
nginx代理集群
通过压测发现本地锁 无效了。使用redis解决分布式锁文件
代码
@Service
public class StockService {@Autowiredprivate StockDao stockDao;@Autowiredprivate StringRedisTemplate redisTemplate;//public String decrement(Integer productid) {ValueOperations<String, String> opsForValue = redisTemplate.opsForValue();//1.获取共享锁资源Boolean flag = opsForValue.setIfAbsent("product::" + productid, "1111", 30, TimeUnit.SECONDS);//表示获取锁成功if(flag) {try {//根据id查询商品的库存int num = stockDao.findById(productid);if (num > 0) {//修改库存stockDao.update(productid);System.out.println("商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个");return "商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个";} else {System.out.println("商品编号为:" + productid + "的商品库存不足。");return "商品编号为:" + productid + "的商品库存不足。";}}finally {//释放锁资源redisTemplate.delete("product::"+productid);}}else{//休眠100毫秒 在继续抢锁try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}return decrement(productid);}}
}
redis超时问题[业务代码执行时间超过了上锁时间]. 第三方redisson
引入redisson依赖<!--①依赖--><dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.24.3</version></dependency>
@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redisson(){Config config = new Config();
// //连接的为redis集群
// config.useClusterServers()
// // use "rediss://" for SSL connection
// .addNodeAddress("redis://127.0.0.1:7181","","","")
// ;//连接单机config.useSingleServer().setAddress("redis://192.168.111.188:6379");RedissonClient redisson = Redisson.create(config);return redisson;}
}
@Service
public class StockService {@Autowiredprivate StockDao stockDao;@Autowiredprivate RedissonClient redisson;//public String decrement(Integer productid) {RLock lock = redisson.getLock("product::" + productid);lock.lock();try {//根据id查询商品的库存: 提前预热到redis缓存中int num = stockDao.findById(productid);if (num > 0) {//修改库存---incr---定时器[redis 数据库同步]stockDao.update(productid);System.out.println("商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个");return "商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个";} else {System.out.println("商品编号为:" + productid + "的商品库存不足。");return "商品编号为:" + productid + "的商品库存不足。";}}finally {lock.unlock();}}
}