redisTemplate从单机存储到分布式锁的使用

谷粒商城项目中Redis部分

谷粒商城项目中Redis部分2

使用

引坐标

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

配置端口
在这里插入图片描述

缓存中存什么

放json字符串,拿出json字符串,还可以逆转为能用的对象类型【序列化与反序列化】

数据库中存什么

当然也存字符串 但是确是可以转换具体类型的字符串 思考一下都什么数据存在Redis里

  • 客户端经常访问的热点数据(可能是简单数据)
  • 访问一次需要数据库大量操作 比如关联查 或者 查好多个表 返回一个巨大的数据(复杂数据)
举例

现在假设我们从数据库中查询并且组装一个很重的数据结构返回 比如Map<String,List< User>> 这就是getfromDB()的值吧

如此涉及到数据库和Redis中值的转换 每次从DB函数获取的值要JSON.toJSONString(Map<String,List< User>>)转换为json字符串放到redis里

每次客户端从redis里取值使用 再通过 JSONObject.parseObject(“redisString”,new TypeReference<Map<String,List< User>>>(){})将其强转成客户端操作的数据结构

单个服务器使用
  • 缓存的作用就是不走数据库 加快响应速度 所以缓存雪崩+缓存穿透是为防止多走数据库
  • 服务在面对多线程请求过来时 要做到资源互斥 这就是缓存雪崩在讨论的
缓存穿透 (空穿)
  • 请求访问大量数据库中不存在的数据 当然缓存中也没有 导致每次请求来都去查询数据库 数据库里没值
    解决方案:不判断数据库是否有值 只要请求了即使是null 也存入数据库 代码不做任何逻辑判断 直接全部装入redis
缓存击穿 (有击打说明 有热度 )
  • 请求访问热点数据 然而热点数据的缓存即将过期 请求还是大量过来 在这一时刻失效了 导致瞬间大量对一个热点数据的请求进入数据库
  • 进行资源互斥 保证每次只有一个请求进入数据库
缓存雪崩
  • redis的k-v对设置的缓存清空时间都差不多 大片K没有对应的value一起失效 全部去请求数据库 导致数据库压力增大 所以在设置缓存清空时间时候设置随机时间

查询数据库并将值放入缓存当中一定是原子性的操作 否则这边查完数据库 刚要放入缓存 其他线程就抢再次去查数据库了

 @Test
    public Map<String,List<User>> getData(String key) throws JSONException {
    String redisJson= (String) redisTemplate.opsForValue().get("key");
        if(StringUtils.isBlank(redisJson)){
        原子操作 查数据库+放入缓存
           gotoDB();
        }
    return JSON.parseObject(redisJson,new TypeReference<Map<String,List<User>>>(){});
    }
private Map<String, List<User>> gotoDB() {
        Map<String, List<User>> res=null;
          锁住防止缓存击穿 每次只有一个线程进数据库
        synchronized (this){
            防止第二个获取锁的线程再次查询数据库
            if(null!=redisTemplate.opsForValue().get("key")){
                return JSONObject.parseObject((String)redisTemplate.opsForValue().get("key"),new TypeReference<Map<String, List<User>>>(){});
            }
            //模拟...去数据库查询
            res=new HashMap();
             在原子性里立刻以json形式放到缓存 设置随机失效时间 防止缓存雪崩
            redisTemplate.opsForValue().set("key",JSON.toJSONString(res),1,TimeUnit.DAYS);
        }
        return res;
    }

在这里插入图片描述

  • 从数据库中取值 要加锁防止缓存击穿 同时有大量请求访问数据库 我们加锁保证只有一个请求可以进来访问数据库 其余的在等待队列等待 同时把将数据库值放入缓存也放到锁当中 保证原子性 也保证后续直接从缓存中读 不走数据库了

上面程序为本地加锁 只在当前服务器环境下的访问达到互斥 但是多个不同的服务器即使运行相同的代码因为环境不同 都会访问一次数据库

多台服务器 分布式下使用
  • 多台服务器使用就不能用synchronized这种本地锁(只能锁住当前服务器 只能保证当前服务器下的请求只有一个获取锁查询数据库 无法管理其他服务器)

  • 我们要自己写一套 和本地锁功能一致的适合 分布式的代码逻辑
    核心就是大家共同抢占一个对象 操作一条共享数据 达到互斥
    分布式大家都共享同一数据库 数据库的记录可以作为锁
    同时redis也可以

set();
get();
setIfAbsent();
是不同的方法 前两个是缓存的功能存储拿出
 后面是抢占锁相关的 

在这里插入图片描述
在这里插入图片描述

分布式下只保证一个请求进入数据库 锁就变成根据redis来获取锁了
简单来说就是我们自己使用redistemplate来编写一个类似synchronized功能的分布式锁
redistemplate.setIfAbsent()返回一个布尔值 说明当前线程是否抢到了锁
如果抢到锁了 就可以进行

抢锁
  1. synchronized是自己内部维护 自己到时间销毁锁
    我们使用redistemplate抢占锁也要记得写上过期时间 防止一直占锁
  2. 本地锁多线程抢锁维护一个对象 达到互斥
    分布式抢锁我们自己写uuid作为当前占据锁(key)的 钥匙值(value)保证了互斥 独占 原子性
抢锁失败等待
  • 本地锁对象有一个等待队列 线程在自旋锁等待抢占锁
    我们自己实现也要达到这个效果,如果布尔值没抢到锁 就休眠一会然后再次调用访问当前函数 去抢占
释放锁
  • 在抢锁时为了保证独占性 互斥性 我们在抢到的锁上加了自己的钥匙值 现在要将这个锁释放 要删除我们独占的钥匙
  • 删除独占钥匙的第二个情况 k-uuid1设置的清空缓存时间是10s 业务太长走了20s
    此时锁早已经被别人抢走了 k-uuid2了 所以删除锁要对应自己的名字 否则把别人正在走业务的线程释放锁 就报错了
  • uuid的值是赋值操作 在多线程里赋值操作是最无法保证原子性的 当uuid1走到了删除锁 想通过uuid1.equals()来释放锁的时候发现uuid1已经被赋值改为uuid2了 我们只能用lua脚本来释放锁
    原子性+过期解锁+自己的锁名字(防止将别人的锁释放)
    在这里插入图片描述
  public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {

        1、占分布式锁。去redis占坑    
          设置过期时间必须和加锁是同步的,保证原子性(避免死锁)
        String uuid = UUID.randomUUID().toString();  无法保证原子性 会被篡改
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
        if (lock) {
            System.out.println("获取分布式锁成功...");
            Map<String, List<Catelog2Vo>> dataFromDb = null;
            try {
                加锁成功...执行业务 这里去访问数据库就不用sychronized本地锁了
                dataFromDb = getDataFromDb();
            } finally {
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

                删除锁
                stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);

            }
            //先去redis查询下保证当前的锁是自己的
            //获取值对比,对比成功删除无法保证uuid没被修改值原子性 lua脚本解锁
            // String lockValue = stringRedisTemplate.opsForValue().get("lock");
            // if (uuid.equals(lockValue)) {
            //     //删除我自己的锁
            //     stringRedisTemplate.delete("lock");
            // }

            return dataFromDb;
        } else {
            System.out.println("获取分布式锁失败...等待重试...");
            //加锁失败...重试机制
            //休眠一百毫秒
            try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
            return getCatalogJsonFromDbWithRedisLock();     //自旋的方式
        }
    }
  private Map<String, List<Catelog2Vo>> getDataFromDb() {
        //得到锁以后,我们应该再去缓存中确定一次,如果没有才需要继续查询
        String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson");
        if (!StringUtils.isEmpty(catalogJson)) {
            //缓存不为空直接返回
            Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });

            return result;
        }

        System.out.println("查询了数据库");
         //3、将查到的数据放入缓存,将对象转为json
        String valueJson = JSON.toJSONString(parentCid);
        stringRedisTemplate.opsForValue().set("catalogJson", valueJson, 1, TimeUnit.DAYS);

        return parentCid;

public Object getData(){
    抢锁
    if(block){
    try{
    查缓存
    查数据库+存如缓存
    }finally{
    释放锁
    }
  }else{
自旋锁
 }

}

Redisson

使用分布式锁刚才是我们自己用redisTemplate代码实现的
不仅承担了存储 还承担了分布式锁的功能

  • 也在上面讲了就是在模拟单机的synchronized的功能
    比如过期自动释放锁 抢锁失败自旋锁等待 删除锁等
    现在我们使用redission框架成熟完成以上功能 而不是自己实现
引入坐标

谷粒商城Redisson部分直接阅读搜寻

配置redis地址及端口
  • 文件
  • 配置类
    使用我们自己创建@Bean Redissionclient
抢锁 释放锁
try{
  redissionCLient.lock();
}catch{
   
}finally{
 redissionClinet.unlock();
}

看门狗原理

redistemplate 里面有原子的 抢锁+过期时间 确保不会因为 服务器突然宕机 等突发情况 没有为锁设置过期时间而导致一直占着锁 成为活的死锁

但redission的lock没有显式的让我们看到他有设置原子性的过期时间

实现 Redis 分布式锁可以使用 Redis 的 SETNX 命令,在 Redis 中设置一个 key,并且设置过期时间,当 SETNX 返回成功时说明获取锁成功,否则获取锁失败。为了保证锁的安全性,可以将 key 的值设置为一个随机字符串。 下面是使用 redisTemplate 实现分布式锁的示例代码: ```java public class RedisLock { private static final Logger LOGGER = LoggerFactory.getLogger(RedisLock.class); private RedisTemplate<String, Object> redisTemplate; private String lockKey; private String lockValue; private long expireTime; public RedisLock(RedisTemplate<String, Object> redisTemplate, String lockKey, long expireTime) { this.redisTemplate = redisTemplate; this.lockKey = lockKey; this.expireTime = expireTime; this.lockValue = UUID.randomUUID().toString(); } public boolean tryLock() { Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.MILLISECONDS); if (result == null) { return false; } return result; } public void unlock() { try { Object value = redisTemplate.opsForValue().get(lockKey); if (value != null && lockValue.equals(value.toString())) { redisTemplate.delete(lockKey); } } catch (Exception e) { LOGGER.error("unlock error", e); } } } ``` 使用示例: ```java public class RedisLockTest { @Autowired private RedisTemplate<String, Object> redisTemplate; @Test public void testRedisLock() { RedisLock lock = new RedisLock(redisTemplate, "lock_key", 5000); try { if (lock.tryLock()) { // 获取锁成功,执行业务逻辑 // ... } else { // 获取锁失败,执行其他逻辑 // ... } } finally { lock.unlock(); } } } ``` 在以上代码中,tryLock 方法尝试获取锁,如果获取成功返回 true,否则返回 false。unlock 方法用于释放锁。在获取锁时使用Redis 的 SETNX 命令,设置 key 的值为随机字符串,并设置过期时间。在释放锁时,先获取 key 的值,如果值与之前设置的值相同,则删除 key。这里需要注意的是,在高并发场景下,释放锁时可能会出现竞态条件,因此可以使用 Lua 脚本来保证原子性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值