JAVA学习笔记_Redis进阶

文章目录

初识redis

redis简介

Redis(远程词典服务器),是一个基于内存的键值型NoSQL数据库

Redis是一个基于内存的key-value结构数据库

官网,www.redis.net.cn

  • 基于内存存储,读写性能高
  • 适合存储热点数据(热点商品、资讯、新闻)
  • 企业应用广泛

windows启动redis服务器

复制代码
redis在windows上启动服务端
redis属于绿色软件(文件夹),解压即可使用
启动redis服务
cmd中启动redis-server.exe
客户端(这里的客户指的是开发人员)使用服务
cmd中启动redis-cli.exe
命令示例,redis-cli.exe -h localhost -p 6379 -a 123456
-h 地址 -p 端口号 -a 密码
密码需要在redis.windows-service.conf手动设置
虽然可以在命令行使用redis,但一般还是图形界面的redis更好用更主流(还是要手动启动redis服务的)

linux启动redis服务器

windows版本的redis都是微软自己重写的,redis官方并没有windows版本,只有linux版本

复制代码
linux版本
下载安装略过
三种启动方式
- 默认启动,redis-server
- 指定配置启动,需要修改redis.conf文件中的一些配置,主要是设置哪些ip可访问redis,设置为守护进程(运行状态不显示在前端),用户密码,日志文件等,启动命令,redis-server redis.conf
- 开机自启动,需要配置vi /etc/systemd/system/redis.service
systemctl enable redis//redis开机自启
systemctl daemon-reload//重载系统服务
# 启动
systemctl start redis
# 停止
systemctl stop redis
# 重启
systemctl restart redis
# 查看状态
systemctl status redis

图形用户界面客户端RDM

GitHub上的大神编写了Redis的图形化桌面客户端,地址:https://github.com/uglide/RedisDesktopManager(收费)

在下面这个仓库可以找到安装包:https://github.com/lework/RedisDesktopManager-Windows/releases(免费)

redis命令

常用数据类型

各种命令可通过官网查看'https://www.redis.net.cn/'

key为字符串类型,value有5种常用的基本数据类型

  • 字符串string
  • 哈希hash ,类似HashMap
  • 列表list ,类似LinkedList
  • 集合set ,类似HashSet
  • 有序集合sorted set/zset 集合中每个元素关联一个分数score,根据分数升序

特殊类型

  • GEO
  • BitMap
  • HyperLog

字符串操作命令

  • SET key value ,设置指定key的值
  • GET key ,获取指定key的值
  • MSET key value,批量添加或修改
  • MGET key value,批量获取
  • SETEX key seconds value ,设置指定key值,并将key的过期时间设置为sencond秒
    复合命令,等同于SET key value ex seconds
  • SETNX key value ,只有在key不存在时,设置key的值
    复合命令,等同于SET key value nx
  • INCR key,自增1
  • INCRBY key increment,自增指定步长
  • INCRBYFLOAT key increment,浮点数自增并指定步长
    string类型的三种格式,字符串,int,float,虽然都是string类型但是存储规则不同,都是怎么节省空间怎么存储

Key的层级格式

哈希操作命令

  • HSET key field value,设置值
  • HGET key field ,获取值
  • HMSET key field value field value,批量设置
  • HGET key field1 field2,批量获取
  • HGETALL key,获取全部field字段和value
  • HKEYS key,获取表中所有字段
  • HVALS key,获取表中所有值
  • HINCRBY key field increment,设置一个key中的字段自增
  • HSETNX,添加一个Hash类型的key的field值,前提是这个field不存在,否则不执行
  • HDEL key field,删除值

列表操作命令

  • LPUSH key element,将一个或多个值插入头部(后插入的那端为头部)
  • LPUSH key element,将一个或多个值插入尾部
  • LPOP key,移除并返回列表左侧第一个元素,没有则返回nil
  • RPOP key,移除并获取列表最后一个元素,也就是第一个被插入的
  • LRANGE key start stop,获取列表指定范围的元素
  • BLPOP和BRPOP,与lpop和rpop类似,只不过在没有元素时,等待指定时间,而不是直接返回nil
  • LLEN KEY,获取列表长度

集合操作命令

  • SADD key mamber1 member2,向集合添加一个或多个成员
  • SMEMBERS key,返回集合中的所有成员
  • SISMEMBER s1 a,判断a是否在集合中
  • SCARD key,获取集合的成员数
  • SINTER key1 key2,返回给定集合的交集
  • SUNION key1 key2,返回给定集合的并集
  • SREM key member1 member2,删除集合中的一个或多个成员

有序集合操作命令

每个元素关联一个double类型的分数

  • ZADD key score1 member1 score2 member2 向有序集合添加一个或多个成员
  • ZSCORE KEY member,获取sorted set中的指定元素的score值
  • ZRANK KEY member,获取sorted set中的指定元素的排名
  • ZCARD key,获取元素个数
  • ZCOUNT KEY min max,统计score值在指定范围内的所有元素的个数
  • ZRANGE key start stop WITHSCORS,通过索引区间返回指定区间的成员
  • ZINCRBY key increment member,添加上增量increment
  • ZRANGEBYSCORE key min max,按照score排序后,获取指定score范围内的元素
  • ZREM key member member...,移除集合中的一个或多个成员
  • ZDIFF\ZINTER\ZUNION,求差集\交集\并集
    所有的排名默认都是升序的,如果要降序则在命令的Z后面添加REV即可

通用命令

help command查看一个命令的信息

  • KEYS pattern ,查找所有符合给定模式的key
  • EXISTS key, 检查给定key是否存在
  • TYPE key , 返回key所存储的值的类型
  • DEL key ,该命令用于在key存在时,删除key
  • EXPIRE key age,设置key的有效期
  • TTL,查看一个KEY的剩余有效期

java客户端

Jedis和Lettuce和Redisson

Jedis

引入依赖

复制代码
<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
	<version>5.2.0</version>
</dependency>

public class JedisTest {
    private Jedis jedis;
    @BeforeEach
    void setUp(){
        jedis = new Jedis("192.168.88.130", 6379);
        jedis.auth("123321");
        jedis.select(0);
    }
    @Test
    void testString(){
        String set = jedis.set("name", "虎哥");
        System.out.println(set);
        String name = jedis.get("name");
        System.out.println(name);
    }
    @AfterEach
    void tearDown(){
        if(jedis != null){
            jedis.close();
        }
    }
}

jedis连接池

配置连接池

复制代码
public class JedisConnectionFactory {
    private static JedisPool jedisPool;
    static {
        //配置连接池,
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        //最大连接
        poolConfig.setMaxIdle(8);
        //临时连接
        poolConfig.setMaxIdle(8);
        //超过等待时间清零连接
        poolConfig.setMinIdle(0);
        //最大等待时间
        poolConfig.setMaxWaitMillis(1000);
        //创建连接池
        jedisPool = new JedisPool(poolConfig,"192.168.88.130",6379,1000,"123321");

    }
    public static Jedis getJedis(){
        return jedisPool.getResource();
    }
}

修改为从连接池中获取jedis资源

复制代码
jedis = JedisConnectionFactory.getJedis();

SpringDataRedis

提供了redisTemplate工具类,其中封装了各种对redis的操作,并且将不同数据类型的操作API封装到了不同的类型中

SpringDataRedis默认使用Lettuce

引入依赖

复制代码
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-pool2</artifactId>
</dependency>

spring配置

复制代码
spring:
  redis:
    host: 192.168.88.130
    port: 6379
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
        max-wait: 100ms
    password: 123321

注入RedisTemplate

复制代码
@Autowired
private RedisTemplate redisTemplate;
@Test
void testString(){
	redisTemplate.opsForValue().set("name","虎哥");
	Object name = redisTemplate.opsForValue().get("name");
	System.out.println(name);
}

序列化

redisTemplate可以接收到任意object作为值写入redis,只不过写入之前会把object序列化为字节形式,默认是采用JDK序列化(可读性差,内存占用较大)

所以更改key和value的默认序列化

复制代码
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory){
        //创建redisteplate对象
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        //设置连接工厂
        template.setConnectionFactory(connectionFactory);
        //创建json序列化工具
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        //设置key的序列化为string序列化
        template.setValueSerializer(RedisSerializer.string());
        template.setHashValueSerializer(RedisSerializer.string());
        //设置value的序列化为json序列化
        template.setValueSerializer(genericJackson2JsonRedisSerializer);
        template.setHashValueSerializer(genericJackson2JsonRedisSerializer);
        return template;
    };
}

@Test
void testSaveUser(){
	//写入数据
	redisTemplate.opsForValue().set("user:100",new User("虎哥",100));
	//获取数据
	Object o = redisTemplate.opsForValue().get("user:100");
	System.out.println(o);
	
}

但是通常会定义一个类去与redis传输,redis中要存储这个类的信息,也比较耗内存

手动序列化

为了节省内存空间,我们并不会使用json序列化器来处理value,而是统一使用string序列化器,要求只能存储string类型的key和value,当需要存储java对象时,手动完成对象的序列化和反序列化

spring默认提供了一个StringRedisTemplate类,它的key和value的序列化方式默认就似乎string方式,省去我们自定义RedisTemplate的过程

复制代码
@Test
void testSaveUser() throws JsonProcessingException {
	//创建对象
	User user = new User("虎哥", 21);
	//手动序列化
	String s = objectMapper.writeValueAsString(user);
	//写入数据
	stringRedisTemplate.opsForValue().set("user:200",s);
	//获得数据
	String jsonUser = stringRedisTemplate.opsForValue().get("user:200");
	//手动反序列化
	User user1 = objectMapper.readValue(jsonUser, User.class);
	System.out.println(user1);

}

redisTemplate的方法习惯

redisTemplate的方法命名习惯更贴近java的习惯比如redis的Hash的使用更贴近集合中的Hashmap的方法命名而不是HSET或HGET这样的方法名

复制代码
@Test
void testHash(){
	stringRedisTemplate.opsForHash().put("user:400","name","虎哥");
	stringRedisTemplate.opsForHash().put("user:400","age","20");
	Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries("user:400");
	System.out.println(entries);
}

实战篇

  • 短信登录
  • 用户查询缓存
  • 达人探店
  • 优惠券秒杀
  • 好友关注
  • 附近商户
  • 用户签到
  • UV统计

短信登录

发送验证码

复制代码
@Override
public Result sendCode(String phone, HttpSession session) {
	//校验手机号
	if(RegexUtils.isPhoneInvalid(phone)){
		//不符合返回失败
		return Result.fail("手机号格式错误");
	}
	//符合生成验证码
	String code = RandomUtil.randomNumbers(6);
	//将验证码放入session
	session.setAttribute("code",code);
	//模拟发送验证码
	log.debug("发送验证码: {}"+ code);
	//返回ok
	return Result.ok();
}

短信验证码登录和注册

登录校验拦截器

隐藏用户敏感信息

session共享问题

session共享问题,多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题.

多台Tomcat可以互相传输session信息,但是问题是数据重复,内存浪费,而且传输也需要一定的延迟

所以需要替代方案满足:数据共享,内存存储,key\value结构(redis)

基于redis代替session登录流程

基于redis实现短信登陆

解决状态登录刷新的问题

  • service层

  • 拦截器

  • 常量类
    service层

    复制代码
      @Override
      public Result sendCode(String phone, HttpSession session) {
          //校验手机号
          if(RegexUtils.isPhoneInvalid(phone)){
              //不符合返回失败
              return Result.fail("手机号格式错误");
          }
          //符合生成验证码
          String code = RandomUtil.randomNumbers(6);
          //将验证码放入redis
          stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
          //模拟发送验证码
          log.debug("发送验证码: {}", code);
          //返回ok
          return Result.ok();
      }
    
      @Override
      public Result login(LoginFormDTO loginForm, HttpSession session) {
          // 1.校验手机号
          String phone = loginForm.getPhone();
          if (RegexUtils.isPhoneInvalid(phone)) {
              // 2.如果不符合,返回错误信息
              return Result.fail("手机号格式错误!");
          }
          // 3.从redis获取验证码并校验
          String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
          String code = loginForm.getCode();
          if (cacheCode == null || !cacheCode.equals(code)) {
              // 不一致,报错
              return Result.fail("验证码错误");
          }
    
          // 4.一致,根据手机号查询用户 select * from tb_user where phone = ?
          User user = query().eq("phone", phone).one();
          //判断用户是否存在
          if (user == null) {
              user = createUserWithPhone(phone);
          }
          //随机生成token
          String token = UUID.randomUUID().toString(true);
          //token和用户信息存入redis
          UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
          Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                  CopyOptions.create()
                          .setIgnoreNullValue(true)
                          .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
          //设置token的有效期
          String tokenKey = LOGIN_USER_KEY +token;
          stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
          stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
          //返回token给前端
          return Result.ok(token);
    
      }
    
      private User createUserWithPhone(String phone){
          //创建用户
          User user = new User();
          user.setPhone(phone);
          user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
          //保存用户
          save(user);
          return user;
      }

第一级拦截器拦截所有

复制代码
public class RefreshTokenInterceptor implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;
    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取请求头中的token
        String token = request.getHeader("authorzation");
        //还没token放行
        if(StrUtil.isBlank(token)) {
            return true;
        }
        //基于token获取redis中的用户
        String key = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        //判断用户是否存在
        if(userMap.isEmpty()){
            return true;
        }
        //将查询到的hash数据转为UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //存在,保存用户信息到threalocal
        UserHolder.saveUser(userDTO);
        //刷新token有效期
        stringRedisTemplate.expire(key,LOGIN_USER_TTL, TimeUnit.MINUTES);
        //放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}

第二级校验是否为登录用户,不是登录用户不用处理请求了(热点访问,登录,发送验证码要排除)

复制代码
public class LoginInterceptor implements HandlerInterceptor {
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断是否需要拦截(threalocal中是否有用户,看看是不是有人瞎jb点)
        //之前那个拦截器已经用token获取threadlocal中的用户信息了
        if(UserHolder.getUser()==null){
            //没有,需要拦截,设置状态码
            response.setStatus(401);
            //拦截
            return false;
        }
        return true;
    }
}

为了代码的简洁性优雅性和开闭原则,需要封装常量

复制代码
public class RedisConstants {
    public static final String LOGIN_CODE_KEY = "login:code:";
    public static final Long LOGIN_CODE_TTL = 2L;
    public static final String LOGIN_USER_KEY = "login:token:";
    public static final Long LOGIN_USER_TTL = 36000L;

    public static final Long CACHE_NULL_TTL = 2L;

    public static final Long CACHE_SHOP_TTL = 30L;
    public static final String CACHE_SHOP_KEY = "cache:shop:";

    public static final String LOCK_SHOP_KEY = "lock:shop:";
    public static final Long LOCK_SHOP_TTL = 10L;

    public static final String SECKILL_STOCK_KEY = "seckill:stock:";
    public static final String BLOG_LIKED_KEY = "blog:liked:";
    public static final String FEED_KEY = "feed:";
    public static final String SHOP_GEO_KEY = "shop:geo:";
    public static final String USER_SIGN_KEY = "sign:";
}

商户查询缓存

缓存是数据交换的缓冲区(cache),是存贮数据的临时地方,一般读写性能较高

CPU的缓存就在cpu内部,比磁盘和内存更快,一般1MB-64MB

redis缓存还是在CPU中

添加商户缓存

先到redis中查商户信息,查不到再到mysql中查,查出来放入redis中

缓存更新策略

  • 内存淘汰,reids自动实现,但一致性差

  • 超时剔除,可以给数据添加TTL,一致性一般

  • 主动更新,编写逻辑,主动实现更新,一致性好
    主动更新是最好的方案,当然也可以结合其他方案使用
    主动更新策略

  • cache aside pattern,调用者主动更新,

  • read/write through pattern,缓存与数据库整合为一个服务,但是找一个现成的这样的业务很难

  • write behind caching pattern,只改缓存
    cache aside pattern是最好的方案
    先写数据库,再删缓存要比先删缓存再写数据库的出错率低

    高一致性需求,主动更新,并以超时剔除作为兜底方案
    读操作:
    缓存命中则直接返回
    缓存未命中查询数据库,并写入缓存,设定超时时间
    写操作:
    先写数据库,然后再删除缓存
    要确保数据库与缓存操作的原子性

缓存穿透

缓存这一系列问题就是为了减少对sql数据库的查询,因为对数据库的操作相当一次网络请求,耗时长,对计算资源的消耗也高

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库,给数据库带来巨大压力

  • 缓存空对象
    优点:实现简单,维护方便
    缺点:额外的内存消耗,可能造成短期的不一致
  • 布隆过滤
    优点:内存占用较少,没有多余key
    缺点:实现复杂,存在误判可能
    缓存穿透的解决方案:
  • 缓存null值
  • 布隆过滤
  • 增强id的复杂度,避免被猜测id规律
  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限流

缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者redis服务宕机,导致大量请求到达数据库,带来巨大压力

解决方案:

  • 给不同的key的TTL添加随机值
  • 利用redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

缓存击穿

缓存击穿问题也叫热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效,无数的请求访问会在瞬间给数据库带来巨大的冲击

常见的解决方案:

  • 互斥锁
    优点:没有额外的内存消耗,保证取数据一致性,实现简单
    缺点:线程需要等待,性能受影响,可能有死锁风险

  • 逻辑过期
    优点:线程无需等待,性能较好
    缺点:不保证一致性,有额外内存消耗,实现复杂
    Apache JMeter,高并发压力测试工具
    可以显示线程处理的最大\最小\平均值,异常值,吞吐量等
    基于互斥锁解决缓存击穿

    public <R, ID> R queryWithMutex(
    String keyPrefix, ID id, Class type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
    String key = keyPrefix + id;
    // 1.从redis查询商铺缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2.判断是否存在
    if (StrUtil.isNotBlank(shopJson)) {
    // 3.存在,直接返回
    return JSONUtil.toBean(shopJson, type);
    }
    // 判断命中的是否是空值
    if (shopJson != null) {
    // 返回一个错误信息
    return null;
    }

    复制代码
      // 4.实现缓存重建
      // 4.1.获取互斥锁
      String lockKey = LOCK_SHOP_KEY + id;
      R r = null;
      try {
      	boolean isLock = tryLock(lockKey);
      	// 4.2.判断是否获取成功
      	if (!isLock) {
      		// 4.3.获取锁失败,休眠并重试
      		Thread.sleep(50);
      		return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
      	}
      	// 4.4.获取锁成功,根据id查询数据库
      	r = dbFallback.apply(id);
      	// 5.不存在,返回错误
      	if (r == null) {
      		// 将空值写入redis
      		stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
      		// 返回错误信息
      		return null;
      	}
      	// 6.存在,写入redis
      	this.set(key, r, time, unit);
      } catch (InterruptedException e) {
      	throw new RuntimeException(e);
      }finally {
      	// 7.释放锁
      	unlock(lockKey);
      }
      // 8.返回
      return r;

    }

    private boolean tryLock(String key) {
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
    }

    private void unlock(String key) {
    stringRedisTemplate.delete(key);
    }

基于逻辑过期解决缓存击穿

复制代码
    public <R, ID> R queryWithLogicalExpire(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        // 1.从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isBlank(json)) {
            // 3.存在,直接返回
            return null;
        }
        // 4.命中,需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 5.判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())) {
            // 5.1.未过期,直接返回店铺信息
            return r;
        }
        // 5.2.已过期,需要缓存重建
        // 6.缓存重建
        // 6.1.获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        // 6.2.判断是否获取锁成功
        if (isLock){
            // 6.3.成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 查询数据库
                    R newR = dbFallback.apply(id);
                    // 重建缓存
                    this.setWithLogicalExpire(key, newR, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    // 释放锁
                    unlock(lockKey);
                }
            });
        }
        // 6.4.返回过期的商铺信息
        return r;
    }

缓存工具封装

为了缓存工具开发维护成本,需要将缓存常用代码封装成工具类

分布式锁

sychronized只能对同一个jvm内的线程进行锁操作

分布式系统:多个 Java 程序可能在不同的物理或虚拟机器上运行,每个程序启动一个 JVM。

分布式锁,满足分布式系统或集群模式下多进程可见并且互斥的锁(对多个jvm的所有线程锁操作)

分布式锁的实现

Mysql:利用mysql本身的互斥锁机制

redis:利用setnx这样的互斥命令

zookeeper:利用节点的唯一性和有序性实现互斥

基于redis的分布式锁

需要实现获取锁(设置一个redis键值对)和释放锁,确保只有一个线程获取锁,也要保证获取锁和释放锁操作设置的原子性,否则某个线程获取了锁,但进程突然宕机,就无法释放锁

set lock thread1 nx ex 10//是最好的选择,nx保证互斥,ex 10在一定时间后释放锁

分布式锁误删问题

需要判断是不是自己的锁再删除

  • 在获取锁时存入线程标识(可以用UUID表示,因为不同jvm里可能有不同的线程有同一线程id)
  • 在释放锁时先获取锁中的线程标识,判断是否与当前线程标识一致,如果一致再释放锁

多条redis命令原子性操作

需要保证判断锁和释放锁的原子性操作,需要用到lua脚本,否则在极端情况会出现线程乱套的问题(比如线程A释放线程B的锁)

lua脚本,redis提供了lua脚本功能,在一个脚本中编写多条redis命令,确保原子性

官网:https://www.runoob.com/lua/lua-tutorial.html

java调用lua脚本

redisTemplate提供了调用lua脚本的API

基于redis的分布式锁实现思路

  • 利用set nx ex获取锁,并设置过期时间,保存线程标识(重点1)
  • 释放锁时,先判断线程标识是否与自己一致,一致则删除锁(重点2)
    特性:
  • 利用set nx满足互斥性
  • 利用set ex保证故障时锁依然能释放,避免死锁.提高安全性
  • 利用lua脚本保证redis命令原子性操作
  • 利用redis集群保证高可用性和高并发性

redisson

redisson是一个在redis的基础上实现的java驻内存数据网格(in-memory data grid),不仅提供了一系列的分布式的java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现

官网:https://redisson.org/

reidsson中的API方案不仅集成了上面的优化策略,还有解决以下问题的策略:

  • 不可重入:同一个线程无法多次获取同一把锁
  • 不可重试:获取锁只尝试一次就返回false,没有重试机制
  • 超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患
  • 主从一致性:如果redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从并没有同步主中的锁数据,则会出现锁实现

入门

引入依赖

复制代码
<dependency>
	<groupId>org.redisson</groupId>
	<artifactId>redisson</artifactId>
	<version>3.13.6</version>
</dependency>

配置redisson

复制代码
@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient(){
        // 配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.88.130:6379").setPassword("123321");
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
}

使用redisson

复制代码
@Transactional
public Result createVoucherOrder(Long voucherId) {
	// 5.一人一单
	Long userId = UserHolder.getUser().getId();

	// 创建锁对象
	RLock redisLock = redissonClient.getLock("lock:order:" + userId);
	// 尝试获取锁
	boolean isLock = redisLock.tryLock();
	// 判断
	if(!isLock){
		// 获取锁失败,直接返回失败或者重试
		return Result.fail("不允许重复下单!");
	}

	try {
		.....
	} finally {
		// 释放锁
		redisLock.unlock();
	}

}

可重入锁

利用hash结构记录线程id和重入次数

既要记录线程id和重入次数还是hash比string要方便

重试和超时续约

可重试:利用信号量和PubSub功能实现等待,唤醒,获取锁失败的重试机制

超时续约:利用watchDog,每隔一段时间(releaseTime/3),重置超时时间

主从一致性

redisson的multiLock:

  • 原理: 多个独立的redis节点(redis集群,相当于多个mysql数据库备份),必须在所有节点都获取重入锁,才算获取锁成功
  • 缺陷:运维成本高,实现复杂
相关推荐
ysa051030几秒前
【板子】二分答案(最大最小?)
c++·笔记·算法·板子
jiay230 分钟前
【.net10】顶级程序语句
java·开发语言
2601_953824611 小时前
【计算机毕业设计】基于Spring Boot的画师接稿平台设计与实现
java·spring boot·后端
吃好睡好便好1 小时前
近期几点体会
学习·生活
xlrqx2 小时前
商丘家电清洗培训零基础学习需掌握哪些要点零基础到底能不能学
python·学习
小宋10212 小时前
不用学算法也能玩转 AI?普通 Java 程序员转 AI 应用开发路线
java·人工智能·重构
编程圈子2 小时前
电机驱动开发学习19. 霍尔 BLDC 三段式 FOC 启动算法
驱动开发·学习·算法
我是唐青枫2 小时前
Java Jetty 实战详解:从嵌入式 HTTP 服务到 Spring Boot 容器替换
java
带刺的坐椅2 小时前
当所有人都在用 TS/Python 写 Agent,我们为什么坚持 Java
java·ai·solon·codex·opencode·soloncode
MartinYeung52 小时前
[论文学习]LLM 中的个性化安全:一个基准测试与基于规划的智能体方法
人工智能·学习·安全