02Gulimall分布式锁原理与使用

02Gulimall分布式锁原理与使用,第1张

02Gulimall分布式锁原理与使用 分布式锁原理与使用

所有的商品服务都去占同一把锁。

利用redis,的set nx , 当redis中不存在时才放入redis

set nx 是一个原子性的 *** 作,只要set失败的线程就认为占锁失败。

进入虚拟机

输入

docker exec -it redis redis-cli

进入redis,模拟多个服务set nx

set lock haha NX

只有一个人会返回OK,其他都是nil

这样是否存在问题?

public Map> getCatalogJsonFromDbWithRedisLock() {
        //占分布式锁
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
    if (lock) {
        //加锁成功,执行业务
        return getStringListMap();

    }else {
        //加锁失败
        
    }
}
synchronized
`自旋`:同步锁会在这里一直监听,别人一释放他就会拿到。一直重试
public Map> getCatalogJsonFromDbWithRedisLock() {
        //占分布式锁
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
    if (lock) {
        //加锁成功,执行业务
        Map> listMap = getStringListMap();
        redisTemplate.delete("lock");//删除锁
        return listMap;

    }else {
        //加锁失败
        //可以休眠100ms重试,调用自己
        return getCatalogJsonFromDbWithRedisLock(); //自旋
    }
}

如果出现异常,没有删锁,就会导致死锁。

我们可以给锁设置过期时间

 if (lock) {
            //设置过期时间
            redisTemplate.expire("lock",30,TimeUnit.SECONDS);
            //加锁成功,执行业务
            Map> listMap = getStringListMap();
            redisTemplate.delete("lock");//删除锁
            return listMap;

        }else {

那如果在if下面,设置过期时间上面断电怎么办?

我们必须保证加锁和删除锁是一个原子 *** 作

所以要写成这样

public Map> getCatalogJsonFromDbWithRedisLock() {
        //占分布式锁
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",300,TimeUnit.SECONDS);
    if (lock) {
        //设置过期时间
        //redisTemplate.expire("lock",30,TimeUnit.SECONDS);
        //加锁成功,执行业务
        Map> listMap = getStringListMap();
        redisTemplate.delete("lock");//删除锁
        return listMap;

    }else {
        //加锁失败
        //可以休眠100ms重试,调用自己
        return getCatalogJsonFromDbWithRedisLock(); //自旋
    }
}

再一次改进

 String uuid = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",300,TimeUnit.SECONDS);
        if (lock) {
            //设置过期时间
            //redisTemplate.expire("lock",30,TimeUnit.SECONDS);
            //加锁成功,执行业务
            Map> listMap = getStringListMap();

            //获取值-对比-对比成功再删除 == 原子 *** 作
            String lock1 = redisTemplate.opsForValue().get("lock");
            if (uuid.equals(lock1)) {
                redisTemplate.delete("lock");//删除锁
            }


            return listMap;

        }else {

但是还是会出现删除别人的锁的情况。

需要用到lua脚本

if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end

1.2 采用Redission框架
@Controller
public class IndexController {

    @Autowired
    RedissonClient redisson;

  

    @GetMapping("/hello")
    @ResponseBody
    public String hello(){
        //获取锁
        RLock lock = redisson.getLock("my-lock");
        //加锁
        lock.lock(); //阻塞式等待,直到拿到锁才运行下面方法
        try {
            System.out.println("加锁成功,执行业务" + Thread.currentThread().getId());
            //模拟业务
            Thread.sleep(30000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //解锁
            System.out.println("释放锁:"+Thread.currentThread().getId());
            lock.unlock();
        }

        return "hello";
    }
}

redission的强大之处:

1.锁的自动续期,如果业务超长,会在运行期间会自动给锁续上新的TTL,不用担心业务时间长,锁自动过期被删掉。

2.加锁的业务只要运行完成,不会给当前锁续期,即使不手动解锁,锁默认也会在30S后自动删除。

读写锁,保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁)。读锁是一个共享锁。

写锁没有释放,读就必须等待。

写+读:等待写锁释放

写+写:阻塞方式

读+写:

@GetMapping("/write")
@ResponseBody
public String writevalue(){

    //读写锁
    RReadWriteLock rwLock = redisson.getReadWriteLock("rw-lock");
    String s = "";

    //写锁
    RLock writeLock = rwLock.writeLock();

    try {
        //改数据加写锁
        writeLock.lock();

        s = UUID.randomUUID().toString();
        Thread.sleep(3000);
        stringRedisTemplate.opsForValue().set("writevalue",s);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        writeLock.unlock();
    }

    return s;
}

@GetMapping("/read")
@ResponseBody
public String readValue(){
    RReadWriteLock rwLock = redisson.getReadWriteLock("rw-lock");
    //读锁
    RLock readLock = rwLock.readLock();

    String s = "";
    readLock.lock();

    try {
        s = stringRedisTemplate.opsForValue().get("writevalue");
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        readLock.unlock();
    }

    return s;
}
1.3 缓存里面的数据如何和数据库保存一致? 1.双写模式

即更新数据库的数据后,随即更新缓存里的数据

存在问题:

2.失效模式

即更新数据库的数据后删除缓存中的数据

存在问题:

3.总结

无论是双写模式还是失效模式,都会有缓存的不一致的问题,即并发情况下同时更新就会有不一致的问题

解决方式:

如果是用户维度数据(比如订单数据,用户数据),这种并发几率小的数据,可以不考虑这方面的问题。可以使用过期时间这一方式,让数据每一段时间触发数据的读的主动更新即可如果是菜单,商品介绍等基础数据,可以使用Canal(阿里开源中间件)订阅binlog的方式通常情况下,使用缓存数据 + 过期时间 就可以解决大部分业务的需求
通过加锁保证并发读写,写写的时候按顺序排队(读写锁)

总而言之:

缓存中的数据不应该是实时性,一致性要求较高的数据大部分业务使用缓存数据 + 过期时间,保证每天能拿到最新数据即可实时性,一致性较高的数据应该存储在数据库

一般缓存数据一致性解决方案:

  • 缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新缓存
  • 读写数据的时候,加上分布式读写锁

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5612048.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-15
下一篇 2022-12-15

发表评论

登录后才能评论

评论列表(0条)

保存