Springboot使用Redis实现分布式锁

逃离我推掉我的手 2023-02-16 01:42 115阅读 0赞

基于 redis的 SETNX()、EXPIRE() 方法做分布式锁

-SETNX()
setnx接收两个参数key,value。如果key存在,则不做任何操作,返回0,若key不存在,则设置成功,返回1。
-EXPIRE()
expire 设置过期时间,要注意的是 setnx 命令不能设置 key 的超时时间,只能通过 expire() 来对 key 设置。

注意:

1.加锁过程必须设置过期时间,加锁和设置过期时间过程必须是原子操作
如果没有设置过期时间,那么就发生死锁,锁永远不能被释放。如果加锁后服务宕机或程序崩溃,来不及设置过期时间,同样会发生死锁。
2.解锁必须是解除自己加上的锁
试想一个这样的场景,服务A加锁,但执行效率非常慢,导致锁失效后还未执行完,但这时候服务B已经拿到锁了,这时候服务A执行完毕了去解锁,把服务B的锁给解掉了,其他服务C、D、E…都可以拿到锁了,这就有问题了。加锁的时候我们可以设置唯一value,解锁时判断是不是自己先前的value就行了。

项目目录结构:

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzMzcxNzY2_size_16_color_FFFFFF_t_70

pom.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  4. <modelVersion>4.0.0</modelVersion>
  5. <parent>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-parent</artifactId>
  8. <version>2.3.0.RELEASE</version>
  9. <relativePath/> <!-- lookup parent from repository -->
  10. </parent>
  11. <groupId>com.cxb</groupId>
  12. <artifactId>redis</artifactId>
  13. <version>0.0.1-SNAPSHOT</version>
  14. <name>redis-lock</name>
  15. <description>Demo project for Spring Boot</description>
  16. <properties>
  17. <java.version>1.8</java.version>
  18. </properties>
  19. <dependencies>
  20. <dependency>
  21. <groupId>org.springframework.boot</groupId>
  22. <artifactId>spring-boot-starter-web</artifactId>
  23. </dependency>
  24. <dependency>
  25. <groupId>org.springframework.boot</groupId>
  26. <artifactId>spring-boot-starter-test</artifactId>
  27. <scope>test</scope>
  28. <exclusions>
  29. <exclusion>
  30. <groupId>org.junit.vintage</groupId>
  31. <artifactId>junit-vintage-engine</artifactId>
  32. </exclusion>
  33. </exclusions>
  34. </dependency>
  35. <!-- redis依赖 -->
  36. <dependency>
  37. <groupId>org.springframework.boot</groupId>
  38. <artifactId>spring-boot-starter-cache</artifactId>
  39. </dependency>
  40. <dependency>
  41. <groupId>org.springframework.boot</groupId>
  42. <artifactId>spring-boot-starter-data-redis</artifactId>
  43. <exclusions>
  44. <!-- 不依赖Redis的异步客户端lettuce -->
  45. <exclusion>
  46. <groupId>io.lettuce</groupId>
  47. <artifactId>lettuce-core</artifactId>
  48. </exclusion>
  49. </exclusions>
  50. </dependency>
  51. <dependency>
  52. <groupId>redis.clients</groupId>
  53. <artifactId>jedis</artifactId>
  54. <version>3.0.1</version>
  55. </dependency>
  56. <dependency>
  57. <groupId>org.apache.commons</groupId>
  58. <artifactId>commons-pool2</artifactId>
  59. <version>2.5.0</version>
  60. </dependency>
  61. </dependencies>
  62. <build>
  63. <plugins>
  64. <plugin>
  65. <groupId>org.springframework.boot</groupId>
  66. <artifactId>spring-boot-maven-plugin</artifactId>
  67. </plugin>
  68. </plugins>
  69. </build>
  70. </project>

application.properties

  1. # Redis数据库索引(默认为0)
  2. spring.redis.database=0
  3. # Redis服务器地址
  4. spring.redis.host=127.0.0.1
  5. # Redis服务器连接端口
  6. spring.redis.port=6379
  7. # Redis服务器连接密码(默认为空)
  8. spring.redis.password=
  9. # 连接池最大连接数(使用负值表示没有限制)
  10. spring.redis.pool.max-active=200
  11. # 连接池最大阻塞等待时间(使用负值表示没有限制)
  12. spring.redis.pool.max-wait=-1
  13. # 连接池中的最大空闲连接
  14. spring.redis.pool.max-idle=10
  15. # 连接池中的最小空闲连接
  16. spring.redis.pool.min-idle=1
  17. # 连接超时时间(毫秒)
  18. spring.redis.timeout=10000
  19. #是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个
  20. redis.testOnBorrow=true
  21. #在空闲时检查有效性, 默认false
  22. redis.testWhileIdle=true
  23. server.port=8082
  24. RedisConfig
  25. package com.cxb.redis;
  26. import org.springframework.beans.factory.annotation.Autowired;
  27. import org.springframework.context.annotation.Bean;
  28. import org.springframework.context.annotation.Configuration;
  29. import org.springframework.data.redis.connection.RedisConnectionFactory;
  30. import org.springframework.data.redis.core.*;
  31. import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
  32. import org.springframework.data.redis.serializer.StringRedisSerializer;
  33. @Configuration
  34. public class RedisConfig {
  35. /**
  36. * 注入 RedisConnectionFactory
  37. */
  38. @Autowired
  39. RedisConnectionFactory redisConnectionFactory;
  40. /**
  41. * 实例化 RedisTemplate 对象
  42. *
  43. * @return
  44. */
  45. @Bean
  46. public RedisTemplate<String, Object> functionDomainRedisTemplate() {
  47. RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
  48. initDomainRedisTemplate(redisTemplate, redisConnectionFactory);
  49. return redisTemplate;
  50. }
  51. /**
  52. * 设置数据存入 redis 的序列化方式
  53. *
  54. * @param redisTemplate
  55. * @param factory
  56. */
  57. private void initDomainRedisTemplate(RedisTemplate<String, Object> redisTemplate, RedisConnectionFactory factory) {
  58. redisTemplate.setKeySerializer(new StringRedisSerializer());
  59. redisTemplate.setHashKeySerializer(new StringRedisSerializer());
  60. redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
  61. redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
  62. redisTemplate.setConnectionFactory(factory);
  63. }
  64. /**
  65. * 实例化 HashOperations 对象,可以使用 Hash 类型操作
  66. *
  67. * @param redisTemplate
  68. * @return
  69. */
  70. @Bean
  71. public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
  72. return redisTemplate.opsForHash();
  73. }
  74. /**
  75. * 实例化 ValueOperations 对象,可以使用 String 操作
  76. *
  77. * @param redisTemplate
  78. * @return
  79. */
  80. @Bean
  81. public ValueOperations<String, Object> valueOperations(RedisTemplate<String, Object> redisTemplate) {
  82. return redisTemplate.opsForValue();
  83. }
  84. /**
  85. * 实例化 ListOperations 对象,可以使用 List 操作
  86. *
  87. * @param redisTemplate
  88. * @return
  89. */
  90. @Bean
  91. public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
  92. return redisTemplate.opsForList();
  93. }
  94. /**
  95. * 实例化 SetOperations 对象,可以使用 Set 操作
  96. *
  97. * @param redisTemplate
  98. * @return
  99. */
  100. @Bean
  101. public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
  102. return redisTemplate.opsForSet();
  103. }
  104. /**
  105. * 实例化 ZSetOperations 对象,可以使用 ZSet 操作
  106. *
  107. * @param redisTemplate
  108. * @return
  109. */
  110. @Bean
  111. public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
  112. return redisTemplate.opsForZSet();
  113. }
  114. }
  115. RedisLock
  116. package com.cxb.redis;
  117. import org.springframework.data.redis.connection.RedisConnection;
  118. import org.springframework.data.redis.connection.RedisConnectionFactory;
  119. import org.springframework.data.redis.connection.RedisStringCommands;
  120. import org.springframework.data.redis.connection.ReturnType;
  121. import org.springframework.data.redis.core.RedisConnectionUtils;
  122. import org.springframework.data.redis.core.StringRedisTemplate;
  123. import org.springframework.data.redis.core.types.Expiration;
  124. import org.springframework.stereotype.Repository;
  125. import java.nio.charset.Charset;
  126. import java.util.UUID;
  127. import java.util.concurrent.TimeUnit;
  128. @Repository
  129. public class RedisLock {
  130. /**
  131. * 解锁脚本,原子操作
  132. */
  133. private static final String unlockScript =
  134. "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n"
  135. + "then\n"
  136. + " return redis.call(\"del\",KEYS[1])\n"
  137. + "else\n"
  138. + " return 0\n"
  139. + "end";
  140. private StringRedisTemplate redisTemplate;
  141. public RedisLock(StringRedisTemplate redisTemplate) {
  142. this.redisTemplate = redisTemplate;
  143. }
  144. /**
  145. * 加锁,有阻塞
  146. * @param name
  147. * @param expire
  148. * @param timeout
  149. * @return
  150. */
  151. public String lock(String name, long expire, long timeout){
  152. long startTime = System.currentTimeMillis();
  153. String token;
  154. do{
  155. token = tryLock(name, expire);
  156. if(token == null) {
  157. if((System.currentTimeMillis()-startTime) > (timeout-50))
  158. break;
  159. try {
  160. Thread.sleep(50); //try 50 per sec
  161. } catch (InterruptedException e) {
  162. e.printStackTrace();
  163. return null;
  164. }
  165. }
  166. }while(token==null);
  167. return token;
  168. }
  169. /**
  170. * 加锁,无阻塞
  171. * @param name
  172. * @param expire
  173. * @return
  174. */
  175. public String tryLock(String name, long expire) {
  176. String token = UUID.randomUUID().toString();
  177. RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
  178. RedisConnection conn = factory.getConnection();
  179. try{
  180. Boolean result = conn.set(name.getBytes(Charset.forName("UTF-8")), token.getBytes(Charset.forName("UTF-8")),
  181. Expiration.from(expire, TimeUnit.MILLISECONDS), RedisStringCommands.SetOption.SET_IF_ABSENT);
  182. if(result!=null && result)
  183. return token;
  184. }finally {
  185. RedisConnectionUtils.releaseConnection(conn, factory);
  186. }
  187. return null;
  188. }
  189. /**
  190. * 解锁
  191. * @param name
  192. * @param token
  193. * @return
  194. */
  195. public boolean unlock(String name, String token) {
  196. byte[][] keysAndArgs = new byte[2][];
  197. keysAndArgs[0] = name.getBytes(Charset.forName("UTF-8"));
  198. keysAndArgs[1] = token.getBytes(Charset.forName("UTF-8"));
  199. RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
  200. RedisConnection conn = factory.getConnection();
  201. try {
  202. Long result = (Long)conn.scriptingCommands().eval(unlockScript.getBytes(Charset.forName("UTF-8")), ReturnType.INTEGER, 1, keysAndArgs);
  203. if(result!=null && result>0)
  204. return true;
  205. }finally {
  206. RedisConnectionUtils.releaseConnection(conn, factory);
  207. }
  208. return false;
  209. }
  210. }
  211. IndexController
  212. package com.cxb.redis;
  213. import org.springframework.beans.factory.annotation.Autowired;
  214. import org.springframework.transaction.annotation.Transactional;
  215. import org.springframework.web.bind.annotation.GetMapping;
  216. import org.springframework.web.bind.annotation.RestController;
  217. @RestController
  218. public class IndexController {
  219. @Autowired
  220. private RedisLock redisLock;
  221. @GetMapping("/")
  222. @Transactional(rollbackFor = Exception.class)
  223. public String testLock() {
  224. String token = null;
  225. String result = null;
  226. try {
  227. token = redisLock.lock("lock_name", 10000, 11000);
  228. if (token != null) {
  229. System.out.println("我拿到了锁哦...");
  230. // 执行业务代码
  231. Thread.sleep(15000L);
  232. result = "我拿到了锁哦, 执行业务代码";
  233. } else {
  234. System.out.println("我没有拿到锁唉...");
  235. result = "我没有拿到锁唉";
  236. }
  237. }
  238. catch (Exception e){
  239. System.out.println("处理业务逻辑报错...");
  240. result = "处理业务逻辑报错";
  241. }
  242. finally {
  243. if (token != null) {
  244. redisLock.unlock("lock_name", token);
  245. System.out.println("释放了锁...");
  246. }
  247. }
  248. return result;
  249. }
  250. }
  251. RedisLockApplication
  252. package com.cxb.redis;
  253. import org.springframework.boot.SpringApplication;
  254. import org.springframework.boot.autoconfigure.SpringBootApplication;
  255. import org.springframework.cache.annotation.EnableCaching;
  256. @SpringBootApplication
  257. @EnableCaching
  258. public class RedisLockApplication {
  259. public static void main(String[] args) {
  260. SpringApplication.run(RedisLockApplication.class, args);
  261. }
  262. }

启动项目测试:这里启动多个实例,先直接run RedisLockApplication 启动实例一

勾选图中所示,然后修改application.properties启动的端口,再run RedisLockApplication 启动实例二

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzMzcxNzY2_size_16_color_FFFFFF_t_70 1

访问:http://localhost:8082/ 和 http://localhost:8081/

因为处理业务逻辑这里模拟睡眠了15s,看谁先访问,必须等前者处理完之后才会执行后者。

代码下载

参考文件

发表评论

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

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

相关阅读

    相关 使用redis实现分布式

    简介: 当高并发访问某个接口的时候,如果这个接口访问的数据库中的资源,并且你的数据库事务级别是可重复读(Repeatable read)的话,确实是没有线程问题的,因为数