目录
分布式锁介绍
模拟一个并发场景
基于Redis实现分布式锁
原理
案例优化(加入分布式锁)
Redisson
Redisson简介
Redisson功能特性
基于Redisson实现分布式锁
在计算机系统中,锁作为一种控制并发的机制无处不在,单机环境下,操作系统能够在进程或线程之间通过本地的锁来控制并发程序的行为。而在如今的大型复杂系统中,通常采用的是分布式架构提供服务。分布式环境下,基于本地单机的锁无法控制分布式系统中分开部署客户端的并发行为,此时分布式锁就应运而生了。
一个可靠的分布式锁应该具有以下特征:
①互斥锁:作为锁,需要保证任何时候只能有一个客户端持有锁。
②可重入:同一个客户端在获得锁后,可以再次进行加锁。
③高可用:获取锁和释放锁的效率较高,不会出现单点故障。
④自动重试机制:当客户加锁失败时,能够提供一种机制让客户端自动重试。
众所周知,电商系统秒杀活动优惠非常之大,同时也给系统带来了不可想象的并发压力,如果不做并发处理,会出现什么情况呢?没错,就是会出现超卖的情况。
那么,什么是超卖呢?
比如,某米10纪念版要参与秒杀活动,2000元/台,相比正常价格下优惠力度非常之大,所以参加活动当然也需要确定秒杀的库存数。这边我们假设待秒杀的库存数为100台,抢完100台则活动结束。
案例:当我们系统中如果没有加入分布式锁,会出现如下问题:
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("deductStock")
public String deductStock(){
Integer stock = Integer.parseInt(this.stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0){
Integer realStock = stock - 1;
this.stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
}else {
System.out.println("扣减失败,库存不足!");
}
return "执行完毕";
}
}
代码在一般情况下感觉没有任何问题,但是当同一时间段进行秒杀操作,是有可能存在并发的,我们假设并发数为10,那么这10个并发会同一时间段去get("stock"),此时获取的stock都是100,再同一时间进行减库存设置库存的操作,那么此时Redis中的stock值为99。正确应该为90。
利用Redis的setnx命令。因为此命令是原子性操作,只有在key不存在的情况下,才能set成功。
看看加了分布式锁后的代码
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("deductStock")
public String deductStock(){
// 锁的key,实际业务需要拼接
String lockKey = "product_mi10";
// 随机字符,保证释放锁时释放的是自己的锁
String clientId = UUID.randomUUID().toString();
try {
// Redis的setnx,加上过期时间,程序还没到释放锁的步骤已经宕机
Boolean result = this.stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);
if (!result){// 保证只有获得锁的线程在执行后续操作
return "未获取到锁!";
}
// 查询商品库存数
Integer stock = Integer.parseInt(this.stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0){
// 减库存操作
Integer realStock = stock - 1;
this.stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
}else {
System.out.println("扣减失败,库存不足!");
}
}finally {
if (clientId.equals(this.stringRedisTemplate.opsForValue().get(lockKey))){
// 释放锁
this.stringRedisTemplate.delete(lockKey);
}
}
return "执行完毕";
}
}
此时你们觉得程序没有问题了吗?
不一定,我们想象一个场景,如果我们程序执行到释放锁操作需要15秒,那么会发生什么?
那么程序还没执行完,锁就过期了,外部还有很多线程在等待着去抢锁。
这问题其实很好解决,比如我们内部可以再启一个子线程,定时器每隔一段时间(建议是过期时间的1/3)去重置当前key的失效时间,强行续命。也不用担心消耗性等问题,因为只有一个线程在做检查重置操作。
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
①多种连接方式:同步、异步、异步流、管道流。
②数据序列化:多种数据序列化方式。
③集合数据分片:在集群模式下,Redisson为单个Redis集合类型提供了自动分片功能。单个集合被拆分后,均匀地分布在整个集群里,而不是挤在单一的一个节点里。
④分布式对象:Object Bucket(通用对象桶), Binary Stream(二进制流), Geospatial Bucket(地理空间对象), AtomicLong,AtomicDouble,Bloom Filter(布隆过滤器)...
⑤分布式集合:Map,Multimap,Set,SortedSet,List,Queue,双端,堵塞,延迟队列...
⑥分布式锁:Reentrant Lock(可重入锁),Fair Lock(公平锁),ReadWrite Lock(读写锁),CountDownLatch(闭锁)),Semaphore(信号量)...
⑦分布式服务:Remote Service(分布式远程服务),Scheduler Service(分布式调度任务服务),MapReduce,Executor Service...
添加Redisson初始化配置类
@Configuration
public class RedissonConfig {
@Bean
public Redisson redisson(){
Config config = new Config();
// 单机模式
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
实现分布式锁
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private Redisson redisson;
@GetMapping("deductStock")
public String deductStock(){
// 锁的key,实际业务需要拼接
String lockKey = "product_mi10";
// 获取锁对象
RLock redissonLock = this.redisson.getLock(lockKey);
try {
// 加锁
redissonLock.lock(10, TimeUnit.SECONDS);
// 查询商品库存数
Integer stock = Integer.parseInt(this.stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0){
// 减库存操作
Integer realStock = stock - 1;
this.stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
}else {
System.out.println("扣减失败,库存不足!");
}
}finally {
// 释放锁
redissonLock.unlock();
}
return "执行完毕";
}
}
是不是感觉使用Redisson实现分布式锁更加简单。