Redis
实现分布式锁为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java
并发处理相关的API
(如ReentrantLock
或Synchronized
)进行互斥控制。
在单机环境中,Java中提供了很多并发处理相关的API。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
上图可以看到,变量A
存在JVM1、JVM2、JVM3
三个JVM
内存中(这个变量A
主要体现是在一个类中的一个成员变量,是一个有状态的对象,例如:UserController
控制器中的一个整形类型的成员变量),如果不加任何控制的话,变量A
同时都会在JVM
分配一块内存,三个请求发过来同时对这个变量操作,显然结果是不对的!即使不是同时发过来,三个请求分别操作三个不同JVM内存区域的数据,变量A之间不存在共享,也不具有可见性,处理的结果也是不对的!
如果我们业务中确实存在这个场景的话,我们就需要一种方法解决这个问题!
1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2、高可用的获取锁与释放锁;
3、高性能的获取锁与释放锁;
4、具备可重入特性;
5、具备锁失效机制,防止死锁;
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
我们可以同时去一个地方“占坑”,如果占到,就执行逻辑。否则就必须等待,直到释放锁。 “占坑”可以去redis
,可以去数据库,可以去任何大家都能访问的地方。 等待可以自旋的方式。
目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP
理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。
在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如:分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。
Redis
等)实现分布式锁;Zookeeper
实现分布式锁;加锁实际上就是在redis
中,给Key
键设置一个值,为避免死锁,并给定一个过期时间。
SET lock_key random_value NX PX 5000
random_value
是客户端生成的唯一的字符串。解锁的过程就是将Key
键删除。但也不能乱删,不能说客户端1的请求将客户端2的锁给删除掉。这时候random_value
的作用就体现出来。
为了保证解锁操作的原子性,我们用LUA脚本完成这一操作。先判断当前锁的字符串是否与传入的值相等,是的话就删除Key,解锁成功。
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end
注意
redis+Lua
脚本完成
redis
分布式锁,核心两处,加锁保证原子性,解锁保证原子性
依赖:
<!--jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.0.0</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
</dependency>
配置类以及IDUtil
工具类:
@Configuration
public class RedisConfig {
@Bean
public JedisPool jedisPool(){
return new JedisPool(new JedisPoolConfig(),"127.0.0.1",6379);
}
}
public class IDUtil {
//唯一id生成
public static String getId() {
// 取当前时间的长整形值包含毫秒
long millis = System.currentTimeMillis();
// 加上三位随机数
Random random = new Random();
int end3 = random.nextInt(999);
// 如果不足三位前面补0
String str = millis + String.format("%03d", end3);
return str;
}
public static void main(String[] args) {
System.out.println(IDUtil.getId());
}
}
上锁以及解锁的类:
@Slf4j
@Component
public class RedisLock {
//锁键
private String lock_key = "redis_lock";
//锁过期时间
protected long internalLockLeaseTime = 30000;
//获取锁的超时时间
private long timeout = 10000;
//SET命令的参数
SetParams params = SetParams.setParams().nx().px(internalLockLeaseTime);
@Autowired
JedisPool jedisPool;
//redis密码
String password="zhongguo";
//加锁
public boolean lock(String id){
Jedis jedis = jedisPool.getResource();
jedis.auth(password);
Long start = System.currentTimeMillis();
try{
for(;;){
//第一条线程进来设值,第二条线程进来
// key="redis_lock"已经存在了,则不进行设值操作,
// 往下执行,等待锁过期,如果一直都等不到锁,
// 循环等待的时间大于了timeout,则直接返回false
//SET命令返回OK ,则证明获取锁成功
//设置锁和超时时间同一原子操作
String lock = jedis.set(lock_key, id, params);
if("OK".equals(lock)){
log.info("线程{}获取锁成功",Thread.currentThread().getName() );
return true;
}
//否则循环等待,在timeout时间内仍未获取到锁,则获取失败
long l = System.currentTimeMillis() - start;
if (l>=timeout) {
log.info("线程{}获取锁超时",Thread.currentThread().getName() );
return false;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}catch (Exception e){
log.error("获取锁失败:",e);
new RuntimeException(String.format("当前线程{}获取锁失败",Thread.currentThread().getName()));
}
finally {
jedis.close();
}
return false;
}
//解锁
public boolean unlock(String id){
Jedis jedis = jedisPool.getResource();
jedis.auth(password);
String script =
"if redis.call('get',KEYS[1]) == ARGV[1] then" +
" return redis.call('del',KEYS[1]) " +
"else" +
" return 0 " +
"end";
try {
//Collections.singletonList
//返回一个只包含指定对象的不可变集合,但是这个长度的集合只有1,可以减少内存空间。
//删除锁必须保证原子性。使用redis+Lua脚本完成
Object result = jedis.eval(script, Collections.singletonList(lock_key),
Collections.singletonList(id));
if("1".equals(result.toString())){
log.info("线程{}释放锁成功",Thread.currentThread().getName() );
return true;
}
log.info("线程{}释放锁失败",Thread.currentThread().getName() );
return false;
}catch (Exception e){
log.error("释放锁失败:",e);
new RuntimeException(String.format("当前线程{}释放锁失败",Thread.currentThread().getName()));
}
finally {
jedis.close();
}
return false;
}
}
测试:
@Slf4j
@Controller
public class IndexController {
@Autowired
RedisLock redisLock;
int count = 0;
@RequestMapping("/index")
@ResponseBody
public String index() throws InterruptedException {
int clientcount =10;
//减法计数器,总数为clientcount
CountDownLatch countDownLatch = new CountDownLatch(clientcount);
//线程池
ExecutorService executorService = Executors.newFixedThreadPool(clientcount);
long start = System.currentTimeMillis();
for (int i = 0;i<clientcount;i++){
executorService.execute(() -> {
//通过Snowflake算法获取唯一的ID字符串
String id = IDUtil.getId();
try {
redisLock.lock(id);
//执行业务逻辑
count++;
}catch (Exception e){
log.error("获取锁失败:",e);
new RuntimeException(String.format("当前线程{}获取锁失败",Thread.currentThread().getName()));
}
finally {
//解锁就是删除key,id是唯一的作用就体现出来了
//客户端A的请求删不掉客户端B的锁,因为id唯一
redisLock.unlock(id);
}
//数量减一
countDownLatch.countDown();
});
}
//所有线程阻塞在此
//直到所有线程的任务执行完,计数器归0,才向下执行
countDownLatch.await();
long end = System.currentTimeMillis();
log.info("执行线程数:{},总耗时:{},count数为:{}",clientcount,end-start,count);
return "Hello";
}
}
问题:
1.不具备锁不具有可重入特性。
2.如果业务逻辑的执行时间比锁的过期时间长,怎么办?
可参考redisson可重入分布式锁的原理:
https://www.cnblogs.com/cjsblog/p/9831423.html
https://www.bilibili.com/video/BV1Xa4y1s79P?from=search&seid=4569538984682713544
需求:查询三级菜单分类,先查询缓存,缓存有,则直接返回,缓存没有,先查数据数据库,在把数据放进缓存,在返回数据,需解决各种分布式环境下的问题,包括缓存问题。
不使用jedis
,这里是springboot
整合的redis
,使用的是spring-boot-starter-data-redis
使用本地锁实现需求
@Override
public Map> getCatalogJson() {
/**
* 1.空结果缓存:解决缓存穿透。
* 2.设置过期时间(加随机值): 解决缓存雪崩的问题
* 3.加锁:解决缓存击穿问题
* 本地锁只能锁住当前进程,我们需要分布式锁
*/
//加入缓存逻辑,缓存中存的数据是json字符串
//json跨语言,跨平台兼容
ValueOperations stringStringValueOperations = stringRedisTemplate.opsForValue();
String catalogJson = stringStringValueOperations.get("catalogJson");
//缓存为空
if (StringUtils.isEmpty(catalogJson)) {
System.out.println("缓存没命中,查询数据库");
//==========================================================================
//缓存中没有,就查询数据库
//加锁:解决缓存击穿问题
Map> catalogJsonFromDb= getCatalogJsonFromDb();
//==========================================================================
//查询菜单这个业务,不存在缓存穿透的问题,因为查询菜单不需要入参,所以不需要将空结果缓存
return catalogJsonFromDb;
}
System.out.println("缓存命中,直接返回");
//缓存不为空,把json数据转为对象
Map> stringListMap = JSON.parseObject(catalogJson, new TypeReference
锁时序问题:本地锁需要将确认缓存是否存在,查询数据库,结果放入缓存
,这三步锁起来。如果结果放入缓存
这一步没有锁起来,就会出现锁时序问题,比如,并发访问时,一条线程进来获取到锁,并确认没缓存,查询了数据库,然后把锁释放了,但是在它,将结果放入缓存之前,另一个并发进来的线程抢到锁,此时,它发现缓存也是没有的,又查询了一次数据库,这就不符合我们的预期了,我们预期是并发访问,只有一条线程能查询数据库,其他全走缓存。因此,也要把结果放入缓存这一步,也要锁起来
使用Jmeter
,压力测试该服务:只查询了一次数据库,其他全走,缓存,符合预期。
复制三个当前服务
启动参数,把端口改一改
启动,就相当于,当前微服务启动了4个,由nginx
将请求转发到网关,在由网关负载均衡到这四个服务。
Jmeter
压测:
设置线程组
HTTP
请求设置
发现同一个微服务的不同节点,每一个都去查了一次数据库:
这就说明了本地锁,只能锁住当前进程
使用分布式锁实现上面的需求。
@Override
public Map> getCatalogJson() {
/**
* 1.空结果缓存:解决缓存穿透。
* 2.设置过期时间(加随机值): 解决缓存雪崩的问题
* 3.加锁:解决缓存击穿问题
* 本地锁只能锁住当前进程,我们需要分布式锁
*/
//加入缓存逻辑,缓存中存的数据是json字符串
//json跨语言,跨平台兼容
ValueOperations stringStringValueOperations = stringRedisTemplate.opsForValue();
String catalogJson = stringStringValueOperations.get("catalogJson");
//缓存为空
if (StringUtils.isEmpty(catalogJson)) {
//缓存中没有,就查询数据库
/加锁:解决缓存击穿问题
//本地锁: synchronized、JUC(Lock),分布式情况下,想要锁住所有,必须使用分布式锁
System.out.println("缓存没命中,查询数据库");
//使用本地锁
//Map> catalogJsonFromDb= getCatalogJsonFromDb();
//使用分布式锁
Map> catalogJsonFromDb= getCatalogJsonFromDbWithRedisLock();
//***查询菜单这个业务,不存在缓存穿透的问题,因为查询菜单不需要入参,所以不需要将空结果缓存
return catalogJsonFromDb;
}
System.out.println("缓存命中,直接返回");
//缓存不为空,把json数据转为对象
Map> stringListMap = JSON.parseObject(catalogJson, new TypeReference>>() {
});
return stringListMap;
}
//分布式锁查询数据库
public Map> getCatalogJsonFromDbWithRedisLock() {
//占分布式锁。去redis占锁
String uuid = UUID.randomUUID().toString();
//setIfAbsent如果键不存在则新增,存在则不改变已经有的值。
//设置值和超时时间,保证加锁是原子性的
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
//上锁成功
if (lock){
System.out.println("获取分布式锁成功...");
Map> dataFromDb;
try {
//执行业务逻辑(查数据库的方法,不码出来了)
dataFromDb= getDataFromDb();
}finally {
//lua脚本解锁
String script="if redis.call('get',KEYS[1]) == ARGV[1] then \n" +
" return redis.call('del',KEYS[1]) \n" +
"else\n" +
" return 0 \n" +
"end";
//删除锁,保证原子性
Long lock1 = stringRedisTemplate.execute(new DefaultRedisScript(script, Long.class),
Arrays.asList("lock"), uuid);
}
return dataFromDb;
}else{
System.out.println("获取分布式锁失败...等待重试");
//加锁失败...重试
//休眠
try {
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
//自旋锁
return getCatalogJsonFromDbWithRedisLock();
}
}
分布式锁可以封装工具类,但是分布式锁有更专业的框架!
附录:
https://blog.csdn.net/wuzhiwei549/article/details/80692278
https://www.jianshu.com/p/47fd7f86c848