Foreword
In the previous section, we introduced how to use traditional locks (row locks, optimistic locks, pessimistic locks) of the MySQL database to solve the “oversold problem” caused by concurrent access. Although mysql’s traditional lock can solve the problem of concurrent access very well, in terms of performance, mysql’s performance does not seem to be that good, and it will be subject to single points of failure. In this section, we introduce a solution with better performance, using the in-memory database redis to implement distributed locks to control the “overselling” problem caused by concurrent access. There is no introduction to the establishment of the redis environment here. You can check the author’s previous blog content.
Text
- Add redis dependencies and configuration information to the project
– pom dependency configuration
<!-- Database connection pool toolkit --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <!--redis launcher--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>– application.yml configuration
spring: application: name: ht-atp-plat datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.110.88:3306/ht-atp?characterEncoding=utf-8 & serverTimezone=GMT+8 & useAffectedRows=true & nullCatalogMeansCurrent=true username: root password: root profiles: active:dev # redis configuration redis: host: 192.168.110.88 lettuce: pool: #The maximum number of connections in the connection pool (use a negative value to indicate no limit), the default is 8 max-active: 8 # The minimum idle connection in the connection pool defaults to 0 min-idle: 1 # The maximum blocking waiting time of the connection pool (use a negative value to indicate no limit), the default is -1 max-wait: 1000 #The maximum idle connection in the connection pool defaults to 8 max-idle: 8– redis serialization configuration
package com.ht.atp.plat.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration public class RedisConfig { /** * @param factory * @return */ @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { //Cache serialization configuration to avoid storing garbled characters RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); // key adopts String serialization method template.setKeySerializer(stringRedisSerializer); // The hash key also uses String serialization method template.setHashKeySerializer(stringRedisSerializer); //The value serialization method uses jackson template.setValueSerializer(jackson2JsonRedisSerializer); // The value serialization method of hash uses Jackson template.setHashValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); return template; } }
- Add the inventory quantity of product P0001 to 10000 in redis
- Business test without locking using redis
– Business test code
/** * Use redis without locking */ @Override public void checkAndReduceStock() { // 1. Query inventory quantity String stockQuantity = redisTemplate.opsForValue().get("P0001").toString(); // 2. Determine whether the inventory is sufficient if (stockQuantity != null & amp; & amp; stockQuantity.length() != 0) { Integer quantity = Integer.valueOf(stockQuantity); if (quantity > 0) { // 3. Deduct inventory redisTemplate.opsForValue().set("P0001", String.valueOf(--quantity)); } } }– Use jmeter stress test to check the test results: the inventory has not been reduced to 0, indicating that there is an “oversold” problem.
- Use the setnx command of redis to lock, open three identical services, and use jmeter to perform stress testing
– redis lock test code
/** * Use redis to lock * */ @Override public void checkAndReduceStock() { // 1. Use setnx to lock Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock-stock", "0000"); // 2. Retry: recursive call, if the lock cannot be obtained if (!lock) { try { //pause 50ms Thread.sleep(50); this.checkAndReduceStock(); } catch (InterruptedException e) { e.printStackTrace(); } } else { try { // 3. Query inventory quantity String stockQuantity = (String) redisTemplate.opsForValue().get("P0001"); // 4. Determine whether the inventory is sufficient if (stockQuantity != null & amp; & amp; stockQuantity.length() != 0) { Integer quantity = Integer.valueOf(stockQuantity); if (quantity > 0) { // 5. Deduct inventory redisTemplate.opsForValue().set("P0001", String.valueOf(--quantity)); } } else { System.out.println("This inventory does not exist!"); } } finally { // 5. Unlock redisTemplate.delete("lock-stock"); } } }– Start services 7000, 7001, 7002
– jmeter Stress test results: average access time 364ms, interface throughput 249 per second
– The redis database inventory result is: 0, the concurrent “oversold” problem is solved
- The above common locking methods have deadlock problems and solutions to the deadlock problems
– Causes of deadlock: Under normal circumstances, the above-mentioned redis locking can solve the problem of concurrent access, but there are also deadlock problems. For example, after the 7000 service obtains the lock, the lock is not released due to a service exception. Then the 7001 and 7002 services will never be able to obtain the lock.
– Solution: Set expiration time for the lock and automatically release the lock
① Use expire to set the expiration time (lack of atomicity: if an exception occurs between setnx and expire, the lock cannot be released)
② Use the setex command to set the expiration time: set key value ex 3 nx (to ensure that the atomic operation not only achieves the effect of setnx, but also sets the expiration time)
– Code
public void checkAndReduceStock() { // 1. Use setex to lock to ensure the atomicity of the lock and the lock can be automatically released Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock-stock", "0000",3, TimeUnit.SECONDS); // 2. Retry: recursive call, if the lock cannot be obtained if (!lock) { try { //pause 50ms Thread.sleep(50); this.checkAndReduceStock(); } catch (InterruptedException e) { e.printStackTrace(); } } else { try { // 3. Query inventory quantity String stockQuantity = (String) redisTemplate.opsForValue().get("P0001"); // 4. Determine whether the inventory is sufficient if (stockQuantity != null & amp; & amp; stockQuantity.length() != 0) { Integer quantity = Integer.valueOf(stockQuantity); if (quantity > 0) { // 5. Deduct inventory redisTemplate.opsForValue().set("P0001", String.valueOf(--quantity)); } } else { System.out.println("This inventory does not exist!"); } } finally { // 5. Unlock redisTemplate.delete("lock-stock"); } } }– Test result: Inventory deduction is 0 and the lock is released
- Prevent accidental deletion. In the above ordinary locking method, the lock may be accidentally deleted
– The reason why the lock is accidentally deleted: In the above locking scenario, the following situation will occur. After request method A acquires the lock, the lock is automatically released before the business is completed. At this time, request method B will also acquire the lock. When the lock is reached, before B business is completed, A completes the execution and performs manual deletion of the lock operation. At this time, the lock of B business will be released, resulting in B being released just after acquiring the lock, resulting in subsequent concurrent access problems. .
– Concurrency issues caused by accidental deletion of simulation locks
– Inventory deduction result: no deduction is 0, resulting in concurrency problems
– Solution: Each request uses a globally unique UUID as the value. Before deleting the lock, first determine whether the value is the same, and then delete the lock if it is the same.
public void checkAndReduceStock() { // 1. Use setex to lock to ensure the atomicity of the lock and the lock can be automatically released String uuid = UUID.randomUUID().toString(); Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock-stock", uuid, 1, TimeUnit.SECONDS); // 2. Retry: recursive call, if the lock cannot be obtained if (!lock) { try { //pause 50ms Thread.sleep(10); this.checkAndReduceStock(); } catch (InterruptedException e) { e.printStackTrace(); } } else { try { // 3. Query inventory quantity String stockQuantity = (String) redisTemplate.opsForValue().get("P0001"); // 4. Determine whether the inventory is sufficient if (stockQuantity != null & amp; & amp; stockQuantity.length() != 0) { Integer quantity = Integer.valueOf(stockQuantity); if (quantity > 0) { // 5. Deduct inventory redisTemplate.opsForValue().set("P0001", String.valueOf(--quantity)); } } else { System.out.println("This inventory does not exist!"); } } finally { // 5. First determine whether it is your own lock, and then unlock it String redisUuid = (String) redisTemplate.opsForValue().get("lock-stock"); if (StringUtils.equals(uuid, redisUuid)) { redisTemplate.delete("lock-stock"); } } } }– Existing problems: Since the operations of judging locks and unlocking are not atomic, there will still be accidental deletion operations. For example, when A requests to delete the lock after completing the judgment, A’s lock is automatically released at this time, and B requests to obtain it. lock. At this time, request A will manually delete the lock requested by B, and the problem of concurrent access still exists. The probability is very small.
- Use Lua script to solve the problem of manual lock release and deletion, which is an atomic operation
– Lua code to solve accidental deletion operation
public void checkAndReduceStock() { // 1. Use setex to lock to ensure the atomicity of the lock and the lock can be automatically released String uuid = UUID.randomUUID().toString(); Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock-stock", uuid, 1, TimeUnit.SECONDS); // 2. Retry: recursive call, if the lock cannot be obtained if (!lock) { try { //pause 50ms Thread.sleep(10); this.checkAndReduceStock(); } catch (InterruptedException e) { e.printStackTrace(); } } else { try { // 3. Query inventory quantity String stockQuantity = (String) redisTemplate.opsForValue().get("P0001"); // 4. Determine whether the inventory is sufficient if (stockQuantity != null & amp; & amp; stockQuantity.length() != 0) { Integer quantity = Integer.valueOf(stockQuantity); if (quantity > 0) { // 5. Deduct inventory redisTemplate.opsForValue().set("P0001", String.valueOf(--quantity)); } } else { System.out.println("This inventory does not exist!"); } } finally { // 5. First determine whether it is your own lock, and then unlock it String script = "if redis.call('get', KEYS[1]) == ARGV[1] " + "then " + " return redis.call('del', KEYS[1]) " + "else " + " return 0 " + "end"; redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList("lock-stock"), uuid); } } }
Conclusion
The content about using redis distributed locks to solve the “oversold” problem ends here. See you in the next issue. . . . . .
The knowledge points of the article match the official knowledge files, and you can further learn relevant knowledge. Cloud native entry-level skills treeHomepageOverview 16,750 people are learning the system