Recommended learning: Redis video tutorial
In a single application, if we do not lock the shared data, Data consistency problems will arise, and our solution is usually to lock them.
In the distributed architecture, we will also encounter data sharing operation problems. This article uses Redis
to solve the data consistency problem in the distributed architecture.
The single-machine data consistency architecture is shown in the figure below: multiple clients can access the same server and connect to the same database.
Scene description: The client simulates the process of purchasing goods, and sets the total inventory in Redis
to leave 100, multiple Clients make concurrent purchases at the same time.
@RestController public class IndexController1 { @Autowired StringRedisTemplate template; @RequestMapping("/buy1") public String index(){ // Redis中存有goods:001号商品,数量为100 String result = template.opsForValue().get("goods:001"); // 获取到剩余商品数 int total = result == null ? 0 : Integer.parseInt(result); if( total > 0 ){ // 剩余商品数大于0 ,则进行扣减 int realTotal = total -1; // 将商品数回写数据库 template.opsForValue().set("goods:001",String.valueOf(realTotal)); System.out.println("购买商品成功,库存还剩:"+realTotal +"件, 服务端口为8001"); return "购买商品成功,库存还剩:"+realTotal +"件, 服务端口为8001"; }else{ System.out.println("购买商品失败,服务端口为8001"); } return "购买商品失败,服务端口为8001"; } }
Use Jmeter
to simulate high concurrency scenarios. The test results are as follows:
Test results Multiple users purchased the same product, and data inconsistency occurred!
Solution: In the case of a single application, perform locking operations on concurrent operations to ensure that data operations are atomic
synchronized
ReentrantLock
@RestController public class IndexController2 { // 使用ReentrantLock锁解决单体应用的并发问题 Lock lock = new ReentrantLock(); @Autowired StringRedisTemplate template; @RequestMapping("/buy2") public String index() { lock.lock(); try { String result = template.opsForValue().get("goods:001"); int total = result == null ? 0 : Integer.parseInt(result); if (total > 0) { int realTotal = total - 1; template.opsForValue().set("goods:001", String.valueOf(realTotal)); System.out.println("购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8001"); return "购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8001"; } else { System.out.println("购买商品失败,服务端口为8001"); } } catch (Exception e) { lock.unlock(); } finally { lock.unlock(); } return "购买商品失败,服务端口为8001"; } }
The above solves the problem of single application data consistency problem, but if it is a distributed architecture deployment, the architecture is as follows:
provides two services, the ports are 8001
, 8002
, and the connection is the same A Redis
service, in front of the service there is a Nginx
as a load balancer
The two service codes are the same, but the ports are different
Start the two services 8001
and 8002
. Each service is still locked with ReentrantLock
and done with Jmeter
Concurrency testing found that data consistency issues may occur!
Cancel the stand-alone lock, use redis
below set
Command to implement distributed locking
SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]
@RestController public class IndexController4 { // Redis分布式锁的key public static final String REDIS_LOCK = "good_lock"; @Autowired StringRedisTemplate template; @RequestMapping("/buy4") public String index(){ // 每个人进来先要进行加锁,key值为"good_lock",value随机生成 String value = UUID.randomUUID().toString().replace("-",""); try{ // 加锁 Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value); // 加锁失败 if(!flag){ return "抢锁失败!"; } System.out.println( value+ " 抢锁成功"); String result = template.opsForValue().get("goods:001"); int total = result == null ? 0 : Integer.parseInt(result); if (total > 0) { int realTotal = total - 1; template.opsForValue().set("goods:001", String.valueOf(realTotal)); // 如果在抢到所之后,删除锁之前,发生了异常,锁就无法被释放, // 释放锁操作不能在此操作,要在finally处理 // template.delete(REDIS_LOCK); System.out.println("购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8001"); return "购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8001"; } else { System.out.println("购买商品失败,服务端口为8001"); } return "购买商品失败,服务端口为8001"; }finally { // 释放锁 template.delete(REDIS_LOCK); } } }
The above code can solve the problem of data consistency in distributed architecture. But if you think about it more carefully, there will still be problems. Let’s make improvements below.
In the above code, if the machine where the microservice jar
package is deployed suddenly hangs during the running of the program, The code level has not reached the finally
code block at all, which means that the lock has not been deleted before the shutdown. In this case, there is no way to guarantee unlocking
So, here is what is needed Add an expiration time to this key
. There are two ways to set the expiration time in Redis
:
template.expire(REDIS_LOCK,10, TimeUnit.SECONDS)
template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS)
The first method It requires a separate line of code, and it is not placed in the same step as locking, so it is not atomic and will cause problems.
The second method sets the expiration time at the same time as locking. There is no problem. Here we use this method
Adjust the code and set the expiration time while locking:
// 为key加一个过期时间,其余代码不变 Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK,value,10L,TimeUnit.SECONDS);
This method solves the problem of being unable to release the lock due to sudden service downtime. The problem. But if you think about it more carefully, there will still be problems. Let’s make improvements below.
Method two sets the expiration time of key
, which solves the problem that key
cannot be deleted, but The problem is here again
上面设置了key
的过期时间为10
秒,如果业务逻辑比较复杂,需要调用其他微服务,处理时间需要15
秒(模拟场
景,别较真),而当10
秒钟过去之后,这个key
就过期了,其他请求就又可以设置这个key
,此时如果耗时15
秒
的请求处理完了,回来继续执行程序,就会把别人设置的key
给删除了,这是个很严重的问题!
所以,谁上的锁,谁才能删除
@RestController public class IndexController6 { public static final String REDIS_LOCK = "good_lock"; @Autowired StringRedisTemplate template; @RequestMapping("/buy6") public String index(){ // 每个人进来先要进行加锁,key值为"good_lock" String value = UUID.randomUUID().toString().replace("-",""); try{ // 为key加一个过期时间 Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS); // 加锁失败 if(!flag){ return "抢锁失败!"; } System.out.println( value+ " 抢锁成功"); String result = template.opsForValue().get("goods:001"); int total = result == null ? 0 : Integer.parseInt(result); if (total > 0) { // 如果在此处需要调用其他微服务,处理时间较长。。。 int realTotal = total - 1; template.opsForValue().set("goods:001", String.valueOf(realTotal)); System.out.println("购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8001"); return "购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8001"; } else { System.out.println("购买商品失败,服务端口为8001"); } return "购买商品失败,服务端口为8001"; }finally { // 谁加的锁,谁才能删除!!!! if(template.opsForValue().get(REDIS_LOCK).equals(value)){ template.delete(REDIS_LOCK); } } } }
这种方式解决了因服务处理时间太长而释放了别人锁的问题。这样就没问题了吗?
在上面方式三下,规定了谁上的锁,谁才能删除,但finally
快的判断和del
删除操作不是原子操作,并发的时候也会出问题,并发嘛,就是要保证数据的一致性,保证数据的一致性,最好要保证对数据的操作具有原子性。
在Redis
的set
命令介绍中,最后推荐Lua
脚本进行锁的删除,地址
@RestController public class IndexController7 { public static final String REDIS_LOCK = "good_lock"; @Autowired StringRedisTemplate template; @RequestMapping("/buy7") public String index(){ // 每个人进来先要进行加锁,key值为"good_lock" String value = UUID.randomUUID().toString().replace("-",""); try{ // 为key加一个过期时间 Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS); // 加锁失败 if(!flag){ return "抢锁失败!"; } System.out.println( value+ " 抢锁成功"); String result = template.opsForValue().get("goods:001"); int total = result == null ? 0 : Integer.parseInt(result); if (total > 0) { // 如果在此处需要调用其他微服务,处理时间较长。。。 int realTotal = total - 1; template.opsForValue().set("goods:001", String.valueOf(realTotal)); System.out.println("购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8001"); return "购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8001"; } else { System.out.println("购买商品失败,服务端口为8001"); } return "购买商品失败,服务端口为8001"; }finally { // 谁加的锁,谁才能删除,使用Lua脚本,进行锁的删除 Jedis jedis = null; try{ jedis = RedisUtils.getJedis(); String script = "if redis.call('get',KEYS[1]) == ARGV[1] " + "then " + "return redis.call('del',KEYS[1]) " + "else " + " return 0 " + "end"; Object eval = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value)); if("1".equals(eval.toString())){ System.out.println("-----del redis lock ok...."); }else{ System.out.println("-----del redis lock error ...."); } }catch (Exception e){ }finally { if(null != jedis){ jedis.close(); } } } } }
在方式四下,规定了谁上的锁,谁才能删除,并且解决了删除操作没有原子性问题。但还没有考虑缓存续命,以及Redis
集群部署下,异步复制造成的锁丢失:主节点没来得及把刚刚set
进来这条数据给从节点,就挂了。所以直接上RedLock
的Redisson
落地实现。
@RestController public class IndexController8 { public static final String REDIS_LOCK = "good_lock"; @Autowired StringRedisTemplate template; @Autowired Redisson redisson; @RequestMapping("/buy8") public String index(){ RLock lock = redisson.getLock(REDIS_LOCK); lock.lock(); // 每个人进来先要进行加锁,key值为"good_lock" String value = UUID.randomUUID().toString().replace("-",""); try{ String result = template.opsForValue().get("goods:001"); int total = result == null ? 0 : Integer.parseInt(result); if (total > 0) { // 如果在此处需要调用其他微服务,处理时间较长。。。 int realTotal = total - 1; template.opsForValue().set("goods:001", String.valueOf(realTotal)); System.out.println("购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8001"); return "购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8001"; } else { System.out.println("购买商品失败,服务端口为8001"); } return "购买商品失败,服务端口为8001"; }finally { if(lock.isLocked() && lock.isHeldByCurrentThread()){ lock.unlock(); } } } }
推荐学习:Redis视频教程
The above is the detailed content of Summary of five methods for implementing distributed locks in Redis. For more information, please follow other related articles on the PHP Chinese website!