使用redis incr处理并发,存在死锁问题

布满荆棘的人生 2023-10-17 22:52 103阅读 0赞

目录

1.项目场景:

2.问题代码 :

3.修改后的代码:

4.实战示例:

5.更新:setIfAbsent

6.再次更新:redisson


1.项目场景:

锁主要是用来实现资源共享同步,只有获取到了锁才能访问该同步代码,否则等待其他线程使用结束释放锁。

2.问题代码 :

  1. @Autowired
  2. private RedisTemplate redisTemplate;
  3. /**
  4. * 加锁
  5. */
  6. public boolean getLock(String key) {
  7. try {
  8. long count = redisTemplate.opsForValue().increment(key, 1);
  9. //此段代码出现异常则会出现死锁问题,key一直都存在
  10. if(count == 1){
  11. //设置有效期2秒
  12. redisTemplate.expire(key, 2, TimeUnit.SECONDS);
  13. return true;
  14. }
  15. //如果存在表示重复
  16. return false;
  17. } catch (Exception e) {
  18. logger.error("redis加锁异常", e);
  19. redisTemplate.delete(key); //出现异常删除锁
  20. return true;
  21. }
  22. }

当正常情况没有问题,但是当计数器设置成功后,服务出现异常,那么这个key会永远存在,这样肯定不行。

Redis Incr 命令将 key 中储存的数字值增一,如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作,且将key的有效时间设置为长期有效。

3.修改后的代码:

  1. @Autowired
  2. private RedisTemplate redisTemplate;
  3. /**
  4. * 加锁
  5. */
  6. public boolean getLock(String key) {
  7. try {
  8. long count = redisTemplate.opsForValue().increment(key, 1);
  9. if(count == 1){
  10. //设置有效期2秒
  11. redisTemplate.expire(key, 2, TimeUnit.SECONDS);
  12. return true;
  13. }else{
  14. long time = redisTemplate.getExpire(key,TimeUnit.SECONDS);
  15. if(time == -1){
  16. //设置失败重新设置过期时间
  17. redisTemplate.expire(key, 2, TimeUnit.SECONDS);
  18. return true;
  19. }
  20. }
  21. //如果存在表示重复
  22. return false;
  23. } catch (Exception e) {
  24. logger.error("redis加锁异常", e);
  25. redisTemplate.delete(key); //出现异常删除锁
  26. return true;
  27. }
  28. }

4.实战示例:

场景:同一个接口,必须等待上个线程处理完后,业务才能处理。

Service层:RedisLock.java

  1. package com.test.utils;
  2. import java.util.concurrent.TimeUnit;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.data.redis.core.RedisTemplate;
  5. import org.springframework.stereotype.Service;
  6. import lombok.extern.slf4j.Slf4j;
  7. /**
  8. * Redis锁
  9. */
  10. @Service
  11. @Slf4j
  12. public class RedisLock {
  13. @Autowired
  14. private RedisTemplate<String, ?> redisTemplate;
  15. /**
  16. * 加锁
  17. * @param key key
  18. * @param timeExpire 过期时间单位秒
  19. * @return true表示key存在
  20. */
  21. public boolean getLock(String key, long timeExpire) {
  22. try {
  23. long count = redisTemplate.opsForValue().increment(key, 1);
  24. if(count == 1){
  25. //设置有效期timeExpire
  26. redisTemplate.expire(key, timeExpire, TimeUnit.SECONDS);
  27. return false;
  28. }else{
  29. long time = redisTemplate.getExpire(key,TimeUnit.SECONDS);
  30. if(time == -1){
  31. //设置失败重新设置过期时间
  32. redisTemplate.expire(key, timeExpire, TimeUnit.SECONDS);
  33. return false;
  34. }
  35. }
  36. //如果存在表示重复
  37. return true;
  38. } catch (Exception e) {
  39. log.error("redis加锁异常", e);
  40. redisTemplate.delete(key); //出现异常删除锁
  41. return false;
  42. }
  43. }
  44. /**
  45. * 解锁
  46. * @param key key
  47. * @return true表示成功
  48. */
  49. public boolean unLock(String key) {
  50. try {
  51. redisTemplate.delete(key); //删除key
  52. return true;
  53. } catch (Exception e) {
  54. log.error("redis解锁异常", e);
  55. redisTemplate.delete(key); //出现异常删除锁
  56. return false;
  57. }
  58. }
  59. }

Controller层:TestController.java

  1. package com.test.controller;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.web.bind.annotation.RequestMapping;
  4. import org.springframework.web.bind.annotation.RequestMethod;
  5. import org.springframework.web.bind.annotation.RestController;
  6. import com.test.utils.RedisLock;
  7. import lombok.extern.slf4j.Slf4j;
  8. @RestController
  9. @Slf4j
  10. public class TestController{
  11. @Autowired
  12. private RedisLock redisLock;
  13. /**
  14. * 测试并发
  15. */
  16. @RequestMapping(value = "/test", method = RequestMethod.GET)
  17. public String test(String key) {
  18. try {
  19. long expireTime = 30000; //有效时长(毫秒)
  20. long beginTime = System.currentTimeMillis(); //开始时间
  21. //判断同一时间是否有多个用户操作
  22. boolean flagLock = redisLock.getLock(key, 6);
  23. if(flagLock){
  24. //如果锁存在,尝试重新获取锁
  25. while (redisLock.getLock(key, 6)) {
  26. //执行时长(毫秒)
  27. long time = System.currentTimeMillis() - beginTime;
  28. //防止死循环
  29. if(time > expireTime){
  30. log.error("超时,请稍后重试");
  31. return "超时,请稍后重试!";
  32. }
  33. Thread.sleep(100);
  34. }
  35. }
  36. //TODO 业务操作...
  37. return "success";
  38. } catch (Exception e) {
  39. log.error("出错:", e);
  40. return "出错,请联系管理员!";
  41. }finally {
  42. //解锁
  43. redisLock.unLock(key);
  44. }
  45. }
  46. }

5.更新:setIfAbsent

将redis版本升级到2.1以上,可以直接使用setIfAbsent设置过期时间,使用 4 个参数的重载方法是原子性的。

  1. redisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.SECONDS);

说明:当key不存在,将key的值设为value,并设置过期时间,返回true;若给定的key已经存在,则不做任何动作,并返回false。此方法是原子性的。

watermark_type_d3F5LXplbmhlaQ_shadow_50_text_Q1NETiBA5rab5ZOl5piv5Liq5aSn5biF5q-U_size_15_color_FFFFFF_t_70_g_se_x_16

使用setIfAbsent

  1. @Autowired
  2. private RedisTemplate redisTemplate;
  3. /**
  4. * 加锁
  5. * @param key key
  6. * @param value value
  7. * @param timeout 过期时间单位秒
  8. * @param true表示key不存在
  9. */
  10. public boolean getLock(String key, String value, long timeout) {
  11. try {
  12. //原子性操作
  13. boolean flag = redisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.SECONDS);
  14. //如果存在false表示重复
  15. return flag;
  16. } catch (Exception e) {
  17. logger.error("redis加锁异常", e);
  18. redisTemplate.delete(key); //出现异常删除锁
  19. return true;
  20. }
  21. }

6.再次更新:redisson

推荐使用Redisson来实现分布式锁,当然前面两种方式也是可行的,条条大路通罗马。

什么是redisson:

Redisson - 是一个高级的分布式协调Redis客服端,Redisson API 侧重于分布式开发,充分的利用了Redis键值数据库提供的一系列优势,在Java实用工具包中常用接口的基础上,为使用者提供了一系列具有分布式特性的常用工具类。

引入依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-redis</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.redisson</groupId>
  7. <artifactId>redisson</artifactId>
  8. <version>3.7.5</version>
  9. </dependency>

application.properties

  1. # Redis服务器地址
  2. spring.redis.host=127.0.0.1
  3. # Redis服务器连接密码(默认为空)
  4. spring.redis.password=
  5. # Redis服务器连接端口
  6. spring.redis.port=6379

Redisson的配置类

  1. import org.redisson.Redisson;
  2. import org.redisson.api.RedissonClient;
  3. import org.redisson.config.Config;
  4. import org.springframework.beans.factory.annotation.Value;
  5. import org.springframework.context.annotation.Bean;
  6. import org.springframework.context.annotation.Configuration;
  7. import java.io.IOException;
  8. @Configuration
  9. public class RedissonConfig {
  10. @Value("${spring.redis.host}")
  11. private String host;
  12. @Value("${spring.redis.port}")
  13. private String port;
  14. @Value("${spring.redis.password}")
  15. private String password;
  16. /**
  17. * RedissonClient,单机模式
  18. * @return
  19. * @throws IOException
  20. */
  21. @Bean
  22. public RedissonClient redissonClient() {
  23. Config config = new Config();
  24. config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password);
  25. return Redisson.create(config);
  26. }
  27. }

Redisson分布式锁业务类

  1. import java.util.concurrent.TimeUnit;
  2. import org.redisson.api.RLock;
  3. import org.redisson.api.RedissonClient;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.stereotype.Service;
  6. import lombok.extern.slf4j.Slf4j;
  7. @Slf4j
  8. @Service
  9. public class TestService {
  10. @Autowired
  11. RedissonClient redissonClient;
  12. private final static String LOCK_KEY = "TEST_KEY";
  13. int count = 50;
  14. public void seckill() {
  15. //定义锁
  16. RLock rlock = redissonClient.getLock(LOCK_KEY);
  17. try {
  18. //尝试加锁,最大等待时间30秒,上锁10秒自动解锁
  19. boolean flag = rlock.tryLock(30, 10, TimeUnit.SECONDS);
  20. if (flag) {
  21. log.info("线程:" + Thread.currentThread().getName() + "获得了锁");
  22. log.info("剩余数量:{}", --count);
  23. }
  24. } catch (Exception e) {
  25. log.error("程序执行异常:", e);
  26. //释放锁
  27. rlock.unlock();
  28. } finally {
  29. log.info("线程:" + Thread.currentThread().getName() + "准备释放锁");
  30. //释放锁
  31. rlock.unlock();
  32. }
  33. }
  34. }

源码地址包含三种方式:

https://download.csdn.net/download/u011974797/86772996

发表评论

表情:
评论列表 (有 0 条评论,103人围观)

还没有评论,来说两句吧...

相关阅读