Redis 分布式锁原理与实现
Java 应用在多线程环境下,我们可以通过 Java 内存模型实现同步,比如 Lock,synchronized 等, 但是在分布式环境下,特别是现在微服务盛行的时代,服务为了高可用会做集群。在这样的情况下每个服务都有自己独立进程,当高并发的情况下,会存在同步问题,本文主要记录自己学习Redis分布式锁的过程。从浅到深一步步通过代码去分析。
1. 什么场景下用分布式锁
比如,有一个账户服务对用户账户进行金额操作并且做了集群。此时有A,B两个请求同时对账户金额进行操作,操作步骤如下:
- 从数据库读取账户金额
- 对账户金额进行操作
- 保存账户金额到数据库
假如不做互斥,A得到账户金额,还没等A先修改完保存,B也从数据库中得到账户金额,那么等A处理完金额保存后,B随后的保存就将覆盖A的保存。
2. 场景分析和解决方案
我们通过代码演示不同的场景下,我们会出现什么样的问题,并且怎么样去解决。
2.1 不做任何同步
可以通过代码来解释这种情况,由于本地学习,用 Redis 模拟数据库存储账户金额。用多线程来模拟分布式环境,同时对账户进行操作。
UserAccount.java
//用户账户Model
public class UserAccount {
//用户ID
private String userId;
//账户内金额
private int amount;
public UserAccount(String userId, int amount) {
this.userId = userId;
this.amount = amount;
}
public UserAccount() {
}
//添加账户金额
public void addAmount(int amount) {
this.amount = this.amount + amount;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public int getAmount() {
return amount;
}
public void setAmount(int amount) {
this.amount = amount;
}
}
AccountOperationThread.java
线程,模拟分布式环境
/**
* 账户操作
* 多线程方式模拟分布式环境
*/
public class AccountOperationThread implements Runnable {
private final static Logger logger = LoggerFactory.getLogger(AccountOperationThread.class);
//操作金额
private int amount;
private String userId;
private RedisTemplate redisTemplate;
public AccountOperationThread(String userId, int amount) {
this.amount = amount;
this.userId = userId;
redisTemplate = (RedisTemplate) SpringBeanUtil.getBeanByName("redisTemplate");
}
@Override
public void run() {
noLock();
}
/**
* 不做任何同步(锁)
*/
private void noLock() {
try {
Random random = new Random();
// 为了更好测试,模拟线程先后进入,每个线程随机休眠 1-100毫秒再进行业务操作
TimeUnit.MILLISECONDS.sleep(random.nextInt(100) + 1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//模拟数据库中获取用户账号
UserAccount userAccount = (UserAccount) redisTemplate.opsForValue().get(userId);
//设置金额
userAccount.setAmount(userAccount.getAmount() + amount);
logger.info(Thread.currentThread().getName() + " : user id : " + userId + " amount : " + userAccount.getAmount());
//模拟存回数据库
redisTemplate.opsForValue().set(userId, userAccount);
}
}
为了方便测试,我用的是SpringBoot
,RedisTemplate
配置了自定义序列化方式,TestController
来进行测试。
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置value的序列化规则和 key的序列化规则
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
@RestController
public class TestController {
private final static Logger logger = LoggerFactory.getLogger(TestController.class)