前言
之前项目需要对数据库数据下的记录进行更新的时候设计多个进程中多线程操作,为保证数据的准确需要进行加锁操作;一开始通过setnx那种形式,在没有获取到锁的时候,线程不能等待,除非无限循环,但比较耗费行能,后来就想着能否像synchronized那样自己等待,由此出发。
项目结构
redis配置
redis的配置类,根据自己看着配,没啥说的
@Configuration
public class RedisConfiguration {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
redis监听配置
一、redis监听容器
@Configuration
public class RedisLockMessageListenerContainer {
@Bean
public MessageListenerAdapter messageListenerAdapter() {
return new MessageListenerAdapter(new RedisLockListener());
}
@Bean
public RedisMessageListenerContainer redisContainer(RedisConnectionFactory factory) {
final RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(factory);
container.addMessageListener(messageListenerAdapter(), new ChannelTopic("redis-topic"));
return container;
}
}
二、实际监听类
@Slf4j
@Component
public class RedisLockListener implements MessageListener {
@Override
public void onMessage(Message message, byte[] bytes) {
byte[] channel = message.getChannel();
byte[] body = message.getBody();
Jackson2JsonRedisSerializer<String> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<String>(String.class);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
String msg = jackson2JsonRedisSerializer.deserialize(body);
String topic = stringRedisSerializer.deserialize(channel);
if (topic.equals("redis-topic")) {
RedisUtil redisUtil = SpringUtil.getBean(RedisUtil.class);
redisUtil.notifyLock(msg);
}
}
}
redis操作工具
@Slf4j
@Component
public class RedisUtil {
private static Map<String, String> synchronizedMap = new HashMap<>();
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void publish(String topic, String msg) {
redisTemplate.convertAndSend(topic, msg);
}
public Boolean getLock(String key, String value, Long timeOut) {
boolean flag = redisTemplate.opsForValue().setIfAbsent(key, value, timeOut, TimeUnit.SECONDS);
log.info("redis获取锁:{}-{}-{}", key, value, flag);
return flag;
}
@SneakyThrows
public void getLock(String lock) {
while(!getLock(lock,lock,100l)) {
String lockKey = getValue(lock);
synchronized (lockKey) {
lockKey.wait();
}
}
}
private String getValue(String lock) {
synchronizedMap.putIfAbsent(lock, lock);
return synchronizedMap.get(lock);
}
public void notifyLock(String key) {
String lock = synchronizedMap.get(key);
if (null != lock) {
synchronized (lock) {
synchronizedMap.remove(lock);
lock.notifyAll();
}
}
}
public void delete(String lock) {
redisTemplate.delete(lock);
}
}
这里主要想法就是redis在获取不到锁的时候通过synchronized和wait方法阻塞线程,等待监听到后唤醒线程;getValue这个方法的作用是保证用到同一个锁的时候,用同一个对象去锁线程(此处一直认为这个方法应该加上synchronized,但是后期测试的时候发现不加也行,想不明白,这里我去了),例如:A、B、C三个线程都是操作同一条数据的时候,A进程先执行,B、C需要加锁,若用不同的对象锁的话,那么监听到的时候就需要不同的对象去唤醒,所以这里对同一条数据的多个线程操作采用同个锁;而在不同进程间通过redis本身就可以控制了。
测试
随便写了简单的测试
@Slf4j
@RestController
public class TestController {
@Autowired
TestService testService;
@GetMapping("/test")
public void test() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 0L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
for (int i = 0; i < 10; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
testService.add(Thread.currentThread().getName());
}
});
}
}
@GetMapping("/getnum")
public void getNum() {
log.info("**************{}", testService.getNum());
}
}
@Slf4j
@Service
public class TestService {
@Autowired
RedisUtil redisUtil;
int num = 1;
public void add(String name) {
log.info("step into thread-{}........", name);
redisUtil.getLock("lock");
log.info("{} get lock true===========", name);
num = num + 1;
log.info("{} execute end -----------", name);
redisUtil.delete("lock");
redisUtil.publish("redis-topic", "lock");
}
public int getNum() {
return num;
}
}
启动项目,可以改变端口号启动多次,模拟多个进程,这边只启动一个进程不多做测试了,因为在实际运用中已测过多个进程
先通过 http://localhost:9999/getnum看下num值
再调用 http://localhost:9999/test,可以观察下日志
再调用下getnum查看num值
配置文件
server.port=9999
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
#spring.redis.password=aaa
spring.redis.jedis.pool.max-active=20
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.max-wait=-1
spring.redis.jedis.pool.min-idle=0
spring.redis.timeout=1000